refactor: 重构数据库配置为SQLite开发环境并移除冗余文档
This commit is contained in:
456
mini_program/common/api/common.js
Normal file
456
mini_program/common/api/common.js
Normal file
@@ -0,0 +1,456 @@
|
||||
/**
|
||||
* 通用 API 接口
|
||||
*/
|
||||
|
||||
import request from '@/common/utils/request'
|
||||
|
||||
export const commonApi = {
|
||||
/**
|
||||
* 文件上传
|
||||
* @param {File} file 文件对象
|
||||
* @param {Object} options 上传选项
|
||||
*/
|
||||
uploadFile(file, options = {}) {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
|
||||
// 添加额外参数
|
||||
Object.keys(options).forEach(key => {
|
||||
formData.append(key, options[key])
|
||||
})
|
||||
|
||||
return request.post('/common/upload', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
},
|
||||
timeout: 60000 // 60秒超时
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 批量文件上传
|
||||
* @param {FileList} files 文件列表
|
||||
* @param {Object} options 上传选项
|
||||
*/
|
||||
uploadMultipleFiles(files, options = {}) {
|
||||
const formData = new FormData()
|
||||
|
||||
// 添加多个文件
|
||||
Array.from(files).forEach((file, index) => {
|
||||
formData.append(`files[${index}]`, file)
|
||||
})
|
||||
|
||||
// 添加额外参数
|
||||
Object.keys(options).forEach(key => {
|
||||
formData.append(key, options[key])
|
||||
})
|
||||
|
||||
return request.post('/common/upload/multiple', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
},
|
||||
timeout: 120000 // 2分钟超时
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取文件信息
|
||||
* @param {string} fileId 文件ID
|
||||
*/
|
||||
getFileInfo(fileId) {
|
||||
return request.get(`/common/files/${fileId}`)
|
||||
},
|
||||
|
||||
/**
|
||||
* 删除文件
|
||||
* @param {string} fileId 文件ID
|
||||
*/
|
||||
deleteFile(fileId) {
|
||||
return request.delete(`/common/files/${fileId}`)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取省市区数据
|
||||
* @param {number} parentId 父级ID,不传则获取省份
|
||||
*/
|
||||
getRegions(parentId) {
|
||||
const params = parentId ? { parentId } : {}
|
||||
return request.get('/common/regions', { params })
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取完整地区树
|
||||
*/
|
||||
getRegionTree() {
|
||||
return request.get('/common/regions/tree')
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取字典数据
|
||||
* @param {string} type 字典类型
|
||||
*/
|
||||
getDictData(type) {
|
||||
return request.get(`/common/dict/${type}`)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取多个字典数据
|
||||
* @param {Array} types 字典类型数组
|
||||
*/
|
||||
getMultipleDictData(types) {
|
||||
return request.post('/common/dict/multiple', { types })
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取系统配置
|
||||
* @param {string} key 配置键名
|
||||
*/
|
||||
getSystemConfig(key) {
|
||||
return request.get(`/common/config/${key}`)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取多个系统配置
|
||||
* @param {Array} keys 配置键名数组
|
||||
*/
|
||||
getMultipleSystemConfig(keys) {
|
||||
return request.post('/common/config/multiple', { keys })
|
||||
},
|
||||
|
||||
/**
|
||||
* 发送短信验证码
|
||||
* @param {Object} data 发送数据
|
||||
* @param {string} data.phone 手机号
|
||||
* @param {string} data.type 验证码类型
|
||||
* @param {string} data.template 短信模板
|
||||
*/
|
||||
sendSmsCode(data) {
|
||||
return request.post('/common/sms/send-code', data)
|
||||
},
|
||||
|
||||
/**
|
||||
* 验证短信验证码
|
||||
* @param {Object} data 验证数据
|
||||
* @param {string} data.phone 手机号
|
||||
* @param {string} data.code 验证码
|
||||
* @param {string} data.type 验证码类型
|
||||
*/
|
||||
verifySmsCode(data) {
|
||||
return request.post('/common/sms/verify-code', data)
|
||||
},
|
||||
|
||||
/**
|
||||
* 发送邮件
|
||||
* @param {Object} emailData 邮件数据
|
||||
* @param {string} emailData.to 收件人
|
||||
* @param {string} emailData.subject 主题
|
||||
* @param {string} emailData.content 内容
|
||||
* @param {string} emailData.template 邮件模板
|
||||
*/
|
||||
sendEmail(emailData) {
|
||||
return request.post('/common/email/send', emailData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 发送邮箱验证码
|
||||
* @param {Object} data 发送数据
|
||||
* @param {string} data.email 邮箱
|
||||
* @param {string} data.type 验证码类型
|
||||
*/
|
||||
sendEmailCode(data) {
|
||||
return request.post('/common/email/send-code', data)
|
||||
},
|
||||
|
||||
/**
|
||||
* 验证邮箱验证码
|
||||
* @param {Object} data 验证数据
|
||||
* @param {string} data.email 邮箱
|
||||
* @param {string} data.code 验证码
|
||||
* @param {string} data.type 验证码类型
|
||||
*/
|
||||
verifyEmailCode(data) {
|
||||
return request.post('/common/email/verify-code', data)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取二维码
|
||||
* @param {Object} qrData 二维码数据
|
||||
* @param {string} qrData.content 二维码内容
|
||||
* @param {number} qrData.size 二维码大小
|
||||
* @param {string} qrData.format 图片格式
|
||||
*/
|
||||
generateQRCode(qrData) {
|
||||
return request.post('/common/qrcode/generate', qrData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 解析二维码
|
||||
* @param {File} file 二维码图片文件
|
||||
*/
|
||||
parseQRCode(file) {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
|
||||
return request.post('/common/qrcode/parse', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取天气信息
|
||||
* @param {Object} params 查询参数
|
||||
* @param {string} params.city 城市名称
|
||||
* @param {number} params.days 预报天数
|
||||
*/
|
||||
getWeatherInfo(params) {
|
||||
return request.get('/common/weather', { params })
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取IP信息
|
||||
* @param {string} ip IP地址,不传则获取当前IP信息
|
||||
*/
|
||||
getIpInfo(ip) {
|
||||
const params = ip ? { ip } : {}
|
||||
return request.get('/common/ip-info', { params })
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取系统时间
|
||||
*/
|
||||
getSystemTime() {
|
||||
return request.get('/common/system-time')
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取系统信息
|
||||
*/
|
||||
getSystemInfo() {
|
||||
return request.get('/common/system-info')
|
||||
},
|
||||
|
||||
/**
|
||||
* 健康检查
|
||||
*/
|
||||
healthCheck() {
|
||||
return request.get('/common/health')
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取版本信息
|
||||
*/
|
||||
getVersionInfo() {
|
||||
return request.get('/common/version')
|
||||
},
|
||||
|
||||
/**
|
||||
* 意见反馈
|
||||
* @param {Object} feedbackData 反馈数据
|
||||
* @param {string} feedbackData.type 反馈类型
|
||||
* @param {string} feedbackData.content 反馈内容
|
||||
* @param {string} feedbackData.contact 联系方式
|
||||
*/
|
||||
submitFeedback(feedbackData) {
|
||||
return request.post('/common/feedback', feedbackData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取反馈列表
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
getFeedbackList(params) {
|
||||
return request.get('/common/feedback', { params })
|
||||
},
|
||||
|
||||
/**
|
||||
* 处理反馈
|
||||
* @param {number} id 反馈ID
|
||||
* @param {Object} handleData 处理数据
|
||||
*/
|
||||
handleFeedback(id, handleData) {
|
||||
return request.post(`/common/feedback/${id}/handle`, handleData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取公告列表
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
getAnnouncementList(params) {
|
||||
return request.get('/common/announcements', { params })
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取公告详情
|
||||
* @param {number} id 公告ID
|
||||
*/
|
||||
getAnnouncementDetail(id) {
|
||||
return request.get(`/common/announcements/${id}`)
|
||||
},
|
||||
|
||||
/**
|
||||
* 标记公告已读
|
||||
* @param {number} id 公告ID
|
||||
*/
|
||||
markAnnouncementRead(id) {
|
||||
return request.post(`/common/announcements/${id}/read`)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取常见问题
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
getFaqList(params) {
|
||||
return request.get('/common/faq', { params })
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取帮助文档
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
getHelpDocuments(params) {
|
||||
return request.get('/common/help', { params })
|
||||
},
|
||||
|
||||
/**
|
||||
* 搜索帮助文档
|
||||
* @param {string} keyword 搜索关键词
|
||||
*/
|
||||
searchHelpDocuments(keyword) {
|
||||
return request.get('/common/help/search', { params: { keyword } })
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取联系方式
|
||||
*/
|
||||
getContactInfo() {
|
||||
return request.get('/common/contact')
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取关于我们信息
|
||||
*/
|
||||
getAboutInfo() {
|
||||
return request.get('/common/about')
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取隐私政策
|
||||
*/
|
||||
getPrivacyPolicy() {
|
||||
return request.get('/common/privacy-policy')
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取用户协议
|
||||
*/
|
||||
getUserAgreement() {
|
||||
return request.get('/common/user-agreement')
|
||||
},
|
||||
|
||||
/**
|
||||
* 数据统计上报
|
||||
* @param {Object} statsData 统计数据
|
||||
*/
|
||||
reportStats(statsData) {
|
||||
return request.post('/common/stats/report', statsData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 错误日志上报
|
||||
* @param {Object} errorData 错误数据
|
||||
*/
|
||||
reportError(errorData) {
|
||||
return request.post('/common/error/report', errorData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 性能数据上报
|
||||
* @param {Object} performanceData 性能数据
|
||||
*/
|
||||
reportPerformance(performanceData) {
|
||||
return request.post('/common/performance/report', performanceData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取热门搜索关键词
|
||||
* @param {string} type 搜索类型
|
||||
*/
|
||||
getHotSearchKeywords(type) {
|
||||
return request.get('/common/hot-search', { params: { type } })
|
||||
},
|
||||
|
||||
/**
|
||||
* 搜索建议
|
||||
* @param {Object} params 搜索参数
|
||||
* @param {string} params.keyword 关键词
|
||||
* @param {string} params.type 搜索类型
|
||||
*/
|
||||
getSearchSuggestions(params) {
|
||||
return request.get('/common/search-suggestions', { params })
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取轮播图
|
||||
* @param {string} position 位置标识
|
||||
*/
|
||||
getBannerList(position) {
|
||||
return request.get('/common/banners', { params: { position } })
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取广告列表
|
||||
* @param {string} position 位置标识
|
||||
*/
|
||||
getAdList(position) {
|
||||
return request.get('/common/ads', { params: { position } })
|
||||
},
|
||||
|
||||
/**
|
||||
* 广告点击统计
|
||||
* @param {number} adId 广告ID
|
||||
*/
|
||||
clickAd(adId) {
|
||||
return request.post(`/common/ads/${adId}/click`)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取推送消息列表
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
getPushMessageList(params) {
|
||||
return request.get('/common/push-messages', { params })
|
||||
},
|
||||
|
||||
/**
|
||||
* 标记推送消息已读
|
||||
* @param {number} id 消息ID
|
||||
*/
|
||||
markPushMessageRead(id) {
|
||||
return request.post(`/common/push-messages/${id}/read`)
|
||||
},
|
||||
|
||||
/**
|
||||
* 批量标记推送消息已读
|
||||
* @param {Array} ids 消息ID数组
|
||||
*/
|
||||
batchMarkPushMessageRead(ids) {
|
||||
return request.post('/common/push-messages/batch-read', { ids })
|
||||
},
|
||||
|
||||
/**
|
||||
* 删除推送消息
|
||||
* @param {number} id 消息ID
|
||||
*/
|
||||
deletePushMessage(id) {
|
||||
return request.delete(`/common/push-messages/${id}`)
|
||||
},
|
||||
|
||||
/**
|
||||
* 清空推送消息
|
||||
*/
|
||||
clearPushMessages() {
|
||||
return request.delete('/common/push-messages/clear')
|
||||
}
|
||||
}
|
||||
219
mini_program/common/api/farming.js
Normal file
219
mini_program/common/api/farming.js
Normal file
@@ -0,0 +1,219 @@
|
||||
/**
|
||||
* 养殖管理相关 API 接口
|
||||
*/
|
||||
|
||||
import request from '@/common/utils/request'
|
||||
|
||||
export const farmingApi = {
|
||||
/**
|
||||
* 获取养殖场列表
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
getFarmList(params) {
|
||||
return request.get('/farming/farms', { params })
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取养殖场详情
|
||||
* @param {number} id 养殖场ID
|
||||
*/
|
||||
getFarmDetail(id) {
|
||||
return request.get(`/farming/farms/${id}`)
|
||||
},
|
||||
|
||||
/**
|
||||
* 创建养殖场
|
||||
* @param {Object} farmData 养殖场数据
|
||||
*/
|
||||
createFarm(farmData) {
|
||||
return request.post('/farming/farms', farmData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新养殖场信息
|
||||
* @param {number} id 养殖场ID
|
||||
* @param {Object} farmData 养殖场数据
|
||||
*/
|
||||
updateFarm(id, farmData) {
|
||||
return request.put(`/farming/farms/${id}`, farmData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 删除养殖场
|
||||
* @param {number} id 养殖场ID
|
||||
*/
|
||||
deleteFarm(id) {
|
||||
return request.delete(`/farming/farms/${id}`)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取动物列表
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
getAnimalList(params) {
|
||||
return request.get('/farming/animals', { params })
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取动物详情
|
||||
* @param {number} id 动物ID
|
||||
*/
|
||||
getAnimalDetail(id) {
|
||||
return request.get(`/farming/animals/${id}`)
|
||||
},
|
||||
|
||||
/**
|
||||
* 添加动物
|
||||
* @param {Object} animalData 动物数据
|
||||
*/
|
||||
addAnimal(animalData) {
|
||||
return request.post('/farming/animals', animalData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新动物信息
|
||||
* @param {number} id 动物ID
|
||||
* @param {Object} animalData 动物数据
|
||||
*/
|
||||
updateAnimal(id, animalData) {
|
||||
return request.put(`/farming/animals/${id}`, animalData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 删除动物
|
||||
* @param {number} id 动物ID
|
||||
*/
|
||||
deleteAnimal(id) {
|
||||
return request.delete(`/farming/animals/${id}`)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取健康记录
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
getHealthRecords(params) {
|
||||
return request.get('/farming/health-records', { params })
|
||||
},
|
||||
|
||||
/**
|
||||
* 添加健康记录
|
||||
* @param {Object} recordData 健康记录数据
|
||||
*/
|
||||
addHealthRecord(recordData) {
|
||||
return request.post('/farming/health-records', recordData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取疫苗记录
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
getVaccineRecords(params) {
|
||||
return request.get('/farming/vaccine-records', { params })
|
||||
},
|
||||
|
||||
/**
|
||||
* 添加疫苗记录
|
||||
* @param {Object} recordData 疫苗记录数据
|
||||
*/
|
||||
addVaccineRecord(recordData) {
|
||||
return request.post('/farming/vaccine-records', recordData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取饲料记录
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
getFeedRecords(params) {
|
||||
return request.get('/farming/feed-records', { params })
|
||||
},
|
||||
|
||||
/**
|
||||
* 添加饲料记录
|
||||
* @param {Object} recordData 饲料记录数据
|
||||
*/
|
||||
addFeedRecord(recordData) {
|
||||
return request.post('/farming/feed-records', recordData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取生产记录
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
getProductionRecords(params) {
|
||||
return request.get('/farming/production-records', { params })
|
||||
},
|
||||
|
||||
/**
|
||||
* 添加生产记录
|
||||
* @param {Object} recordData 生产记录数据
|
||||
*/
|
||||
addProductionRecord(recordData) {
|
||||
return request.post('/farming/production-records', recordData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取统计数据
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
getStatistics(params) {
|
||||
return request.get('/farming/statistics', { params })
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取环境监测数据
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
getEnvironmentData(params) {
|
||||
return request.get('/farming/environment', { params })
|
||||
},
|
||||
|
||||
/**
|
||||
* 添加环境监测数据
|
||||
* @param {Object} envData 环境数据
|
||||
*/
|
||||
addEnvironmentData(envData) {
|
||||
return request.post('/farming/environment', envData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取设备列表
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
getEquipmentList(params) {
|
||||
return request.get('/farming/equipment', { params })
|
||||
},
|
||||
|
||||
/**
|
||||
* 添加设备
|
||||
* @param {Object} equipmentData 设备数据
|
||||
*/
|
||||
addEquipment(equipmentData) {
|
||||
return request.post('/farming/equipment', equipmentData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新设备状态
|
||||
* @param {number} id 设备ID
|
||||
* @param {Object} statusData 状态数据
|
||||
*/
|
||||
updateEquipmentStatus(id, statusData) {
|
||||
return request.put(`/farming/equipment/${id}/status`, statusData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取报警信息
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
getAlerts(params) {
|
||||
return request.get('/farming/alerts', { params })
|
||||
},
|
||||
|
||||
/**
|
||||
* 处理报警
|
||||
* @param {number} id 报警ID
|
||||
* @param {Object} handleData 处理数据
|
||||
*/
|
||||
handleAlert(id, handleData) {
|
||||
return request.put(`/farming/alerts/${id}/handle`, handleData)
|
||||
}
|
||||
}
|
||||
287
mini_program/common/api/finance.js
Normal file
287
mini_program/common/api/finance.js
Normal file
@@ -0,0 +1,287 @@
|
||||
/**
|
||||
* 银行监管相关 API 接口
|
||||
*/
|
||||
|
||||
import request from '@/common/utils/request'
|
||||
|
||||
export const financeApi = {
|
||||
/**
|
||||
* 获取贷款列表
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
getLoanList(params) {
|
||||
return request.get('/finance/loans', { params })
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取贷款详情
|
||||
* @param {number} id 贷款ID
|
||||
*/
|
||||
getLoanDetail(id) {
|
||||
return request.get(`/finance/loans/${id}`)
|
||||
},
|
||||
|
||||
/**
|
||||
* 申请贷款
|
||||
* @param {Object} loanData 贷款申请数据
|
||||
*/
|
||||
applyLoan(loanData) {
|
||||
return request.post('/finance/loans/apply', loanData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 审批贷款
|
||||
* @param {number} id 贷款ID
|
||||
* @param {Object} approvalData 审批数据
|
||||
*/
|
||||
approveLoan(id, approvalData) {
|
||||
return request.post(`/finance/loans/${id}/approve`, approvalData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 拒绝贷款
|
||||
* @param {number} id 贷款ID
|
||||
* @param {Object} rejectData 拒绝数据
|
||||
*/
|
||||
rejectLoan(id, rejectData) {
|
||||
return request.post(`/finance/loans/${id}/reject`, rejectData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 放款
|
||||
* @param {number} id 贷款ID
|
||||
* @param {Object} disbursementData 放款数据
|
||||
*/
|
||||
disburseLoan(id, disbursementData) {
|
||||
return request.post(`/finance/loans/${id}/disburse`, disbursementData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 还款
|
||||
* @param {number} id 贷款ID
|
||||
* @param {Object} repaymentData 还款数据
|
||||
*/
|
||||
repayLoan(id, repaymentData) {
|
||||
return request.post(`/finance/loans/${id}/repay`, repaymentData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取还款计划
|
||||
* @param {number} loanId 贷款ID
|
||||
*/
|
||||
getRepaymentPlan(loanId) {
|
||||
return request.get(`/finance/loans/${loanId}/repayment-plan`)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取还款记录
|
||||
* @param {number} loanId 贷款ID
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
getRepaymentHistory(loanId, params) {
|
||||
return request.get(`/finance/loans/${loanId}/repayment-history`, { params })
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取风险评估
|
||||
* @param {Object} assessmentData 评估数据
|
||||
*/
|
||||
getRiskAssessment(assessmentData) {
|
||||
return request.post('/finance/risk-assessment', assessmentData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取信用报告
|
||||
* @param {number} userId 用户ID
|
||||
*/
|
||||
getCreditReport(userId) {
|
||||
return request.get(`/finance/credit-report/${userId}`)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取抵押物列表
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
getCollateralList(params) {
|
||||
return request.get('/finance/collaterals', { params })
|
||||
},
|
||||
|
||||
/**
|
||||
* 添加抵押物
|
||||
* @param {Object} collateralData 抵押物数据
|
||||
*/
|
||||
addCollateral(collateralData) {
|
||||
return request.post('/finance/collaterals', collateralData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 评估抵押物
|
||||
* @param {number} id 抵押物ID
|
||||
* @param {Object} evaluationData 评估数据
|
||||
*/
|
||||
evaluateCollateral(id, evaluationData) {
|
||||
return request.post(`/finance/collaterals/${id}/evaluate`, evaluationData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取担保人列表
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
getGuarantorList(params) {
|
||||
return request.get('/finance/guarantors', { params })
|
||||
},
|
||||
|
||||
/**
|
||||
* 添加担保人
|
||||
* @param {Object} guarantorData 担保人数据
|
||||
*/
|
||||
addGuarantor(guarantorData) {
|
||||
return request.post('/finance/guarantors', guarantorData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 验证担保人
|
||||
* @param {number} id 担保人ID
|
||||
* @param {Object} verificationData 验证数据
|
||||
*/
|
||||
verifyGuarantor(id, verificationData) {
|
||||
return request.post(`/finance/guarantors/${id}/verify`, verificationData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取监管报告
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
getSupervisionReports(params) {
|
||||
return request.get('/finance/supervision-reports', { params })
|
||||
},
|
||||
|
||||
/**
|
||||
* 生成监管报告
|
||||
* @param {Object} reportData 报告数据
|
||||
*/
|
||||
generateSupervisionReport(reportData) {
|
||||
return request.post('/finance/supervision-reports/generate', reportData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取合规检查记录
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
getComplianceChecks(params) {
|
||||
return request.get('/finance/compliance-checks', { params })
|
||||
},
|
||||
|
||||
/**
|
||||
* 执行合规检查
|
||||
* @param {Object} checkData 检查数据
|
||||
*/
|
||||
performComplianceCheck(checkData) {
|
||||
return request.post('/finance/compliance-checks', checkData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取审计日志
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
getAuditLogs(params) {
|
||||
return request.get('/finance/audit-logs', { params })
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取统计数据
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
getFinanceStats(params) {
|
||||
return request.get('/finance/statistics', { params })
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取风险预警
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
getRiskAlerts(params) {
|
||||
return request.get('/finance/risk-alerts', { params })
|
||||
},
|
||||
|
||||
/**
|
||||
* 处理风险预警
|
||||
* @param {number} id 预警ID
|
||||
* @param {Object} handleData 处理数据
|
||||
*/
|
||||
handleRiskAlert(id, handleData) {
|
||||
return request.post(`/finance/risk-alerts/${id}/handle`, handleData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取利率配置
|
||||
*/
|
||||
getInterestRates() {
|
||||
return request.get('/finance/interest-rates')
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新利率配置
|
||||
* @param {Object} rateData 利率数据
|
||||
*/
|
||||
updateInterestRates(rateData) {
|
||||
return request.put('/finance/interest-rates', rateData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取贷款产品
|
||||
*/
|
||||
getLoanProducts() {
|
||||
return request.get('/finance/loan-products')
|
||||
},
|
||||
|
||||
/**
|
||||
* 创建贷款产品
|
||||
* @param {Object} productData 产品数据
|
||||
*/
|
||||
createLoanProduct(productData) {
|
||||
return request.post('/finance/loan-products', productData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新贷款产品
|
||||
* @param {number} id 产品ID
|
||||
* @param {Object} productData 产品数据
|
||||
*/
|
||||
updateLoanProduct(id, productData) {
|
||||
return request.put(`/finance/loan-products/${id}`, productData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取银行账户信息
|
||||
* @param {number} userId 用户ID
|
||||
*/
|
||||
getBankAccounts(userId) {
|
||||
return request.get(`/finance/bank-accounts/${userId}`)
|
||||
},
|
||||
|
||||
/**
|
||||
* 绑定银行账户
|
||||
* @param {Object} accountData 账户数据
|
||||
*/
|
||||
bindBankAccount(accountData) {
|
||||
return request.post('/finance/bank-accounts/bind', accountData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 解绑银行账户
|
||||
* @param {number} accountId 账户ID
|
||||
*/
|
||||
unbindBankAccount(accountId) {
|
||||
return request.delete(`/finance/bank-accounts/${accountId}`)
|
||||
},
|
||||
|
||||
/**
|
||||
* 验证银行账户
|
||||
* @param {number} accountId 账户ID
|
||||
* @param {Object} verificationData 验证数据
|
||||
*/
|
||||
verifyBankAccount(accountId, verificationData) {
|
||||
return request.post(`/finance/bank-accounts/${accountId}/verify`, verificationData)
|
||||
}
|
||||
}
|
||||
24
mini_program/common/api/index.js
Normal file
24
mini_program/common/api/index.js
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* API 接口统一入口文件
|
||||
* 导出所有 API 模块
|
||||
*/
|
||||
|
||||
// 导入请求工具
|
||||
import request from '@/common/utils/request'
|
||||
|
||||
// 导入所有 API 模块
|
||||
export { userApi } from './user'
|
||||
export { farmingApi } from './farming'
|
||||
export { tradingApi } from './trading'
|
||||
export { mallApi } from './mall'
|
||||
export { financeApi } from './finance'
|
||||
export { insuranceApi } from './insurance'
|
||||
export { commonApi } from './common'
|
||||
|
||||
// 导出请求工具
|
||||
export { request }
|
||||
|
||||
// 默认导出
|
||||
export default {
|
||||
request
|
||||
}
|
||||
337
mini_program/common/api/insurance.js
Normal file
337
mini_program/common/api/insurance.js
Normal file
@@ -0,0 +1,337 @@
|
||||
/**
|
||||
* 保险监管相关 API 接口
|
||||
*/
|
||||
|
||||
import request from '@/common/utils/request'
|
||||
|
||||
export const insuranceApi = {
|
||||
/**
|
||||
* 获取保险产品列表
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
getInsuranceProducts(params) {
|
||||
return request.get('/insurance/products', { params })
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取保险产品详情
|
||||
* @param {number} id 产品ID
|
||||
*/
|
||||
getInsuranceProductDetail(id) {
|
||||
return request.get(`/insurance/products/${id}`)
|
||||
},
|
||||
|
||||
/**
|
||||
* 创建保险产品
|
||||
* @param {Object} productData 产品数据
|
||||
*/
|
||||
createInsuranceProduct(productData) {
|
||||
return request.post('/insurance/products', productData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新保险产品
|
||||
* @param {number} id 产品ID
|
||||
* @param {Object} productData 产品数据
|
||||
*/
|
||||
updateInsuranceProduct(id, productData) {
|
||||
return request.put(`/insurance/products/${id}`, productData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取保险政策列表
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
getInsurancePolicies(params) {
|
||||
return request.get('/insurance/policies', { params })
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取保险政策详情
|
||||
* @param {number} id 政策ID
|
||||
*/
|
||||
getInsurancePolicyDetail(id) {
|
||||
return request.get(`/insurance/policies/${id}`)
|
||||
},
|
||||
|
||||
/**
|
||||
* 申请保险
|
||||
* @param {Object} applicationData 申请数据
|
||||
*/
|
||||
applyInsurance(applicationData) {
|
||||
return request.post('/insurance/applications', applicationData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取保险申请列表
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
getInsuranceApplications(params) {
|
||||
return request.get('/insurance/applications', { params })
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取保险申请详情
|
||||
* @param {number} id 申请ID
|
||||
*/
|
||||
getInsuranceApplicationDetail(id) {
|
||||
return request.get(`/insurance/applications/${id}`)
|
||||
},
|
||||
|
||||
/**
|
||||
* 审核保险申请
|
||||
* @param {number} id 申请ID
|
||||
* @param {Object} reviewData 审核数据
|
||||
*/
|
||||
reviewInsuranceApplication(id, reviewData) {
|
||||
return request.post(`/insurance/applications/${id}/review`, reviewData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 承保
|
||||
* @param {number} id 申请ID
|
||||
* @param {Object} underwritingData 承保数据
|
||||
*/
|
||||
underwriteInsurance(id, underwritingData) {
|
||||
return request.post(`/insurance/applications/${id}/underwrite`, underwritingData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取理赔申请列表
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
getClaimApplications(params) {
|
||||
return request.get('/insurance/claims', { params })
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取理赔申请详情
|
||||
* @param {number} id 理赔ID
|
||||
*/
|
||||
getClaimApplicationDetail(id) {
|
||||
return request.get(`/insurance/claims/${id}`)
|
||||
},
|
||||
|
||||
/**
|
||||
* 提交理赔申请
|
||||
* @param {Object} claimData 理赔数据
|
||||
*/
|
||||
submitClaimApplication(claimData) {
|
||||
return request.post('/insurance/claims', claimData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 审核理赔申请
|
||||
* @param {number} id 理赔ID
|
||||
* @param {Object} reviewData 审核数据
|
||||
*/
|
||||
reviewClaimApplication(id, reviewData) {
|
||||
return request.post(`/insurance/claims/${id}/review`, reviewData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 理赔调查
|
||||
* @param {number} id 理赔ID
|
||||
* @param {Object} investigationData 调查数据
|
||||
*/
|
||||
investigateClaim(id, investigationData) {
|
||||
return request.post(`/insurance/claims/${id}/investigate`, investigationData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 理赔结算
|
||||
* @param {number} id 理赔ID
|
||||
* @param {Object} settlementData 结算数据
|
||||
*/
|
||||
settleClaim(id, settlementData) {
|
||||
return request.post(`/insurance/claims/${id}/settle`, settlementData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取保费计算
|
||||
* @param {Object} calculationData 计算数据
|
||||
*/
|
||||
calculatePremium(calculationData) {
|
||||
return request.post('/insurance/premium-calculation', calculationData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取风险评估
|
||||
* @param {Object} assessmentData 评估数据
|
||||
*/
|
||||
getRiskAssessment(assessmentData) {
|
||||
return request.post('/insurance/risk-assessment', assessmentData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取保险统计数据
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
getInsuranceStats(params) {
|
||||
return request.get('/insurance/statistics', { params })
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取监管报告
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
getSupervisionReports(params) {
|
||||
return request.get('/insurance/supervision-reports', { params })
|
||||
},
|
||||
|
||||
/**
|
||||
* 生成监管报告
|
||||
* @param {Object} reportData 报告数据
|
||||
*/
|
||||
generateSupervisionReport(reportData) {
|
||||
return request.post('/insurance/supervision-reports/generate', reportData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取合规检查记录
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
getComplianceChecks(params) {
|
||||
return request.get('/insurance/compliance-checks', { params })
|
||||
},
|
||||
|
||||
/**
|
||||
* 执行合规检查
|
||||
* @param {Object} checkData 检查数据
|
||||
*/
|
||||
performComplianceCheck(checkData) {
|
||||
return request.post('/insurance/compliance-checks', checkData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取保险代理人列表
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
getInsuranceAgents(params) {
|
||||
return request.get('/insurance/agents', { params })
|
||||
},
|
||||
|
||||
/**
|
||||
* 注册保险代理人
|
||||
* @param {Object} agentData 代理人数据
|
||||
*/
|
||||
registerInsuranceAgent(agentData) {
|
||||
return request.post('/insurance/agents/register', agentData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 认证保险代理人
|
||||
* @param {number} id 代理人ID
|
||||
* @param {Object} certificationData 认证数据
|
||||
*/
|
||||
certifyInsuranceAgent(id, certificationData) {
|
||||
return request.post(`/insurance/agents/${id}/certify`, certificationData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取保险公司列表
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
getInsuranceCompanies(params) {
|
||||
return request.get('/insurance/companies', { params })
|
||||
},
|
||||
|
||||
/**
|
||||
* 注册保险公司
|
||||
* @param {Object} companyData 公司数据
|
||||
*/
|
||||
registerInsuranceCompany(companyData) {
|
||||
return request.post('/insurance/companies/register', companyData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取保险条款
|
||||
* @param {number} productId 产品ID
|
||||
*/
|
||||
getInsuranceTerms(productId) {
|
||||
return request.get(`/insurance/products/${productId}/terms`)
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新保险条款
|
||||
* @param {number} productId 产品ID
|
||||
* @param {Object} termsData 条款数据
|
||||
*/
|
||||
updateInsuranceTerms(productId, termsData) {
|
||||
return request.put(`/insurance/products/${productId}/terms`, termsData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取保险费率
|
||||
* @param {number} productId 产品ID
|
||||
*/
|
||||
getInsuranceRates(productId) {
|
||||
return request.get(`/insurance/products/${productId}/rates`)
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新保险费率
|
||||
* @param {number} productId 产品ID
|
||||
* @param {Object} ratesData 费率数据
|
||||
*/
|
||||
updateInsuranceRates(productId, ratesData) {
|
||||
return request.put(`/insurance/products/${productId}/rates`, ratesData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取再保险信息
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
getReinsuranceInfo(params) {
|
||||
return request.get('/insurance/reinsurance', { params })
|
||||
},
|
||||
|
||||
/**
|
||||
* 申请再保险
|
||||
* @param {Object} reinsuranceData 再保险数据
|
||||
*/
|
||||
applyReinsurance(reinsuranceData) {
|
||||
return request.post('/insurance/reinsurance/apply', reinsuranceData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取保险欺诈检测记录
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
getFraudDetectionRecords(params) {
|
||||
return request.get('/insurance/fraud-detection', { params })
|
||||
},
|
||||
|
||||
/**
|
||||
* 执行欺诈检测
|
||||
* @param {Object} detectionData 检测数据
|
||||
*/
|
||||
performFraudDetection(detectionData) {
|
||||
return request.post('/insurance/fraud-detection', detectionData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取保险投诉记录
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
getInsuranceComplaints(params) {
|
||||
return request.get('/insurance/complaints', { params })
|
||||
},
|
||||
|
||||
/**
|
||||
* 提交保险投诉
|
||||
* @param {Object} complaintData 投诉数据
|
||||
*/
|
||||
submitInsuranceComplaint(complaintData) {
|
||||
return request.post('/insurance/complaints', complaintData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 处理保险投诉
|
||||
* @param {number} id 投诉ID
|
||||
* @param {Object} handleData 处理数据
|
||||
*/
|
||||
handleInsuranceComplaint(id, handleData) {
|
||||
return request.post(`/insurance/complaints/${id}/handle`, handleData)
|
||||
}
|
||||
}
|
||||
372
mini_program/common/api/mall.js
Normal file
372
mini_program/common/api/mall.js
Normal file
@@ -0,0 +1,372 @@
|
||||
/**
|
||||
* 牛肉商城相关 API 接口
|
||||
*/
|
||||
|
||||
import request from '@/common/utils/request'
|
||||
|
||||
export const mallApi = {
|
||||
/**
|
||||
* 获取商品列表
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
getProductList(params) {
|
||||
return request.get('/mall/products', { params })
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取商品详情
|
||||
* @param {number} id 商品ID
|
||||
*/
|
||||
getProductDetail(id) {
|
||||
return request.get(`/mall/products/${id}`)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取商品分类
|
||||
*/
|
||||
getCategories() {
|
||||
return request.get('/mall/categories')
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取推荐商品
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
getRecommendProducts(params) {
|
||||
return request.get('/mall/products/recommend', { params })
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取热销商品
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
getHotProducts(params) {
|
||||
return request.get('/mall/products/hot', { params })
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取新品推荐
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
getNewProducts(params) {
|
||||
return request.get('/mall/products/new', { params })
|
||||
},
|
||||
|
||||
/**
|
||||
* 搜索商品
|
||||
* @param {Object} params 搜索参数
|
||||
*/
|
||||
searchProducts(params) {
|
||||
return request.get('/mall/products/search', { params })
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取购物车商品
|
||||
*/
|
||||
getCartItems() {
|
||||
return request.get('/mall/cart')
|
||||
},
|
||||
|
||||
/**
|
||||
* 添加商品到购物车
|
||||
* @param {Object} productData 商品数据
|
||||
*/
|
||||
addToCart(productData) {
|
||||
return request.post('/mall/cart', productData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新购物车商品
|
||||
* @param {number} cartItemId 购物车商品ID
|
||||
* @param {Object} updateData 更新数据
|
||||
*/
|
||||
updateCartItem(cartItemId, updateData) {
|
||||
return request.put(`/mall/cart/${cartItemId}`, updateData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 删除购物车商品
|
||||
* @param {number} cartItemId 购物车商品ID
|
||||
*/
|
||||
removeCartItem(cartItemId) {
|
||||
return request.delete(`/mall/cart/${cartItemId}`)
|
||||
},
|
||||
|
||||
/**
|
||||
* 清空购物车
|
||||
*/
|
||||
clearCart() {
|
||||
return request.delete('/mall/cart/clear')
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取订单列表
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
getOrderList(params) {
|
||||
return request.get('/mall/orders', { params })
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取订单详情
|
||||
* @param {number} id 订单ID
|
||||
*/
|
||||
getOrderDetail(id) {
|
||||
return request.get(`/mall/orders/${id}`)
|
||||
},
|
||||
|
||||
/**
|
||||
* 创建订单
|
||||
* @param {Object} orderData 订单数据
|
||||
*/
|
||||
createOrder(orderData) {
|
||||
return request.post('/mall/orders', orderData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 支付订单
|
||||
* @param {number} orderId 订单ID
|
||||
* @param {Object} paymentData 支付数据
|
||||
*/
|
||||
payOrder(orderId, paymentData) {
|
||||
return request.post(`/mall/orders/${orderId}/pay`, paymentData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 取消订单
|
||||
* @param {number} orderId 订单ID
|
||||
* @param {Object} cancelData 取消数据
|
||||
*/
|
||||
cancelOrder(orderId, cancelData) {
|
||||
return request.post(`/mall/orders/${orderId}/cancel`, cancelData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 确认收货
|
||||
* @param {number} orderId 订单ID
|
||||
*/
|
||||
confirmReceive(orderId) {
|
||||
return request.post(`/mall/orders/${orderId}/confirm-receive`)
|
||||
},
|
||||
|
||||
/**
|
||||
* 申请退款
|
||||
* @param {number} orderId 订单ID
|
||||
* @param {Object} refundData 退款数据
|
||||
*/
|
||||
applyRefund(orderId, refundData) {
|
||||
return request.post(`/mall/orders/${orderId}/refund`, refundData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取收货地址列表
|
||||
*/
|
||||
getAddressList() {
|
||||
return request.get('/mall/addresses')
|
||||
},
|
||||
|
||||
/**
|
||||
* 添加收货地址
|
||||
* @param {Object} addressData 地址数据
|
||||
*/
|
||||
addAddress(addressData) {
|
||||
return request.post('/mall/addresses', addressData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新收货地址
|
||||
* @param {number} id 地址ID
|
||||
* @param {Object} addressData 地址数据
|
||||
*/
|
||||
updateAddress(id, addressData) {
|
||||
return request.put(`/mall/addresses/${id}`, addressData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 删除收货地址
|
||||
* @param {number} id 地址ID
|
||||
*/
|
||||
deleteAddress(id) {
|
||||
return request.delete(`/mall/addresses/${id}`)
|
||||
},
|
||||
|
||||
/**
|
||||
* 设置默认地址
|
||||
* @param {number} id 地址ID
|
||||
*/
|
||||
setDefaultAddress(id) {
|
||||
return request.put(`/mall/addresses/${id}/default`)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取优惠券列表
|
||||
*/
|
||||
getCouponList() {
|
||||
return request.get('/mall/coupons')
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取可用优惠券
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
getAvailableCoupons(params) {
|
||||
return request.get('/mall/coupons/available', { params })
|
||||
},
|
||||
|
||||
/**
|
||||
* 领取优惠券
|
||||
* @param {number} couponId 优惠券ID
|
||||
*/
|
||||
receiveCoupon(couponId) {
|
||||
return request.post(`/mall/coupons/${couponId}/receive`)
|
||||
},
|
||||
|
||||
/**
|
||||
* 使用优惠券
|
||||
* @param {number} couponId 优惠券ID
|
||||
* @param {number} orderId 订单ID
|
||||
*/
|
||||
useCoupon(couponId, orderId) {
|
||||
return request.post(`/mall/coupons/${couponId}/use`, { orderId })
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取商品评价
|
||||
* @param {number} productId 商品ID
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
getProductReviews(productId, params) {
|
||||
return request.get(`/mall/products/${productId}/reviews`, { params })
|
||||
},
|
||||
|
||||
/**
|
||||
* 添加商品评价
|
||||
* @param {Object} reviewData 评价数据
|
||||
*/
|
||||
addProductReview(reviewData) {
|
||||
return request.post('/mall/reviews', reviewData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取物流信息
|
||||
* @param {number} orderId 订单ID
|
||||
*/
|
||||
getLogistics(orderId) {
|
||||
return request.get(`/mall/orders/${orderId}/logistics`)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取商城统计数据
|
||||
*/
|
||||
getMallStats() {
|
||||
return request.get('/mall/stats')
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取品牌列表
|
||||
*/
|
||||
getBrands() {
|
||||
return request.get('/mall/brands')
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取产地列表
|
||||
*/
|
||||
getOrigins() {
|
||||
return request.get('/mall/origins')
|
||||
},
|
||||
|
||||
/**
|
||||
* 收藏商品
|
||||
* @param {number} productId 商品ID
|
||||
*/
|
||||
favoriteProduct(productId) {
|
||||
return request.post('/mall/favorite', { productId })
|
||||
},
|
||||
|
||||
/**
|
||||
* 取消收藏商品
|
||||
* @param {number} productId 商品ID
|
||||
*/
|
||||
unfavoriteProduct(productId) {
|
||||
return request.delete(`/mall/favorite/${productId}`)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取收藏的商品列表
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
getFavoriteProductList(params) {
|
||||
return request.get('/mall/favorites', { params })
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取浏览历史
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
getBrowseHistory(params) {
|
||||
return request.get('/mall/browse-history', { params })
|
||||
},
|
||||
|
||||
/**
|
||||
* 添加浏览历史
|
||||
* @param {number} productId 商品ID
|
||||
*/
|
||||
addBrowseHistory(productId) {
|
||||
return request.post('/mall/browse-history', { productId })
|
||||
},
|
||||
|
||||
/**
|
||||
* 清除浏览历史
|
||||
*/
|
||||
clearBrowseHistory() {
|
||||
return request.delete('/mall/browse-history/clear')
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取热门搜索关键词
|
||||
*/
|
||||
getHotKeywords() {
|
||||
return request.get('/mall/hot-keywords')
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取搜索建议
|
||||
* @param {string} keyword 关键词
|
||||
*/
|
||||
getSearchSuggestions(keyword) {
|
||||
return request.get('/mall/search-suggestions', { params: { keyword } })
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取秒杀活动
|
||||
*/
|
||||
getSeckillActivities() {
|
||||
return request.get('/mall/seckill')
|
||||
},
|
||||
|
||||
/**
|
||||
* 参与秒杀
|
||||
* @param {number} activityId 活动ID
|
||||
* @param {Object} seckillData 秒杀数据
|
||||
*/
|
||||
joinSeckill(activityId, seckillData) {
|
||||
return request.post(`/mall/seckill/${activityId}/join`, seckillData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取团购活动
|
||||
*/
|
||||
getGroupBuyActivities() {
|
||||
return request.get('/mall/group-buy')
|
||||
},
|
||||
|
||||
/**
|
||||
* 参与团购
|
||||
* @param {number} activityId 活动ID
|
||||
* @param {Object} groupBuyData 团购数据
|
||||
*/
|
||||
joinGroupBuy(activityId, groupBuyData) {
|
||||
return request.post(`/mall/group-buy/${activityId}/join`, groupBuyData)
|
||||
}
|
||||
}
|
||||
257
mini_program/common/api/trading.js
Normal file
257
mini_program/common/api/trading.js
Normal file
@@ -0,0 +1,257 @@
|
||||
/**
|
||||
* 牛只交易相关 API 接口
|
||||
*/
|
||||
|
||||
import request from '@/common/utils/request'
|
||||
|
||||
export const tradingApi = {
|
||||
/**
|
||||
* 获取交易列表
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
getTradeList(params) {
|
||||
return request.get('/trading/trades', { params })
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取交易详情
|
||||
* @param {number} id 交易ID
|
||||
*/
|
||||
getTradeDetail(id) {
|
||||
return request.get(`/trading/trades/${id}`)
|
||||
},
|
||||
|
||||
/**
|
||||
* 发布交易信息
|
||||
* @param {Object} tradeData 交易数据
|
||||
*/
|
||||
publishTrade(tradeData) {
|
||||
return request.post('/trading/trades', tradeData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新交易信息
|
||||
* @param {number} id 交易ID
|
||||
* @param {Object} tradeData 交易数据
|
||||
*/
|
||||
updateTrade(id, tradeData) {
|
||||
return request.put(`/trading/trades/${id}`, tradeData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 删除交易信息
|
||||
* @param {number} id 交易ID
|
||||
*/
|
||||
deleteTrade(id) {
|
||||
return request.delete(`/trading/trades/${id}`)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取我的发布列表
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
getMyPublishList(params) {
|
||||
return request.get('/trading/my-publish', { params })
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取我的购买列表
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
getMyPurchaseList(params) {
|
||||
return request.get('/trading/my-purchase', { params })
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取订单列表
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
getOrderList(params) {
|
||||
return request.get('/trading/orders', { params })
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取订单详情
|
||||
* @param {number} id 订单ID
|
||||
*/
|
||||
getOrderDetail(id) {
|
||||
return request.get(`/trading/orders/${id}`)
|
||||
},
|
||||
|
||||
/**
|
||||
* 创建订单
|
||||
* @param {Object} orderData 订单数据
|
||||
*/
|
||||
createOrder(orderData) {
|
||||
return request.post('/trading/orders', orderData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 支付订单
|
||||
* @param {number} orderId 订单ID
|
||||
* @param {Object} paymentData 支付数据
|
||||
*/
|
||||
payOrder(orderId, paymentData) {
|
||||
return request.post(`/trading/orders/${orderId}/pay`, paymentData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 取消订单
|
||||
* @param {number} orderId 订单ID
|
||||
* @param {Object} cancelData 取消数据
|
||||
*/
|
||||
cancelOrder(orderId, cancelData) {
|
||||
return request.post(`/trading/orders/${orderId}/cancel`, cancelData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 确认收货
|
||||
* @param {number} orderId 订单ID
|
||||
*/
|
||||
confirmReceive(orderId) {
|
||||
return request.post(`/trading/orders/${orderId}/confirm-receive`)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取价格行情数据
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
getPriceData(params) {
|
||||
return request.get('/trading/price-data', { params })
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取市场统计数据
|
||||
*/
|
||||
getMarketStats() {
|
||||
return request.get('/trading/market-stats')
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取交易分类
|
||||
*/
|
||||
getCategories() {
|
||||
return request.get('/trading/categories')
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取地区列表
|
||||
*/
|
||||
getRegions() {
|
||||
return request.get('/trading/regions')
|
||||
},
|
||||
|
||||
/**
|
||||
* 收藏交易
|
||||
* @param {number} tradeId 交易ID
|
||||
*/
|
||||
favoriteTrade(tradeId) {
|
||||
return request.post('/trading/favorite', { tradeId })
|
||||
},
|
||||
|
||||
/**
|
||||
* 取消收藏交易
|
||||
* @param {number} tradeId 交易ID
|
||||
*/
|
||||
unfavoriteTrade(tradeId) {
|
||||
return request.delete(`/trading/favorite/${tradeId}`)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取收藏的交易列表
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
getFavoriteTradeList(params) {
|
||||
return request.get('/trading/favorites', { params })
|
||||
},
|
||||
|
||||
/**
|
||||
* 举报交易
|
||||
* @param {Object} reportData 举报数据
|
||||
*/
|
||||
reportTrade(reportData) {
|
||||
return request.post('/trading/report', reportData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取交易评价
|
||||
* @param {number} tradeId 交易ID
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
getTradeReviews(tradeId, params) {
|
||||
return request.get(`/trading/trades/${tradeId}/reviews`, { params })
|
||||
},
|
||||
|
||||
/**
|
||||
* 添加交易评价
|
||||
* @param {Object} reviewData 评价数据
|
||||
*/
|
||||
addTradeReview(reviewData) {
|
||||
return request.post('/trading/reviews', reviewData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取用户信誉信息
|
||||
* @param {number} userId 用户ID
|
||||
*/
|
||||
getUserCredit(userId) {
|
||||
return request.get(`/trading/users/${userId}/credit`)
|
||||
},
|
||||
|
||||
/**
|
||||
* 申请退款
|
||||
* @param {number} orderId 订单ID
|
||||
* @param {Object} refundData 退款数据
|
||||
*/
|
||||
applyRefund(orderId, refundData) {
|
||||
return request.post(`/trading/orders/${orderId}/refund`, refundData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取物流信息
|
||||
* @param {number} orderId 订单ID
|
||||
*/
|
||||
getLogistics(orderId) {
|
||||
return request.get(`/trading/orders/${orderId}/logistics`)
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新物流信息
|
||||
* @param {number} orderId 订单ID
|
||||
* @param {Object} logisticsData 物流数据
|
||||
*/
|
||||
updateLogistics(orderId, logisticsData) {
|
||||
return request.put(`/trading/orders/${orderId}/logistics`, logisticsData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取推荐交易
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
getRecommendTrades(params) {
|
||||
return request.get('/trading/recommend', { params })
|
||||
},
|
||||
|
||||
/**
|
||||
* 搜索交易
|
||||
* @param {Object} params 搜索参数
|
||||
*/
|
||||
searchTrades(params) {
|
||||
return request.get('/trading/search', { params })
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取热门搜索关键词
|
||||
*/
|
||||
getHotKeywords() {
|
||||
return request.get('/trading/hot-keywords')
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取交易历史记录
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
getTradeHistory(params) {
|
||||
return request.get('/trading/history', { params })
|
||||
}
|
||||
}
|
||||
306
mini_program/common/api/user.js
Normal file
306
mini_program/common/api/user.js
Normal file
@@ -0,0 +1,306 @@
|
||||
/**
|
||||
* 用户相关 API 接口
|
||||
*/
|
||||
|
||||
import request from '@/common/utils/request'
|
||||
import { API_CONFIG } from '@/common/config'
|
||||
|
||||
export const userApi = {
|
||||
/**
|
||||
* 用户登录
|
||||
* @param {Object} credentials 登录凭证
|
||||
* @param {string} credentials.username 用户名
|
||||
* @param {string} credentials.password 密码
|
||||
* @param {string} credentials.code 验证码(可选)
|
||||
*/
|
||||
login(credentials) {
|
||||
return request.post(API_CONFIG.user.login, credentials)
|
||||
},
|
||||
|
||||
/**
|
||||
* 微信登录
|
||||
* @param {Object} wxData 微信登录数据
|
||||
* @param {string} wxData.code 微信授权码
|
||||
* @param {string} wxData.encryptedData 加密数据(可选)
|
||||
* @param {string} wxData.iv 初始向量(可选)
|
||||
*/
|
||||
wxLogin(wxData) {
|
||||
return request.post('/auth/wx-login', wxData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 用户注册
|
||||
* @param {Object} userData 注册数据
|
||||
* @param {string} userData.username 用户名
|
||||
* @param {string} userData.password 密码
|
||||
* @param {string} userData.phone 手机号
|
||||
* @param {string} userData.code 验证码
|
||||
*/
|
||||
register(userData) {
|
||||
return request.post(API_CONFIG.user.register, userData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 用户登出
|
||||
*/
|
||||
logout() {
|
||||
return request.post(API_CONFIG.user.logout)
|
||||
},
|
||||
|
||||
/**
|
||||
* 刷新访问令牌
|
||||
* @param {string} refreshToken 刷新令牌
|
||||
*/
|
||||
refreshToken(refreshToken) {
|
||||
return request.post('/auth/refresh-token', { refreshToken })
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取用户信息
|
||||
*/
|
||||
getUserInfo() {
|
||||
return request.get(API_CONFIG.user.profile)
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新用户信息
|
||||
* @param {Object} userData 用户数据
|
||||
*/
|
||||
updateUserInfo(userData) {
|
||||
return request.put(API_CONFIG.user.updateProfile, userData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 修改密码
|
||||
* @param {Object} passwordData 密码数据
|
||||
* @param {string} passwordData.oldPassword 旧密码
|
||||
* @param {string} passwordData.newPassword 新密码
|
||||
*/
|
||||
changePassword(passwordData) {
|
||||
return request.put('/user/change-password', passwordData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 绑定手机号
|
||||
* @param {Object} phoneData 手机号数据
|
||||
* @param {string} phoneData.phone 手机号
|
||||
* @param {string} phoneData.code 验证码
|
||||
*/
|
||||
bindPhone(phoneData) {
|
||||
return request.post('/user/bind-phone', phoneData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 解绑手机号
|
||||
* @param {Object} data 解绑数据
|
||||
* @param {string} data.code 验证码
|
||||
*/
|
||||
unbindPhone(data) {
|
||||
return request.post('/user/unbind-phone', data)
|
||||
},
|
||||
|
||||
/**
|
||||
* 绑定邮箱
|
||||
* @param {Object} emailData 邮箱数据
|
||||
* @param {string} emailData.email 邮箱
|
||||
* @param {string} emailData.code 验证码
|
||||
*/
|
||||
bindEmail(emailData) {
|
||||
return request.post('/user/bind-email', emailData)
|
||||
},
|
||||
|
||||
/**
|
||||
* 上传头像
|
||||
* @param {File} file 头像文件
|
||||
*/
|
||||
uploadAvatar(file) {
|
||||
const formData = new FormData()
|
||||
formData.append('avatar', file)
|
||||
|
||||
return request.post('/user/upload-avatar', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取用户统计信息
|
||||
*/
|
||||
getUserStats() {
|
||||
return request.get('/user/stats')
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取用户操作日志
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
getUserLogs(params) {
|
||||
return request.get('/user/logs', { params })
|
||||
},
|
||||
|
||||
/**
|
||||
* 发送短信验证码
|
||||
* @param {Object} data 发送数据
|
||||
* @param {string} data.phone 手机号
|
||||
* @param {string} data.type 验证码类型
|
||||
*/
|
||||
sendSmsCode(data) {
|
||||
return request.post('/auth/send-sms-code', data)
|
||||
},
|
||||
|
||||
/**
|
||||
* 发送邮箱验证码
|
||||
* @param {Object} data 发送数据
|
||||
* @param {string} data.email 邮箱
|
||||
* @param {string} data.type 验证码类型
|
||||
*/
|
||||
sendEmailCode(data) {
|
||||
return request.post('/auth/send-email-code', data)
|
||||
},
|
||||
|
||||
/**
|
||||
* 验证短信验证码
|
||||
* @param {Object} data 验证数据
|
||||
* @param {string} data.phone 手机号
|
||||
* @param {string} data.code 验证码
|
||||
* @param {string} data.type 验证码类型
|
||||
*/
|
||||
verifySmsCode(data) {
|
||||
return request.post('/auth/verify-sms-code', data)
|
||||
},
|
||||
|
||||
/**
|
||||
* 验证邮箱验证码
|
||||
* @param {Object} data 验证数据
|
||||
* @param {string} data.email 邮箱
|
||||
* @param {string} data.code 验证码
|
||||
* @param {string} data.type 验证码类型
|
||||
*/
|
||||
verifyEmailCode(data) {
|
||||
return request.post('/auth/verify-email-code', data)
|
||||
},
|
||||
|
||||
/**
|
||||
* 忘记密码
|
||||
* @param {Object} data 重置数据
|
||||
* @param {string} data.phone 手机号
|
||||
* @param {string} data.code 验证码
|
||||
* @param {string} data.newPassword 新密码
|
||||
*/
|
||||
forgotPassword(data) {
|
||||
return request.post('/auth/forgot-password', data)
|
||||
},
|
||||
|
||||
/**
|
||||
* 重置密码
|
||||
* @param {Object} data 重置数据
|
||||
* @param {string} data.token 重置令牌
|
||||
* @param {string} data.newPassword 新密码
|
||||
*/
|
||||
resetPassword(data) {
|
||||
return request.post('/auth/reset-password', data)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取用户收藏列表
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
getFavorites(params) {
|
||||
return request.get('/user/favorites', { params })
|
||||
},
|
||||
|
||||
/**
|
||||
* 添加收藏
|
||||
* @param {Object} data 收藏数据
|
||||
* @param {string} data.type 收藏类型
|
||||
* @param {number} data.targetId 目标ID
|
||||
*/
|
||||
addFavorite(data) {
|
||||
return request.post('/user/favorites', data)
|
||||
},
|
||||
|
||||
/**
|
||||
* 取消收藏
|
||||
* @param {number} id 收藏ID
|
||||
*/
|
||||
removeFavorite(id) {
|
||||
return request.delete(`/user/favorites/${id}`)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取用户关注列表
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
getFollows(params) {
|
||||
return request.get('/user/follows', { params })
|
||||
},
|
||||
|
||||
/**
|
||||
* 关注用户
|
||||
* @param {number} userId 用户ID
|
||||
*/
|
||||
followUser(userId) {
|
||||
return request.post('/user/follow', { userId })
|
||||
},
|
||||
|
||||
/**
|
||||
* 取消关注
|
||||
* @param {number} userId 用户ID
|
||||
*/
|
||||
unfollowUser(userId) {
|
||||
return request.delete(`/user/follow/${userId}`)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取用户粉丝列表
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
getFollowers(params) {
|
||||
return request.get('/user/followers', { params })
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取用户设置
|
||||
*/
|
||||
getUserSettings() {
|
||||
return request.get('/user/settings')
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新用户设置
|
||||
* @param {Object} settings 设置数据
|
||||
*/
|
||||
updateUserSettings(settings) {
|
||||
return request.put('/user/settings', settings)
|
||||
},
|
||||
|
||||
/**
|
||||
* 注销账户
|
||||
* @param {Object} data 注销数据
|
||||
* @param {string} data.password 密码
|
||||
* @param {string} data.reason 注销原因
|
||||
*/
|
||||
deleteAccount(data) {
|
||||
return request.post('/user/delete-account', data)
|
||||
},
|
||||
|
||||
/**
|
||||
* 实名认证
|
||||
* @param {Object} data 认证数据
|
||||
* @param {string} data.realName 真实姓名
|
||||
* @param {string} data.idCard 身份证号
|
||||
* @param {string} data.idCardFront 身份证正面照
|
||||
* @param {string} data.idCardBack 身份证背面照
|
||||
*/
|
||||
realNameAuth(data) {
|
||||
return request.post('/user/real-name-auth', data)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取实名认证状态
|
||||
*/
|
||||
getRealNameAuthStatus() {
|
||||
return request.get('/user/real-name-auth/status')
|
||||
}
|
||||
}
|
||||
1006
mini_program/common/components/calendar/calendar.vue
Normal file
1006
mini_program/common/components/calendar/calendar.vue
Normal file
File diff suppressed because it is too large
Load Diff
405
mini_program/common/components/card/card.vue
Normal file
405
mini_program/common/components/card/card.vue
Normal file
@@ -0,0 +1,405 @@
|
||||
<template>
|
||||
<view class="card" :class="cardClass" @click="handleClick">
|
||||
<!-- 卡片头部 -->
|
||||
<view class="card-header" v-if="showHeader">
|
||||
<slot name="header">
|
||||
<view class="header-content">
|
||||
<!-- 标题区域 -->
|
||||
<view class="header-title" v-if="title">
|
||||
<text class="title-text">{{ title }}</text>
|
||||
<text class="subtitle-text" v-if="subtitle">{{ subtitle }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 额外内容 -->
|
||||
<view class="header-extra" v-if="extra || $slots.extra">
|
||||
<slot name="extra">
|
||||
<text class="extra-text">{{ extra }}</text>
|
||||
</slot>
|
||||
</view>
|
||||
</view>
|
||||
</slot>
|
||||
</view>
|
||||
|
||||
<!-- 卡片主体 -->
|
||||
<view class="card-body" :class="bodyClass">
|
||||
<slot>
|
||||
<view class="default-content" v-if="content">
|
||||
{{ content }}
|
||||
</view>
|
||||
</slot>
|
||||
</view>
|
||||
|
||||
<!-- 卡片底部 -->
|
||||
<view class="card-footer" v-if="showFooter">
|
||||
<slot name="footer">
|
||||
<view class="footer-content">
|
||||
{{ footer }}
|
||||
</view>
|
||||
</slot>
|
||||
</view>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<view class="card-loading" v-if="loading">
|
||||
<view class="loading-spinner"></view>
|
||||
<text class="loading-text">{{ loadingText }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Card',
|
||||
props: {
|
||||
// 标题
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 副标题
|
||||
subtitle: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 额外内容
|
||||
extra: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 主体内容
|
||||
content: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 底部内容
|
||||
footer: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 卡片类型
|
||||
type: {
|
||||
type: String,
|
||||
default: 'default', // default, primary, success, warning, danger
|
||||
validator: (value) => ['default', 'primary', 'success', 'warning', 'danger'].includes(value)
|
||||
},
|
||||
// 卡片大小
|
||||
size: {
|
||||
type: String,
|
||||
default: 'normal', // large, normal, small
|
||||
validator: (value) => ['large', 'normal', 'small'].includes(value)
|
||||
},
|
||||
// 是否有阴影
|
||||
shadow: {
|
||||
type: [Boolean, String],
|
||||
default: true // true, false, 'always', 'hover', 'never'
|
||||
},
|
||||
// 是否有边框
|
||||
bordered: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 是否可点击
|
||||
clickable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 是否禁用
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 是否加载中
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 加载文本
|
||||
loadingText: {
|
||||
type: String,
|
||||
default: '加载中...'
|
||||
},
|
||||
// 圆角大小
|
||||
borderRadius: {
|
||||
type: String,
|
||||
default: '12rpx'
|
||||
},
|
||||
// 内边距
|
||||
padding: {
|
||||
type: String,
|
||||
default: '32rpx'
|
||||
},
|
||||
// 外边距
|
||||
margin: {
|
||||
type: String,
|
||||
default: '0'
|
||||
},
|
||||
// 背景色
|
||||
backgroundColor: {
|
||||
type: String,
|
||||
default: '#fff'
|
||||
},
|
||||
// 自定义样式类
|
||||
customClass: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
cardClass() {
|
||||
return [
|
||||
this.customClass,
|
||||
`card-${this.type}`,
|
||||
`card-${this.size}`,
|
||||
{
|
||||
'card-shadow': this.shadow === true || this.shadow === 'always',
|
||||
'card-shadow-hover': this.shadow === 'hover',
|
||||
'card-bordered': this.bordered,
|
||||
'card-clickable': this.clickable,
|
||||
'card-disabled': this.disabled,
|
||||
'card-loading': this.loading
|
||||
}
|
||||
]
|
||||
},
|
||||
bodyClass() {
|
||||
return {
|
||||
'body-no-header': !this.showHeader,
|
||||
'body-no-footer': !this.showFooter
|
||||
}
|
||||
},
|
||||
showHeader() {
|
||||
return this.title || this.subtitle || this.extra || this.$slots.header || this.$slots.extra
|
||||
},
|
||||
showFooter() {
|
||||
return this.footer || this.$slots.footer
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleClick(e) {
|
||||
if (this.disabled || this.loading) return
|
||||
this.$emit('click', e)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.card {
|
||||
position: relative;
|
||||
background-color: v-bind(backgroundColor);
|
||||
border-radius: v-bind(borderRadius);
|
||||
margin: v-bind(margin);
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
// 类型样式
|
||||
&.card-primary {
|
||||
border-left: 8rpx solid #2e8b57;
|
||||
}
|
||||
|
||||
&.card-success {
|
||||
border-left: 8rpx solid #52c41a;
|
||||
}
|
||||
|
||||
&.card-warning {
|
||||
border-left: 8rpx solid #faad14;
|
||||
}
|
||||
|
||||
&.card-danger {
|
||||
border-left: 8rpx solid #ff4d4f;
|
||||
}
|
||||
|
||||
// 大小样式
|
||||
&.card-large {
|
||||
.card-header {
|
||||
padding: 40rpx 40rpx 0;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 32rpx 40rpx;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
padding: 0 40rpx 40rpx;
|
||||
}
|
||||
|
||||
.title-text {
|
||||
font-size: 36rpx;
|
||||
}
|
||||
}
|
||||
|
||||
&.card-normal {
|
||||
.card-header {
|
||||
padding: 32rpx 32rpx 0;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: v-bind(padding);
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
padding: 0 32rpx 32rpx;
|
||||
}
|
||||
|
||||
.title-text {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
}
|
||||
|
||||
&.card-small {
|
||||
.card-header {
|
||||
padding: 24rpx 24rpx 0;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 20rpx 24rpx;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
padding: 0 24rpx 24rpx;
|
||||
}
|
||||
|
||||
.title-text {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
}
|
||||
|
||||
// 阴影样式
|
||||
&.card-shadow {
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
&.card-shadow-hover:hover {
|
||||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
// 边框样式
|
||||
&.card-bordered {
|
||||
border: 2rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
// 可点击样式
|
||||
&.card-clickable {
|
||||
cursor: pointer;
|
||||
|
||||
&:active {
|
||||
transform: translateY(2rpx);
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
// 禁用样式
|
||||
&.card-disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
|
||||
&:active {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
// 加载样式
|
||||
&.card-loading {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.card-header {
|
||||
border-bottom: 2rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.title-text {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.subtitle-text {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
margin-top: 8rpx;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.header-extra {
|
||||
margin-left: 24rpx;
|
||||
}
|
||||
|
||||
.extra-text {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
position: relative;
|
||||
|
||||
&.body-no-header {
|
||||
padding-top: v-bind(padding);
|
||||
}
|
||||
|
||||
&.body-no-footer {
|
||||
padding-bottom: v-bind(padding);
|
||||
}
|
||||
}
|
||||
|
||||
.default-content {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
border-top: 2rpx solid #f0f0f0;
|
||||
padding-top: 24rpx;
|
||||
}
|
||||
|
||||
.footer-content {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.card-loading {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
border: 4rpx solid #f3f3f3;
|
||||
border-top: 4rpx solid #2e8b57;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
745
mini_program/common/components/chart/chart.vue
Normal file
745
mini_program/common/components/chart/chart.vue
Normal file
@@ -0,0 +1,745 @@
|
||||
<template>
|
||||
<view class="chart-container">
|
||||
<canvas
|
||||
:canvas-id="canvasId"
|
||||
:id="canvasId"
|
||||
class="chart-canvas"
|
||||
:style="{ width: width + 'rpx', height: height + 'rpx' }"
|
||||
@touchstart="handleTouchStart"
|
||||
@touchmove="handleTouchMove"
|
||||
@touchend="handleTouchEnd"
|
||||
/>
|
||||
|
||||
<!-- 图例 -->
|
||||
<view class="chart-legend" v-if="showLegend && legendData.length">
|
||||
<view
|
||||
class="legend-item"
|
||||
v-for="(item, index) in legendData"
|
||||
:key="index"
|
||||
@click="handleLegendClick(index)"
|
||||
>
|
||||
<view
|
||||
class="legend-color"
|
||||
:style="{ backgroundColor: item.color }"
|
||||
/>
|
||||
<text class="legend-text">{{ item.name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 数据标签 -->
|
||||
<view class="chart-tooltip" v-if="showTooltip" :style="tooltipStyle">
|
||||
<view class="tooltip-content">
|
||||
<text class="tooltip-title">{{ tooltipData.title }}</text>
|
||||
<view
|
||||
class="tooltip-item"
|
||||
v-for="(item, index) in tooltipData.items"
|
||||
:key="index"
|
||||
>
|
||||
<view
|
||||
class="tooltip-color"
|
||||
:style="{ backgroundColor: item.color }"
|
||||
/>
|
||||
<text class="tooltip-label">{{ item.label }}:</text>
|
||||
<text class="tooltip-value">{{ item.value }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, reactive, computed, watch, onMounted, nextTick } from 'vue'
|
||||
|
||||
export default {
|
||||
name: 'Chart',
|
||||
props: {
|
||||
// 图表类型
|
||||
type: {
|
||||
type: String,
|
||||
default: 'line', // line, bar, pie, doughnut, area
|
||||
validator: (value) => ['line', 'bar', 'pie', 'doughnut', 'area'].includes(value)
|
||||
},
|
||||
// 图表数据
|
||||
data: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
// 图表配置
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
// 画布尺寸
|
||||
width: {
|
||||
type: Number,
|
||||
default: 600
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
default: 400
|
||||
},
|
||||
// 是否显示图例
|
||||
showLegend: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 是否显示工具提示
|
||||
enableTooltip: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 是否启用动画
|
||||
animation: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 动画持续时间
|
||||
animationDuration: {
|
||||
type: Number,
|
||||
default: 1000
|
||||
}
|
||||
},
|
||||
emits: ['click', 'legendClick'],
|
||||
setup(props, { emit }) {
|
||||
const canvasId = `chart-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
||||
const ctx = ref(null)
|
||||
const showTooltip = ref(false)
|
||||
const tooltipData = ref({ title: '', items: [] })
|
||||
const tooltipStyle = ref({})
|
||||
const legendData = ref([])
|
||||
|
||||
// 默认配置
|
||||
const defaultOptions = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
padding: { top: 20, right: 20, bottom: 20, left: 20 },
|
||||
grid: {
|
||||
show: true,
|
||||
color: '#e5e5e5',
|
||||
lineWidth: 1
|
||||
},
|
||||
axis: {
|
||||
x: {
|
||||
show: true,
|
||||
color: '#666',
|
||||
fontSize: 24,
|
||||
labelRotate: 0
|
||||
},
|
||||
y: {
|
||||
show: true,
|
||||
color: '#666',
|
||||
fontSize: 24,
|
||||
min: null,
|
||||
max: null
|
||||
}
|
||||
},
|
||||
colors: [
|
||||
'#2e8b57', '#ff4757', '#3742fa', '#ffa502',
|
||||
'#7bed9f', '#ff6b81', '#70a1ff', '#ffb8b8'
|
||||
]
|
||||
}
|
||||
|
||||
// 合并配置
|
||||
const chartOptions = computed(() => {
|
||||
return Object.assign({}, defaultOptions, props.options)
|
||||
})
|
||||
|
||||
// 初始化画布
|
||||
const initCanvas = () => {
|
||||
return new Promise((resolve) => {
|
||||
nextTick(() => {
|
||||
const query = uni.createSelectorQuery()
|
||||
query.select(`#${canvasId}`).fields({ node: true, size: true }).exec((res) => {
|
||||
if (res[0]) {
|
||||
const canvas = res[0].node
|
||||
const context = canvas.getContext('2d')
|
||||
|
||||
// 设置画布尺寸
|
||||
const dpr = uni.getSystemInfoSync().pixelRatio
|
||||
canvas.width = props.width * dpr
|
||||
canvas.height = props.height * dpr
|
||||
context.scale(dpr, dpr)
|
||||
|
||||
ctx.value = context
|
||||
resolve(context)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 绘制图表
|
||||
const drawChart = async () => {
|
||||
if (!ctx.value) {
|
||||
await initCanvas()
|
||||
}
|
||||
|
||||
if (!ctx.value || !props.data) return
|
||||
|
||||
// 清空画布
|
||||
ctx.value.clearRect(0, 0, props.width, props.height)
|
||||
|
||||
// 根据图表类型绘制
|
||||
switch (props.type) {
|
||||
case 'line':
|
||||
drawLineChart()
|
||||
break
|
||||
case 'bar':
|
||||
drawBarChart()
|
||||
break
|
||||
case 'pie':
|
||||
drawPieChart()
|
||||
break
|
||||
case 'doughnut':
|
||||
drawDoughnutChart()
|
||||
break
|
||||
case 'area':
|
||||
drawAreaChart()
|
||||
break
|
||||
}
|
||||
|
||||
// 更新图例
|
||||
updateLegend()
|
||||
}
|
||||
|
||||
// 绘制折线图
|
||||
const drawLineChart = () => {
|
||||
const { labels, datasets } = props.data
|
||||
const options = chartOptions.value
|
||||
const padding = options.padding
|
||||
|
||||
const chartWidth = props.width - padding.left - padding.right
|
||||
const chartHeight = props.height - padding.top - padding.bottom
|
||||
const startX = padding.left
|
||||
const startY = padding.top
|
||||
|
||||
// 计算数据范围
|
||||
const allValues = datasets.flatMap(dataset => dataset.data)
|
||||
const minValue = options.axis.y.min ?? Math.min(...allValues)
|
||||
const maxValue = options.axis.y.max ?? Math.max(...allValues)
|
||||
const valueRange = maxValue - minValue || 1
|
||||
|
||||
// 绘制网格
|
||||
if (options.grid.show) {
|
||||
drawGrid(startX, startY, chartWidth, chartHeight, labels.length, 5)
|
||||
}
|
||||
|
||||
// 绘制坐标轴
|
||||
drawAxis(startX, startY, chartWidth, chartHeight, labels, minValue, maxValue)
|
||||
|
||||
// 绘制数据线
|
||||
datasets.forEach((dataset, datasetIndex) => {
|
||||
const color = dataset.color || options.colors[datasetIndex % options.colors.length]
|
||||
|
||||
ctx.value.strokeStyle = color
|
||||
ctx.value.lineWidth = dataset.lineWidth || 2
|
||||
ctx.value.beginPath()
|
||||
|
||||
dataset.data.forEach((value, index) => {
|
||||
const x = startX + (index / (labels.length - 1)) * chartWidth
|
||||
const y = startY + chartHeight - ((value - minValue) / valueRange) * chartHeight
|
||||
|
||||
if (index === 0) {
|
||||
ctx.value.moveTo(x, y)
|
||||
} else {
|
||||
ctx.value.lineTo(x, y)
|
||||
}
|
||||
})
|
||||
|
||||
ctx.value.stroke()
|
||||
|
||||
// 绘制数据点
|
||||
if (dataset.showPoints !== false) {
|
||||
ctx.value.fillStyle = color
|
||||
dataset.data.forEach((value, index) => {
|
||||
const x = startX + (index / (labels.length - 1)) * chartWidth
|
||||
const y = startY + chartHeight - ((value - minValue) / valueRange) * chartHeight
|
||||
|
||||
ctx.value.beginPath()
|
||||
ctx.value.arc(x, y, dataset.pointRadius || 4, 0, 2 * Math.PI)
|
||||
ctx.value.fill()
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 绘制柱状图
|
||||
const drawBarChart = () => {
|
||||
const { labels, datasets } = props.data
|
||||
const options = chartOptions.value
|
||||
const padding = options.padding
|
||||
|
||||
const chartWidth = props.width - padding.left - padding.right
|
||||
const chartHeight = props.height - padding.top - padding.bottom
|
||||
const startX = padding.left
|
||||
const startY = padding.top
|
||||
|
||||
// 计算数据范围
|
||||
const allValues = datasets.flatMap(dataset => dataset.data)
|
||||
const minValue = Math.min(0, Math.min(...allValues))
|
||||
const maxValue = Math.max(...allValues)
|
||||
const valueRange = maxValue - minValue || 1
|
||||
|
||||
// 绘制网格
|
||||
if (options.grid.show) {
|
||||
drawGrid(startX, startY, chartWidth, chartHeight, labels.length, 5)
|
||||
}
|
||||
|
||||
// 绘制坐标轴
|
||||
drawAxis(startX, startY, chartWidth, chartHeight, labels, minValue, maxValue)
|
||||
|
||||
// 计算柱子宽度
|
||||
const barGroupWidth = chartWidth / labels.length
|
||||
const barWidth = barGroupWidth / datasets.length * 0.8
|
||||
const barSpacing = barGroupWidth * 0.1
|
||||
|
||||
// 绘制柱子
|
||||
datasets.forEach((dataset, datasetIndex) => {
|
||||
const color = dataset.color || options.colors[datasetIndex % options.colors.length]
|
||||
ctx.value.fillStyle = color
|
||||
|
||||
dataset.data.forEach((value, index) => {
|
||||
const x = startX + index * barGroupWidth + datasetIndex * barWidth + barSpacing
|
||||
const barHeight = Math.abs(value - minValue) / valueRange * chartHeight
|
||||
const y = startY + chartHeight - barHeight
|
||||
|
||||
ctx.value.fillRect(x, y, barWidth, barHeight)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 绘制饼图
|
||||
const drawPieChart = () => {
|
||||
const { labels, datasets } = props.data
|
||||
const dataset = datasets[0]
|
||||
const options = chartOptions.value
|
||||
|
||||
const centerX = props.width / 2
|
||||
const centerY = props.height / 2
|
||||
const radius = Math.min(props.width, props.height) / 2 - 40
|
||||
|
||||
const total = dataset.data.reduce((sum, value) => sum + value, 0)
|
||||
let currentAngle = -Math.PI / 2
|
||||
|
||||
dataset.data.forEach((value, index) => {
|
||||
const sliceAngle = (value / total) * 2 * Math.PI
|
||||
const color = dataset.colors?.[index] || options.colors[index % options.colors.length]
|
||||
|
||||
ctx.value.fillStyle = color
|
||||
ctx.value.beginPath()
|
||||
ctx.value.moveTo(centerX, centerY)
|
||||
ctx.value.arc(centerX, centerY, radius, currentAngle, currentAngle + sliceAngle)
|
||||
ctx.value.closePath()
|
||||
ctx.value.fill()
|
||||
|
||||
// 绘制标签
|
||||
if (options.showLabels !== false) {
|
||||
const labelAngle = currentAngle + sliceAngle / 2
|
||||
const labelX = centerX + Math.cos(labelAngle) * (radius * 0.7)
|
||||
const labelY = centerY + Math.sin(labelAngle) * (radius * 0.7)
|
||||
|
||||
ctx.value.fillStyle = '#fff'
|
||||
ctx.value.font = '24px sans-serif'
|
||||
ctx.value.textAlign = 'center'
|
||||
ctx.value.textBaseline = 'middle'
|
||||
|
||||
const percentage = ((value / total) * 100).toFixed(1)
|
||||
ctx.value.fillText(`${percentage}%`, labelX, labelY)
|
||||
}
|
||||
|
||||
currentAngle += sliceAngle
|
||||
})
|
||||
}
|
||||
|
||||
// 绘制环形图
|
||||
const drawDoughnutChart = () => {
|
||||
const { labels, datasets } = props.data
|
||||
const dataset = datasets[0]
|
||||
const options = chartOptions.value
|
||||
|
||||
const centerX = props.width / 2
|
||||
const centerY = props.height / 2
|
||||
const outerRadius = Math.min(props.width, props.height) / 2 - 40
|
||||
const innerRadius = outerRadius * 0.6
|
||||
|
||||
const total = dataset.data.reduce((sum, value) => sum + value, 0)
|
||||
let currentAngle = -Math.PI / 2
|
||||
|
||||
dataset.data.forEach((value, index) => {
|
||||
const sliceAngle = (value / total) * 2 * Math.PI
|
||||
const color = dataset.colors?.[index] || options.colors[index % options.colors.length]
|
||||
|
||||
// 绘制外圆弧
|
||||
ctx.value.fillStyle = color
|
||||
ctx.value.beginPath()
|
||||
ctx.value.arc(centerX, centerY, outerRadius, currentAngle, currentAngle + sliceAngle)
|
||||
ctx.value.arc(centerX, centerY, innerRadius, currentAngle + sliceAngle, currentAngle, true)
|
||||
ctx.value.closePath()
|
||||
ctx.value.fill()
|
||||
|
||||
currentAngle += sliceAngle
|
||||
})
|
||||
|
||||
// 绘制中心文字
|
||||
if (options.centerText) {
|
||||
ctx.value.fillStyle = '#333'
|
||||
ctx.value.font = 'bold 32px sans-serif'
|
||||
ctx.value.textAlign = 'center'
|
||||
ctx.value.textBaseline = 'middle'
|
||||
ctx.value.fillText(options.centerText, centerX, centerY)
|
||||
}
|
||||
}
|
||||
|
||||
// 绘制面积图
|
||||
const drawAreaChart = () => {
|
||||
const { labels, datasets } = props.data
|
||||
const options = chartOptions.value
|
||||
const padding = options.padding
|
||||
|
||||
const chartWidth = props.width - padding.left - padding.right
|
||||
const chartHeight = props.height - padding.top - padding.bottom
|
||||
const startX = padding.left
|
||||
const startY = padding.top
|
||||
|
||||
// 计算数据范围
|
||||
const allValues = datasets.flatMap(dataset => dataset.data)
|
||||
const minValue = Math.min(0, Math.min(...allValues))
|
||||
const maxValue = Math.max(...allValues)
|
||||
const valueRange = maxValue - minValue || 1
|
||||
|
||||
// 绘制网格
|
||||
if (options.grid.show) {
|
||||
drawGrid(startX, startY, chartWidth, chartHeight, labels.length, 5)
|
||||
}
|
||||
|
||||
// 绘制坐标轴
|
||||
drawAxis(startX, startY, chartWidth, chartHeight, labels, minValue, maxValue)
|
||||
|
||||
// 绘制面积
|
||||
datasets.forEach((dataset, datasetIndex) => {
|
||||
const color = dataset.color || options.colors[datasetIndex % options.colors.length]
|
||||
|
||||
// 创建渐变
|
||||
const gradient = ctx.value.createLinearGradient(0, startY, 0, startY + chartHeight)
|
||||
gradient.addColorStop(0, color + '80') // 50% 透明度
|
||||
gradient.addColorStop(1, color + '20') // 12.5% 透明度
|
||||
|
||||
ctx.value.fillStyle = gradient
|
||||
ctx.value.beginPath()
|
||||
|
||||
// 绘制面积路径
|
||||
dataset.data.forEach((value, index) => {
|
||||
const x = startX + (index / (labels.length - 1)) * chartWidth
|
||||
const y = startY + chartHeight - ((value - minValue) / valueRange) * chartHeight
|
||||
|
||||
if (index === 0) {
|
||||
ctx.value.moveTo(x, startY + chartHeight)
|
||||
ctx.value.lineTo(x, y)
|
||||
} else {
|
||||
ctx.value.lineTo(x, y)
|
||||
}
|
||||
})
|
||||
|
||||
ctx.value.lineTo(startX + chartWidth, startY + chartHeight)
|
||||
ctx.value.closePath()
|
||||
ctx.value.fill()
|
||||
|
||||
// 绘制线条
|
||||
ctx.value.strokeStyle = color
|
||||
ctx.value.lineWidth = 2
|
||||
ctx.value.beginPath()
|
||||
|
||||
dataset.data.forEach((value, index) => {
|
||||
const x = startX + (index / (labels.length - 1)) * chartWidth
|
||||
const y = startY + chartHeight - ((value - minValue) / valueRange) * chartHeight
|
||||
|
||||
if (index === 0) {
|
||||
ctx.value.moveTo(x, y)
|
||||
} else {
|
||||
ctx.value.lineTo(x, y)
|
||||
}
|
||||
})
|
||||
|
||||
ctx.value.stroke()
|
||||
})
|
||||
}
|
||||
|
||||
// 绘制网格
|
||||
const drawGrid = (startX, startY, width, height, xLines, yLines) => {
|
||||
const options = chartOptions.value
|
||||
|
||||
ctx.value.strokeStyle = options.grid.color
|
||||
ctx.value.lineWidth = options.grid.lineWidth
|
||||
ctx.value.setLineDash([2, 2])
|
||||
|
||||
// 垂直网格线
|
||||
for (let i = 0; i <= xLines; i++) {
|
||||
const x = startX + (i / xLines) * width
|
||||
ctx.value.beginPath()
|
||||
ctx.value.moveTo(x, startY)
|
||||
ctx.value.lineTo(x, startY + height)
|
||||
ctx.value.stroke()
|
||||
}
|
||||
|
||||
// 水平网格线
|
||||
for (let i = 0; i <= yLines; i++) {
|
||||
const y = startY + (i / yLines) * height
|
||||
ctx.value.beginPath()
|
||||
ctx.value.moveTo(startX, y)
|
||||
ctx.value.lineTo(startX + width, y)
|
||||
ctx.value.stroke()
|
||||
}
|
||||
|
||||
ctx.value.setLineDash([])
|
||||
}
|
||||
|
||||
// 绘制坐标轴
|
||||
const drawAxis = (startX, startY, width, height, labels, minValue, maxValue) => {
|
||||
const options = chartOptions.value
|
||||
|
||||
ctx.value.strokeStyle = '#333'
|
||||
ctx.value.lineWidth = 1
|
||||
ctx.value.fillStyle = options.axis.x.color
|
||||
ctx.value.font = `${options.axis.x.fontSize}px sans-serif`
|
||||
ctx.value.textAlign = 'center'
|
||||
ctx.value.textBaseline = 'top'
|
||||
|
||||
// X轴标签
|
||||
if (options.axis.x.show) {
|
||||
labels.forEach((label, index) => {
|
||||
const x = startX + (index / (labels.length - 1)) * width
|
||||
ctx.value.fillText(label, x, startY + height + 10)
|
||||
})
|
||||
}
|
||||
|
||||
// Y轴标签
|
||||
if (options.axis.y.show) {
|
||||
ctx.value.fillStyle = options.axis.y.color
|
||||
ctx.value.font = `${options.axis.y.fontSize}px sans-serif`
|
||||
ctx.value.textAlign = 'right'
|
||||
ctx.value.textBaseline = 'middle'
|
||||
|
||||
const valueRange = maxValue - minValue
|
||||
for (let i = 0; i <= 5; i++) {
|
||||
const value = minValue + (valueRange * i / 5)
|
||||
const y = startY + height - (i / 5) * height
|
||||
ctx.value.fillText(value.toFixed(1), startX - 10, y)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更新图例
|
||||
const updateLegend = () => {
|
||||
if (!props.showLegend) return
|
||||
|
||||
const { labels, datasets } = props.data
|
||||
const options = chartOptions.value
|
||||
|
||||
if (props.type === 'pie' || props.type === 'doughnut') {
|
||||
legendData.value = labels.map((label, index) => ({
|
||||
name: label,
|
||||
color: datasets[0].colors?.[index] || options.colors[index % options.colors.length]
|
||||
}))
|
||||
} else {
|
||||
legendData.value = datasets.map((dataset, index) => ({
|
||||
name: dataset.label || `数据${index + 1}`,
|
||||
color: dataset.color || options.colors[index % options.colors.length]
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
// 触摸事件处理
|
||||
const handleTouchStart = (e) => {
|
||||
if (!props.enableTooltip) return
|
||||
|
||||
const touch = e.touches[0]
|
||||
const rect = e.currentTarget.getBoundingClientRect()
|
||||
const x = touch.clientX - rect.left
|
||||
const y = touch.clientY - rect.top
|
||||
|
||||
showTooltipAtPosition(x, y)
|
||||
}
|
||||
|
||||
const handleTouchMove = (e) => {
|
||||
if (!props.enableTooltip) return
|
||||
|
||||
const touch = e.touches[0]
|
||||
const rect = e.currentTarget.getBoundingClientRect()
|
||||
const x = touch.clientX - rect.left
|
||||
const y = touch.clientY - rect.top
|
||||
|
||||
showTooltipAtPosition(x, y)
|
||||
}
|
||||
|
||||
const handleTouchEnd = () => {
|
||||
setTimeout(() => {
|
||||
showTooltip.value = false
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
// 显示工具提示
|
||||
const showTooltipAtPosition = (x, y) => {
|
||||
// 简化的工具提示逻辑
|
||||
const { labels, datasets } = props.data
|
||||
|
||||
if (props.type === 'line' || props.type === 'bar' || props.type === 'area') {
|
||||
const options = chartOptions.value
|
||||
const padding = options.padding
|
||||
const chartWidth = props.width - padding.left - padding.right
|
||||
|
||||
const relativeX = x - padding.left
|
||||
const dataIndex = Math.round((relativeX / chartWidth) * (labels.length - 1))
|
||||
|
||||
if (dataIndex >= 0 && dataIndex < labels.length) {
|
||||
tooltipData.value = {
|
||||
title: labels[dataIndex],
|
||||
items: datasets.map((dataset, index) => ({
|
||||
label: dataset.label || `数据${index + 1}`,
|
||||
value: dataset.data[dataIndex],
|
||||
color: dataset.color || options.colors[index % options.colors.length]
|
||||
}))
|
||||
}
|
||||
|
||||
tooltipStyle.value = {
|
||||
left: x + 'px',
|
||||
top: (y - 60) + 'px'
|
||||
}
|
||||
|
||||
showTooltip.value = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 图例点击事件
|
||||
const handleLegendClick = (index) => {
|
||||
emit('legendClick', { index, item: legendData.value[index] })
|
||||
}
|
||||
|
||||
// 监听数据变化
|
||||
watch(
|
||||
() => [props.data, props.type, props.options],
|
||||
() => {
|
||||
nextTick(() => {
|
||||
drawChart()
|
||||
})
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
// 组件挂载
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
drawChart()
|
||||
})
|
||||
})
|
||||
|
||||
return {
|
||||
canvasId,
|
||||
showTooltip,
|
||||
tooltipData,
|
||||
tooltipStyle,
|
||||
legendData,
|
||||
handleTouchStart,
|
||||
handleTouchMove,
|
||||
handleTouchEnd,
|
||||
handleLegendClick
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.chart-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.chart-canvas {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.chart-legend {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 24rpx;
|
||||
margin-top: 32rpx;
|
||||
padding: 0 32rpx;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 16rpx;
|
||||
background-color: #f8f9fa;
|
||||
|
||||
&:active {
|
||||
background-color: #e9ecef;
|
||||
}
|
||||
}
|
||||
|
||||
.legend-color {
|
||||
width: 24rpx;
|
||||
height: 24rpx;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.legend-text {
|
||||
font-size: 24rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.chart-tooltip {
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.tooltip-content {
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
color: #fff;
|
||||
padding: 16rpx 24rpx;
|
||||
border-radius: 8rpx;
|
||||
min-width: 200rpx;
|
||||
}
|
||||
|
||||
.tooltip-title {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tooltip-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
margin-bottom: 8rpx;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip-color {
|
||||
width: 16rpx;
|
||||
height: 16rpx;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.tooltip-label {
|
||||
font-size: 22rpx;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.tooltip-value {
|
||||
font-size: 22rpx;
|
||||
font-weight: 600;
|
||||
margin-left: auto;
|
||||
}
|
||||
</style>
|
||||
278
mini_program/common/components/empty/empty.vue
Normal file
278
mini_program/common/components/empty/empty.vue
Normal file
@@ -0,0 +1,278 @@
|
||||
<template>
|
||||
<view class="empty-container" :class="[size]">
|
||||
<!-- 空状态图片 -->
|
||||
<view class="empty-image">
|
||||
<image
|
||||
v-if="image"
|
||||
:src="image"
|
||||
mode="aspectFit"
|
||||
class="image"
|
||||
></image>
|
||||
<view v-else class="default-icon" :style="{ color: iconColor }">
|
||||
<text class="iconfont" :class="iconClass"></text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态文本 -->
|
||||
<view class="empty-content">
|
||||
<text class="empty-title" v-if="title">{{ title }}</text>
|
||||
<text class="empty-description" v-if="description">{{ description }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<view class="empty-actions" v-if="showAction">
|
||||
<button
|
||||
class="action-button"
|
||||
:class="[buttonType]"
|
||||
@click="onActionClick"
|
||||
>
|
||||
{{ actionText }}
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<!-- 自定义插槽 -->
|
||||
<slot></slot>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Empty',
|
||||
|
||||
props: {
|
||||
// 空状态图片
|
||||
image: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 图标类名
|
||||
iconClass: {
|
||||
type: String,
|
||||
default: 'icon-empty'
|
||||
},
|
||||
// 图标颜色
|
||||
iconColor: {
|
||||
type: String,
|
||||
default: '#cccccc'
|
||||
},
|
||||
// 标题
|
||||
title: {
|
||||
type: String,
|
||||
default: '暂无数据'
|
||||
},
|
||||
// 描述
|
||||
description: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 是否显示操作按钮
|
||||
showAction: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 操作按钮文本
|
||||
actionText: {
|
||||
type: String,
|
||||
default: '重新加载'
|
||||
},
|
||||
// 按钮类型
|
||||
buttonType: {
|
||||
type: String,
|
||||
default: 'primary',
|
||||
validator: (value) => ['primary', 'secondary', 'text'].includes(value)
|
||||
},
|
||||
// 大小
|
||||
size: {
|
||||
type: String,
|
||||
default: 'medium',
|
||||
validator: (value) => ['small', 'medium', 'large'].includes(value)
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
// 操作按钮点击
|
||||
onActionClick() {
|
||||
this.$emit('action')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.empty-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60rpx 40rpx;
|
||||
text-align: center;
|
||||
|
||||
&.small {
|
||||
padding: 40rpx 30rpx;
|
||||
|
||||
.empty-image {
|
||||
.image {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
}
|
||||
|
||||
.default-icon {
|
||||
font-size: 80rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.empty-description {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
}
|
||||
|
||||
&.medium {
|
||||
padding: 60rpx 40rpx;
|
||||
|
||||
.empty-image {
|
||||
.image {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
}
|
||||
|
||||
.default-icon {
|
||||
font-size: 100rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.empty-description {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
}
|
||||
|
||||
&.large {
|
||||
padding: 80rpx 50rpx;
|
||||
|
||||
.empty-image {
|
||||
.image {
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
}
|
||||
|
||||
.default-icon {
|
||||
font-size: 120rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
font-size: 36rpx;
|
||||
}
|
||||
|
||||
.empty-description {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-image {
|
||||
margin-bottom: 30rpx;
|
||||
|
||||
.image {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.default-icon {
|
||||
font-size: 100rpx;
|
||||
color: #cccccc;
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-content {
|
||||
margin-bottom: 40rpx;
|
||||
|
||||
.empty-title {
|
||||
display: block;
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
color: #666666;
|
||||
margin-bottom: 16rpx;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.empty-description {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
color: #999999;
|
||||
line-height: 1.5;
|
||||
max-width: 400rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-actions {
|
||||
.action-button {
|
||||
border: none;
|
||||
border-radius: 12rpx;
|
||||
padding: 20rpx 40rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
|
||||
&.primary {
|
||||
background: linear-gradient(135deg, #2E8B57, #3CB371);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
&.secondary {
|
||||
background: #ffffff;
|
||||
color: #2E8B57;
|
||||
border: 2rpx solid #2E8B57;
|
||||
}
|
||||
|
||||
&.text {
|
||||
background: transparent;
|
||||
color: #2E8B57;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 预设的空状态样式 */
|
||||
.empty-container {
|
||||
// 无数据
|
||||
&.no-data {
|
||||
.default-icon {
|
||||
color: #e6e6e6;
|
||||
}
|
||||
}
|
||||
|
||||
// 无网络
|
||||
&.no-network {
|
||||
.default-icon {
|
||||
color: #ff6b6b;
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索无结果
|
||||
&.no-search {
|
||||
.default-icon {
|
||||
color: #ffa726;
|
||||
}
|
||||
}
|
||||
|
||||
// 加载失败
|
||||
&.load-error {
|
||||
.default-icon {
|
||||
color: #ef5350;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
477
mini_program/common/components/form/form-item.vue
Normal file
477
mini_program/common/components/form/form-item.vue
Normal file
@@ -0,0 +1,477 @@
|
||||
<template>
|
||||
<view class="form-item" :class="formItemClass">
|
||||
<!-- 标签 -->
|
||||
<view v-if="label || $slots.label" class="form-item-label" :class="labelClass">
|
||||
<slot name="label">
|
||||
<text class="label-text">{{ label }}</text>
|
||||
<text v-if="required" class="required-mark">*</text>
|
||||
</slot>
|
||||
</view>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<view class="form-item-content" :class="contentClass">
|
||||
<slot></slot>
|
||||
|
||||
<!-- 错误提示 -->
|
||||
<view v-if="showError && errorMessage" class="form-item-error">
|
||||
<text class="error-text">{{ errorMessage }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 帮助文本 -->
|
||||
<view v-if="help" class="form-item-help">
|
||||
<text class="help-text">{{ help }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { computed, inject, ref, watch } from 'vue'
|
||||
|
||||
export default {
|
||||
name: 'FormItem',
|
||||
props: {
|
||||
// 标签文本
|
||||
label: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
|
||||
// 字段名
|
||||
prop: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
|
||||
// 是否必填
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
|
||||
// 验证规则
|
||||
rules: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
|
||||
// 错误信息
|
||||
error: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
|
||||
// 帮助文本
|
||||
help: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
|
||||
// 标签宽度
|
||||
labelWidth: {
|
||||
type: [String, Number],
|
||||
default: ''
|
||||
},
|
||||
|
||||
// 标签位置
|
||||
labelPosition: {
|
||||
type: String,
|
||||
default: '',
|
||||
validator: (value) => ['', 'left', 'right', 'top'].includes(value)
|
||||
},
|
||||
|
||||
// 是否显示冒号
|
||||
colon: {
|
||||
type: Boolean,
|
||||
default: null
|
||||
},
|
||||
|
||||
// 验证状态
|
||||
validateStatus: {
|
||||
type: String,
|
||||
default: '',
|
||||
validator: (value) => ['', 'success', 'warning', 'error', 'validating'].includes(value)
|
||||
},
|
||||
|
||||
// 是否显示验证图标
|
||||
hasFeedback: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
setup(props) {
|
||||
// 注入表单实例
|
||||
const form = inject('form', null)
|
||||
|
||||
// 响应式数据
|
||||
const errorMessage = ref(props.error)
|
||||
const validating = ref(false)
|
||||
|
||||
// 计算属性
|
||||
const formItemClass = computed(() => {
|
||||
const classes = []
|
||||
|
||||
if (props.required) {
|
||||
classes.push('is-required')
|
||||
}
|
||||
|
||||
if (errorMessage.value) {
|
||||
classes.push('is-error')
|
||||
}
|
||||
|
||||
if (props.validateStatus) {
|
||||
classes.push(`is-${props.validateStatus}`)
|
||||
}
|
||||
|
||||
if (validating.value) {
|
||||
classes.push('is-validating')
|
||||
}
|
||||
|
||||
const labelPosition = props.labelPosition || form?.labelPosition || 'right'
|
||||
classes.push(`label-${labelPosition}`)
|
||||
|
||||
return classes
|
||||
})
|
||||
|
||||
const labelClass = computed(() => {
|
||||
const classes = []
|
||||
|
||||
const showColon = props.colon !== null ? props.colon : (form?.colon ?? true)
|
||||
if (showColon && props.label) {
|
||||
classes.push('has-colon')
|
||||
}
|
||||
|
||||
return classes
|
||||
})
|
||||
|
||||
const contentClass = computed(() => {
|
||||
const classes = []
|
||||
|
||||
if (props.hasFeedback) {
|
||||
classes.push('has-feedback')
|
||||
}
|
||||
|
||||
return classes
|
||||
})
|
||||
|
||||
const labelStyle = computed(() => {
|
||||
const style = {}
|
||||
|
||||
const labelWidth = props.labelWidth || form?.labelWidth
|
||||
if (labelWidth) {
|
||||
style.width = typeof labelWidth === 'number' ? `${labelWidth}px` : labelWidth
|
||||
}
|
||||
|
||||
return style
|
||||
})
|
||||
|
||||
const showError = computed(() => {
|
||||
return props.validateStatus === 'error' || errorMessage.value
|
||||
})
|
||||
|
||||
// 验证方法
|
||||
const validate = async (trigger = '') => {
|
||||
if (!props.prop || !form) return true
|
||||
|
||||
const value = form.model[props.prop]
|
||||
const rules = [...(props.rules || []), ...(form.rules?.[props.prop] || [])]
|
||||
|
||||
if (rules.length === 0) return true
|
||||
|
||||
// 过滤触发器匹配的规则
|
||||
const filteredRules = trigger
|
||||
? rules.filter(rule => !rule.trigger || rule.trigger === trigger ||
|
||||
(Array.isArray(rule.trigger) && rule.trigger.includes(trigger)))
|
||||
: rules
|
||||
|
||||
if (filteredRules.length === 0) return true
|
||||
|
||||
validating.value = true
|
||||
errorMessage.value = ''
|
||||
|
||||
try {
|
||||
for (const rule of filteredRules) {
|
||||
// 必填验证
|
||||
if (rule.required && (value === undefined || value === null || value === '')) {
|
||||
throw new Error(rule.message || `${props.label}不能为空`)
|
||||
}
|
||||
|
||||
// 跳过空值的非必填验证
|
||||
if (!rule.required && (value === undefined || value === null || value === '')) {
|
||||
continue
|
||||
}
|
||||
|
||||
// 类型验证
|
||||
if (rule.type && !validateType(value, rule.type)) {
|
||||
throw new Error(rule.message || `${props.label}格式不正确`)
|
||||
}
|
||||
|
||||
// 长度验证
|
||||
if (rule.min !== undefined || rule.max !== undefined) {
|
||||
const len = typeof value === 'string' ? value.length :
|
||||
Array.isArray(value) ? value.length :
|
||||
typeof value === 'number' ? value : 0
|
||||
|
||||
if (rule.min !== undefined && len < rule.min) {
|
||||
throw new Error(rule.message || `${props.label}长度不能少于${rule.min}`)
|
||||
}
|
||||
|
||||
if (rule.max !== undefined && len > rule.max) {
|
||||
throw new Error(rule.message || `${props.label}长度不能超过${rule.max}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 正则验证
|
||||
if (rule.pattern && !rule.pattern.test(String(value))) {
|
||||
throw new Error(rule.message || `${props.label}格式不正确`)
|
||||
}
|
||||
|
||||
// 自定义验证器
|
||||
if (rule.validator && typeof rule.validator === 'function') {
|
||||
await new Promise((resolve, reject) => {
|
||||
rule.validator(rule, value, (error) => {
|
||||
if (error) {
|
||||
reject(new Error(error))
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
errorMessage.value = error.message
|
||||
return false
|
||||
} finally {
|
||||
validating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 类型验证
|
||||
const validateType = (value, type) => {
|
||||
switch (type) {
|
||||
case 'string':
|
||||
return typeof value === 'string'
|
||||
case 'number':
|
||||
return typeof value === 'number' && !isNaN(value)
|
||||
case 'boolean':
|
||||
return typeof value === 'boolean'
|
||||
case 'array':
|
||||
return Array.isArray(value)
|
||||
case 'object':
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
||||
case 'email':
|
||||
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(value))
|
||||
case 'url':
|
||||
return /^https?:\/\/.+/.test(String(value))
|
||||
case 'phone':
|
||||
return /^1[3-9]\d{9}$/.test(String(value))
|
||||
case 'idcard':
|
||||
return /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/.test(String(value))
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// 清除验证
|
||||
const clearValidate = () => {
|
||||
errorMessage.value = ''
|
||||
validating.value = false
|
||||
}
|
||||
|
||||
// 重置字段
|
||||
const resetField = () => {
|
||||
if (props.prop && form) {
|
||||
form.model[props.prop] = form.initialModel?.[props.prop]
|
||||
}
|
||||
clearValidate()
|
||||
}
|
||||
|
||||
// 监听错误信息变化
|
||||
watch(() => props.error, (newError) => {
|
||||
errorMessage.value = newError
|
||||
})
|
||||
|
||||
// 向表单注册字段
|
||||
if (form && props.prop) {
|
||||
form.addField({
|
||||
prop: props.prop,
|
||||
validate,
|
||||
clearValidate,
|
||||
resetField
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
formItemClass,
|
||||
labelClass,
|
||||
contentClass,
|
||||
labelStyle,
|
||||
showError,
|
||||
errorMessage,
|
||||
validating,
|
||||
validate,
|
||||
clearValidate,
|
||||
resetField
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.form-item {
|
||||
margin-bottom: 24px;
|
||||
|
||||
&.label-top {
|
||||
.form-item-label {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
&.label-left,
|
||||
&.label-right {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
|
||||
.form-item-label {
|
||||
flex-shrink: 0;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.form-item-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.label-left {
|
||||
.form-item-label {
|
||||
margin-right: 12px;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
&.label-right {
|
||||
.form-item-label {
|
||||
margin-right: 12px;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-required {
|
||||
.form-item-label {
|
||||
.required-mark {
|
||||
color: #f56c6c;
|
||||
margin-left: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-error {
|
||||
.form-item-content {
|
||||
:deep(.uni-input),
|
||||
:deep(.uni-textarea),
|
||||
:deep(.uni-picker) {
|
||||
border-color: #f56c6c !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-success {
|
||||
.form-item-content {
|
||||
:deep(.uni-input),
|
||||
:deep(.uni-textarea),
|
||||
:deep(.uni-picker) {
|
||||
border-color: #67c23a !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-warning {
|
||||
.form-item-content {
|
||||
:deep(.uni-input),
|
||||
:deep(.uni-textarea),
|
||||
:deep(.uni-picker) {
|
||||
border-color: #e6a23c !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-validating {
|
||||
.form-item-content {
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid #e4e7ed;
|
||||
border-top-color: #409eff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-item-label {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
line-height: 1.5;
|
||||
|
||||
.label-text {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
&.has-colon {
|
||||
.label-text::after {
|
||||
content: ':';
|
||||
margin-left: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-item-content {
|
||||
position: relative;
|
||||
|
||||
&.has-feedback {
|
||||
padding-right: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.form-item-error {
|
||||
margin-top: 4px;
|
||||
|
||||
.error-text {
|
||||
font-size: 12px;
|
||||
color: #f56c6c;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
|
||||
.form-item-help {
|
||||
margin-top: 4px;
|
||||
|
||||
.help-text {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: translateY(-50%) rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(-50%) rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
354
mini_program/common/components/form/form.vue
Normal file
354
mini_program/common/components/form/form.vue
Normal file
@@ -0,0 +1,354 @@
|
||||
<template>
|
||||
<view class="form" :class="formClass">
|
||||
<slot></slot>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { provide, reactive, ref, computed, onUnmounted } from 'vue'
|
||||
|
||||
export default {
|
||||
name: 'Form',
|
||||
props: {
|
||||
// 表单数据对象
|
||||
model: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
|
||||
// 表单验证规则
|
||||
rules: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
|
||||
// 标签宽度
|
||||
labelWidth: {
|
||||
type: [String, Number],
|
||||
default: '80px'
|
||||
},
|
||||
|
||||
// 标签位置
|
||||
labelPosition: {
|
||||
type: String,
|
||||
default: 'right',
|
||||
validator: (value) => ['left', 'right', 'top'].includes(value)
|
||||
},
|
||||
|
||||
// 是否显示标签后的冒号
|
||||
colon: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
|
||||
// 表单尺寸
|
||||
size: {
|
||||
type: String,
|
||||
default: 'medium',
|
||||
validator: (value) => ['small', 'medium', 'large'].includes(value)
|
||||
},
|
||||
|
||||
// 是否禁用表单
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
|
||||
// 是否显示验证错误信息
|
||||
showMessage: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
|
||||
// 是否以内联形式展示表单项
|
||||
inline: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
|
||||
// 是否隐藏必填字段的标签旁边的红色星号
|
||||
hideRequiredAsterisk: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
emits: ['validate'],
|
||||
|
||||
setup(props, { emit }) {
|
||||
// 表单字段集合
|
||||
const fields = ref([])
|
||||
|
||||
// 初始表单数据(用于重置)
|
||||
const initialModel = reactive({ ...props.model })
|
||||
|
||||
// 计算属性
|
||||
const formClass = computed(() => {
|
||||
const classes = []
|
||||
|
||||
classes.push(`form-${props.size}`)
|
||||
classes.push(`label-${props.labelPosition}`)
|
||||
|
||||
if (props.inline) {
|
||||
classes.push('form-inline')
|
||||
}
|
||||
|
||||
if (props.disabled) {
|
||||
classes.push('form-disabled')
|
||||
}
|
||||
|
||||
if (props.hideRequiredAsterisk) {
|
||||
classes.push('hide-required-asterisk')
|
||||
}
|
||||
|
||||
return classes
|
||||
})
|
||||
|
||||
// 添加字段
|
||||
const addField = (field) => {
|
||||
if (field.prop) {
|
||||
fields.value.push(field)
|
||||
}
|
||||
}
|
||||
|
||||
// 移除字段
|
||||
const removeField = (field) => {
|
||||
const index = fields.value.indexOf(field)
|
||||
if (index > -1) {
|
||||
fields.value.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// 验证整个表单
|
||||
const validate = async (callback) => {
|
||||
let valid = true
|
||||
let invalidFields = {}
|
||||
|
||||
if (fields.value.length === 0) {
|
||||
if (callback) callback(valid, invalidFields)
|
||||
return valid
|
||||
}
|
||||
|
||||
const promises = fields.value.map(async (field) => {
|
||||
try {
|
||||
const result = await field.validate()
|
||||
return { field: field.prop, valid: result }
|
||||
} catch (error) {
|
||||
return { field: field.prop, valid: false, error: error.message }
|
||||
}
|
||||
})
|
||||
|
||||
const results = await Promise.all(promises)
|
||||
|
||||
results.forEach(result => {
|
||||
if (!result.valid) {
|
||||
valid = false
|
||||
invalidFields[result.field] = result.error || '验证失败'
|
||||
}
|
||||
})
|
||||
|
||||
emit('validate', valid, invalidFields)
|
||||
|
||||
if (callback) {
|
||||
callback(valid, invalidFields)
|
||||
}
|
||||
|
||||
return valid
|
||||
}
|
||||
|
||||
// 验证指定字段
|
||||
const validateField = async (props, callback) => {
|
||||
const propsArray = Array.isArray(props) ? props : [props]
|
||||
let valid = true
|
||||
let invalidFields = {}
|
||||
|
||||
const targetFields = fields.value.filter(field =>
|
||||
propsArray.includes(field.prop)
|
||||
)
|
||||
|
||||
if (targetFields.length === 0) {
|
||||
if (callback) callback(valid, invalidFields)
|
||||
return valid
|
||||
}
|
||||
|
||||
const promises = targetFields.map(async (field) => {
|
||||
try {
|
||||
const result = await field.validate()
|
||||
return { field: field.prop, valid: result }
|
||||
} catch (error) {
|
||||
return { field: field.prop, valid: false, error: error.message }
|
||||
}
|
||||
})
|
||||
|
||||
const results = await Promise.all(promises)
|
||||
|
||||
results.forEach(result => {
|
||||
if (!result.valid) {
|
||||
valid = false
|
||||
invalidFields[result.field] = result.error || '验证失败'
|
||||
}
|
||||
})
|
||||
|
||||
if (callback) {
|
||||
callback(valid, invalidFields)
|
||||
}
|
||||
|
||||
return valid
|
||||
}
|
||||
|
||||
// 重置整个表单
|
||||
const resetFields = () => {
|
||||
fields.value.forEach(field => {
|
||||
field.resetField()
|
||||
})
|
||||
}
|
||||
|
||||
// 清除验证信息
|
||||
const clearValidate = (props) => {
|
||||
if (props) {
|
||||
const propsArray = Array.isArray(props) ? props : [props]
|
||||
fields.value
|
||||
.filter(field => propsArray.includes(field.prop))
|
||||
.forEach(field => field.clearValidate())
|
||||
} else {
|
||||
fields.value.forEach(field => field.clearValidate())
|
||||
}
|
||||
}
|
||||
|
||||
// 滚动到第一个错误字段
|
||||
const scrollToField = (prop) => {
|
||||
const field = fields.value.find(field => field.prop === prop)
|
||||
if (field && field.$el) {
|
||||
field.$el.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 提供给子组件的数据和方法
|
||||
provide('form', {
|
||||
model: props.model,
|
||||
rules: props.rules,
|
||||
labelWidth: props.labelWidth,
|
||||
labelPosition: props.labelPosition,
|
||||
colon: props.colon,
|
||||
size: props.size,
|
||||
disabled: props.disabled,
|
||||
showMessage: props.showMessage,
|
||||
hideRequiredAsterisk: props.hideRequiredAsterisk,
|
||||
initialModel,
|
||||
addField,
|
||||
removeField
|
||||
})
|
||||
|
||||
// 清理
|
||||
onUnmounted(() => {
|
||||
fields.value = []
|
||||
})
|
||||
|
||||
return {
|
||||
formClass,
|
||||
fields,
|
||||
validate,
|
||||
validateField,
|
||||
resetFields,
|
||||
clearValidate,
|
||||
scrollToField
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.form {
|
||||
&.form-inline {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
|
||||
:deep(.form-item) {
|
||||
display: inline-flex;
|
||||
margin-right: 16px;
|
||||
margin-bottom: 0;
|
||||
vertical-align: top;
|
||||
|
||||
.form-item-label {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.form-item-content {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.form-disabled {
|
||||
:deep(.form-item) {
|
||||
.form-item-label {
|
||||
color: #c0c4cc;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.hide-required-asterisk {
|
||||
:deep(.form-item) {
|
||||
.required-mark {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.form-small {
|
||||
:deep(.form-item) {
|
||||
margin-bottom: 18px;
|
||||
|
||||
.form-item-label {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.uni-input,
|
||||
.uni-textarea,
|
||||
.uni-picker {
|
||||
font-size: 12px;
|
||||
padding: 6px 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.form-medium {
|
||||
:deep(.form-item) {
|
||||
margin-bottom: 22px;
|
||||
|
||||
.form-item-label {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.uni-input,
|
||||
.uni-textarea,
|
||||
.uni-picker {
|
||||
font-size: 14px;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.form-large {
|
||||
:deep(.form-item) {
|
||||
margin-bottom: 26px;
|
||||
|
||||
.form-item-label {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.uni-input,
|
||||
.uni-textarea,
|
||||
.uni-picker {
|
||||
font-size: 16px;
|
||||
padding: 10px 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
282
mini_program/common/components/loading/loading.vue
Normal file
282
mini_program/common/components/loading/loading.vue
Normal file
@@ -0,0 +1,282 @@
|
||||
<template>
|
||||
<view class="loading-container" v-if="show" :class="{ 'with-mask': mask }">
|
||||
<!-- 遮罩层 -->
|
||||
<view class="loading-mask" v-if="mask" @tap="onMaskTap"></view>
|
||||
|
||||
<!-- 加载内容 -->
|
||||
<view class="loading-content" :class="[size, type]">
|
||||
<!-- 旋转加载器 -->
|
||||
<view class="loading-spinner" v-if="type === 'spinner'" :style="{ borderTopColor: color }">
|
||||
</view>
|
||||
|
||||
<!-- 点状加载器 -->
|
||||
<view class="loading-dots" v-if="type === 'dots'">
|
||||
<view class="dot" :style="{ backgroundColor: color }"></view>
|
||||
<view class="dot" :style="{ backgroundColor: color }"></view>
|
||||
<view class="dot" :style="{ backgroundColor: color }"></view>
|
||||
</view>
|
||||
|
||||
<!-- 脉冲加载器 -->
|
||||
<view class="loading-pulse" v-if="type === 'pulse'" :style="{ backgroundColor: color }">
|
||||
</view>
|
||||
|
||||
<!-- 波浪加载器 -->
|
||||
<view class="loading-wave" v-if="type === 'wave'">
|
||||
<view class="wave-bar" :style="{ backgroundColor: color }"></view>
|
||||
<view class="wave-bar" :style="{ backgroundColor: color }"></view>
|
||||
<view class="wave-bar" :style="{ backgroundColor: color }"></view>
|
||||
<view class="wave-bar" :style="{ backgroundColor: color }"></view>
|
||||
<view class="wave-bar" :style="{ backgroundColor: color }"></view>
|
||||
</view>
|
||||
|
||||
<!-- 加载文本 -->
|
||||
<text class="loading-text" v-if="text">{{ text }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Loading',
|
||||
|
||||
props: {
|
||||
// 是否显示
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 加载文本
|
||||
text: {
|
||||
type: String,
|
||||
default: '加载中...'
|
||||
},
|
||||
// 加载类型:spinner, dots, pulse, wave
|
||||
type: {
|
||||
type: String,
|
||||
default: 'spinner',
|
||||
validator: (value) => ['spinner', 'dots', 'pulse', 'wave'].includes(value)
|
||||
},
|
||||
// 大小:small, medium, large
|
||||
size: {
|
||||
type: String,
|
||||
default: 'medium',
|
||||
validator: (value) => ['small', 'medium', 'large'].includes(value)
|
||||
},
|
||||
// 颜色
|
||||
color: {
|
||||
type: String,
|
||||
default: '#2E8B57'
|
||||
},
|
||||
// 是否显示遮罩
|
||||
mask: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 是否可以点击遮罩关闭
|
||||
maskClosable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
// 点击遮罩
|
||||
onMaskTap() {
|
||||
if (this.maskClosable) {
|
||||
this.$emit('close')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.loading-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&.with-mask {
|
||||
.loading-mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.loading-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 16rpx;
|
||||
padding: 40rpx;
|
||||
backdrop-filter: blur(10rpx);
|
||||
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* 旋转加载器 */
|
||||
.loading-spinner {
|
||||
border: 4rpx solid rgba(0, 0, 0, 0.1);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.small .loading-spinner {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
border-width: 3rpx;
|
||||
}
|
||||
|
||||
.medium .loading-spinner {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
border-width: 4rpx;
|
||||
}
|
||||
|
||||
.large .loading-spinner {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-width: 5rpx;
|
||||
}
|
||||
|
||||
/* 点状加载器 */
|
||||
.loading-dots {
|
||||
display: flex;
|
||||
gap: 8rpx;
|
||||
|
||||
.dot {
|
||||
border-radius: 50%;
|
||||
animation: pulse 1.4s ease-in-out infinite both;
|
||||
|
||||
&:nth-child(1) { animation-delay: -0.32s; }
|
||||
&:nth-child(2) { animation-delay: -0.16s; }
|
||||
&:nth-child(3) { animation-delay: 0s; }
|
||||
}
|
||||
}
|
||||
|
||||
.small .loading-dots .dot {
|
||||
width: 8rpx;
|
||||
height: 8rpx;
|
||||
}
|
||||
|
||||
.medium .loading-dots .dot {
|
||||
width: 12rpx;
|
||||
height: 12rpx;
|
||||
}
|
||||
|
||||
.large .loading-dots .dot {
|
||||
width: 16rpx;
|
||||
height: 16rpx;
|
||||
}
|
||||
|
||||
/* 脉冲加载器 */
|
||||
.loading-pulse {
|
||||
border-radius: 50%;
|
||||
animation: pulse-scale 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.small .loading-pulse {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
}
|
||||
|
||||
.medium .loading-pulse {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
}
|
||||
|
||||
.large .loading-pulse {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
}
|
||||
|
||||
/* 波浪加载器 */
|
||||
.loading-wave {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 4rpx;
|
||||
|
||||
.wave-bar {
|
||||
border-radius: 2rpx;
|
||||
animation: wave 1.2s ease-in-out infinite;
|
||||
|
||||
&:nth-child(1) { animation-delay: -1.2s; }
|
||||
&:nth-child(2) { animation-delay: -1.1s; }
|
||||
&:nth-child(3) { animation-delay: -1.0s; }
|
||||
&:nth-child(4) { animation-delay: -0.9s; }
|
||||
&:nth-child(5) { animation-delay: -0.8s; }
|
||||
}
|
||||
}
|
||||
|
||||
.small .loading-wave .wave-bar {
|
||||
width: 4rpx;
|
||||
height: 20rpx;
|
||||
}
|
||||
|
||||
.medium .loading-wave .wave-bar {
|
||||
width: 6rpx;
|
||||
height: 30rpx;
|
||||
}
|
||||
|
||||
.large .loading-wave .wave-bar {
|
||||
width: 8rpx;
|
||||
height: 40rpx;
|
||||
}
|
||||
|
||||
/* 加载文本 */
|
||||
.loading-text {
|
||||
margin-top: 24rpx;
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 动画 */
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 80%, 100% {
|
||||
transform: scale(0);
|
||||
opacity: 1;
|
||||
}
|
||||
40% {
|
||||
transform: scale(1);
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse-scale {
|
||||
0%, 100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.2);
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes wave {
|
||||
0%, 40%, 100% {
|
||||
transform: scaleY(0.4);
|
||||
}
|
||||
20% {
|
||||
transform: scaleY(1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
1101
mini_program/common/components/map/map.vue
Normal file
1101
mini_program/common/components/map/map.vue
Normal file
File diff suppressed because it is too large
Load Diff
389
mini_program/common/components/modal/modal.vue
Normal file
389
mini_program/common/components/modal/modal.vue
Normal file
@@ -0,0 +1,389 @@
|
||||
<template>
|
||||
<view class="modal-container" v-if="show" :class="{ 'modal-show': show }">
|
||||
<!-- 遮罩层 -->
|
||||
<view
|
||||
class="modal-mask"
|
||||
:class="{ 'mask-transparent': !mask }"
|
||||
@tap="onMaskTap"
|
||||
></view>
|
||||
|
||||
<!-- 弹窗内容 -->
|
||||
<view class="modal-content" :class="[position, size]" :style="contentStyle">
|
||||
<!-- 头部 -->
|
||||
<view class="modal-header" v-if="showHeader">
|
||||
<text class="modal-title">{{ title }}</text>
|
||||
<view class="modal-close" v-if="showClose" @tap="onClose">
|
||||
<text class="close-icon">×</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 主体内容 -->
|
||||
<view class="modal-body" :class="{ 'no-header': !showHeader, 'no-footer': !showFooter }">
|
||||
<slot>
|
||||
<text class="modal-text" v-if="content">{{ content }}</text>
|
||||
</slot>
|
||||
</view>
|
||||
|
||||
<!-- 底部 -->
|
||||
<view class="modal-footer" v-if="showFooter">
|
||||
<slot name="footer">
|
||||
<button
|
||||
v-if="showCancel"
|
||||
class="modal-button cancel-button"
|
||||
@tap="onCancel"
|
||||
>
|
||||
{{ cancelText }}
|
||||
</button>
|
||||
<button
|
||||
v-if="showConfirm"
|
||||
class="modal-button confirm-button"
|
||||
@tap="onConfirm"
|
||||
>
|
||||
{{ confirmText }}
|
||||
</button>
|
||||
</slot>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Modal',
|
||||
|
||||
props: {
|
||||
// 是否显示
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 标题
|
||||
title: {
|
||||
type: String,
|
||||
default: '提示'
|
||||
},
|
||||
// 内容
|
||||
content: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 是否显示头部
|
||||
showHeader: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 是否显示底部
|
||||
showFooter: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 是否显示关闭按钮
|
||||
showClose: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 是否显示取消按钮
|
||||
showCancel: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 是否显示确认按钮
|
||||
showConfirm: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 取消按钮文本
|
||||
cancelText: {
|
||||
type: String,
|
||||
default: '取消'
|
||||
},
|
||||
// 确认按钮文本
|
||||
confirmText: {
|
||||
type: String,
|
||||
default: '确定'
|
||||
},
|
||||
// 是否显示遮罩
|
||||
mask: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 点击遮罩是否关闭
|
||||
maskClosable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 弹窗位置
|
||||
position: {
|
||||
type: String,
|
||||
default: 'center',
|
||||
validator: (value) => ['center', 'top', 'bottom'].includes(value)
|
||||
},
|
||||
// 弹窗大小
|
||||
size: {
|
||||
type: String,
|
||||
default: 'medium',
|
||||
validator: (value) => ['small', 'medium', 'large', 'full'].includes(value)
|
||||
},
|
||||
// 自定义样式
|
||||
customStyle: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
// 层级
|
||||
zIndex: {
|
||||
type: Number,
|
||||
default: 9999
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
contentStyle() {
|
||||
return {
|
||||
zIndex: this.zIndex + 1,
|
||||
...this.customStyle
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
// 遮罩点击
|
||||
onMaskTap() {
|
||||
if (this.maskClosable) {
|
||||
this.onClose()
|
||||
}
|
||||
},
|
||||
|
||||
// 关闭弹窗
|
||||
onClose() {
|
||||
this.$emit('close')
|
||||
this.$emit('update:show', false)
|
||||
},
|
||||
|
||||
// 取消
|
||||
onCancel() {
|
||||
this.$emit('cancel')
|
||||
this.onClose()
|
||||
},
|
||||
|
||||
// 确认
|
||||
onConfirm() {
|
||||
this.$emit('confirm')
|
||||
this.onClose()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.modal-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&.modal-show {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
|
||||
&.mask-transparent {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
position: relative;
|
||||
background: #ffffff;
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.2);
|
||||
transform: scale(0.8);
|
||||
transition: transform 0.3s ease;
|
||||
|
||||
.modal-show & {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
// 位置样式
|
||||
&.center {
|
||||
margin: 40rpx;
|
||||
}
|
||||
|
||||
&.top {
|
||||
position: absolute;
|
||||
top: 100rpx;
|
||||
left: 40rpx;
|
||||
right: 40rpx;
|
||||
transform: translateY(-100%);
|
||||
|
||||
.modal-show & {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
&.bottom {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
border-radius: 16rpx 16rpx 0 0;
|
||||
transform: translateY(100%);
|
||||
|
||||
.modal-show & {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
// 大小样式
|
||||
&.small {
|
||||
width: 500rpx;
|
||||
max-width: 80vw;
|
||||
}
|
||||
|
||||
&.medium {
|
||||
width: 600rpx;
|
||||
max-width: 85vw;
|
||||
}
|
||||
|
||||
&.large {
|
||||
width: 700rpx;
|
||||
max-width: 90vw;
|
||||
}
|
||||
|
||||
&.full {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
border-radius: 0;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
position: relative;
|
||||
padding: 40rpx 40rpx 20rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
|
||||
.modal-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
color: #333333;
|
||||
text-align: center;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
position: absolute;
|
||||
top: 20rpx;
|
||||
right: 20rpx;
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.close-icon {
|
||||
font-size: 40rpx;
|
||||
color: #999999;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 40rpx;
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
|
||||
&.no-header {
|
||||
padding-top: 40rpx;
|
||||
}
|
||||
|
||||
&.no-footer {
|
||||
padding-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.modal-text {
|
||||
font-size: 32rpx;
|
||||
color: #666666;
|
||||
line-height: 1.6;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
padding: 20rpx 40rpx 40rpx;
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
border-top: 1rpx solid #f0f0f0;
|
||||
|
||||
.modal-button {
|
||||
flex: 1;
|
||||
height: 80rpx;
|
||||
border: none;
|
||||
border-radius: 12rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
|
||||
&.cancel-button {
|
||||
background: #f5f5f5;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
&.confirm-button {
|
||||
background: linear-gradient(135deg, #2E8B57, #3CB371);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 动画效果 */
|
||||
@keyframes modalFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes modalSlideDown {
|
||||
from {
|
||||
transform: translateY(-100%);
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes modalSlideUp {
|
||||
from {
|
||||
transform: translateY(100%);
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
354
mini_program/common/components/picker/picker.vue
Normal file
354
mini_program/common/components/picker/picker.vue
Normal file
@@ -0,0 +1,354 @@
|
||||
<template>
|
||||
<view class="picker-container">
|
||||
<!-- 普通选择器 -->
|
||||
<picker
|
||||
v-if="mode === 'selector'"
|
||||
:value="selectorIndex"
|
||||
:range="range"
|
||||
:range-key="rangeKey"
|
||||
:disabled="disabled"
|
||||
@change="handleSelectorChange"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<view class="picker-input" :class="{ disabled }">
|
||||
<text class="picker-text" :class="{ placeholder: !displayValue }">
|
||||
{{ displayValue || placeholder }}
|
||||
</text>
|
||||
<text class="picker-arrow">▼</text>
|
||||
</view>
|
||||
</picker>
|
||||
|
||||
<!-- 多列选择器 -->
|
||||
<picker
|
||||
v-else-if="mode === 'multiSelector'"
|
||||
:value="multiIndex"
|
||||
:range="multiRange"
|
||||
:range-key="rangeKey"
|
||||
:disabled="disabled"
|
||||
mode="multiSelector"
|
||||
@change="handleMultiChange"
|
||||
@columnchange="handleColumnChange"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<view class="picker-input" :class="{ disabled }">
|
||||
<text class="picker-text" :class="{ placeholder: !displayValue }">
|
||||
{{ displayValue || placeholder }}
|
||||
</text>
|
||||
<text class="picker-arrow">▼</text>
|
||||
</view>
|
||||
</picker>
|
||||
|
||||
<!-- 时间选择器 -->
|
||||
<picker
|
||||
v-else-if="mode === 'time'"
|
||||
:value="timeValue"
|
||||
:start="start"
|
||||
:end="end"
|
||||
:disabled="disabled"
|
||||
mode="time"
|
||||
@change="handleTimeChange"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<view class="picker-input" :class="{ disabled }">
|
||||
<text class="picker-text" :class="{ placeholder: !displayValue }">
|
||||
{{ displayValue || placeholder }}
|
||||
</text>
|
||||
<text class="picker-arrow">▼</text>
|
||||
</view>
|
||||
</picker>
|
||||
|
||||
<!-- 日期选择器 -->
|
||||
<picker
|
||||
v-else-if="mode === 'date'"
|
||||
:value="dateValue"
|
||||
:start="start"
|
||||
:end="end"
|
||||
:fields="fields"
|
||||
:disabled="disabled"
|
||||
mode="date"
|
||||
@change="handleDateChange"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<view class="picker-input" :class="{ disabled }">
|
||||
<text class="picker-text" :class="{ placeholder: !displayValue }">
|
||||
{{ displayValue || placeholder }}
|
||||
</text>
|
||||
<text class="picker-arrow">▼</text>
|
||||
</view>
|
||||
</picker>
|
||||
|
||||
<!-- 地区选择器 -->
|
||||
<picker
|
||||
v-else-if="mode === 'region'"
|
||||
:value="regionValue"
|
||||
:custom-item="customItem"
|
||||
:disabled="disabled"
|
||||
mode="region"
|
||||
@change="handleRegionChange"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<view class="picker-input" :class="{ disabled }">
|
||||
<text class="picker-text" :class="{ placeholder: !displayValue }">
|
||||
{{ displayValue || placeholder }}
|
||||
</text>
|
||||
<text class="picker-arrow">▼</text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Picker',
|
||||
props: {
|
||||
// 选择器类型
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'selector',
|
||||
validator: (value) => ['selector', 'multiSelector', 'time', 'date', 'region'].includes(value)
|
||||
},
|
||||
// 当前选中值
|
||||
value: {
|
||||
type: [String, Number, Array],
|
||||
default: ''
|
||||
},
|
||||
// 占位符
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '请选择'
|
||||
},
|
||||
// 是否禁用
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 普通选择器数据源
|
||||
range: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
// 对象数组时指定显示的key
|
||||
rangeKey: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 多列选择器数据源
|
||||
multiRange: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
// 时间选择器开始时间
|
||||
start: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 时间选择器结束时间
|
||||
end: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 日期选择器精度
|
||||
fields: {
|
||||
type: String,
|
||||
default: 'day',
|
||||
validator: (value) => ['year', 'month', 'day'].includes(value)
|
||||
},
|
||||
// 地区选择器自定义项
|
||||
customItem: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectorIndex: 0,
|
||||
multiIndex: [],
|
||||
timeValue: '',
|
||||
dateValue: '',
|
||||
regionValue: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
displayValue() {
|
||||
switch (this.mode) {
|
||||
case 'selector':
|
||||
if (this.range.length && this.selectorIndex >= 0) {
|
||||
const item = this.range[this.selectorIndex]
|
||||
return this.rangeKey ? item[this.rangeKey] : item
|
||||
}
|
||||
return ''
|
||||
case 'multiSelector':
|
||||
if (this.multiRange.length && this.multiIndex.length) {
|
||||
return this.multiIndex.map((index, columnIndex) => {
|
||||
const column = this.multiRange[columnIndex]
|
||||
if (column && column[index]) {
|
||||
const item = column[index]
|
||||
return this.rangeKey ? item[this.rangeKey] : item
|
||||
}
|
||||
return ''
|
||||
}).join(' ')
|
||||
}
|
||||
return ''
|
||||
case 'time':
|
||||
return this.timeValue
|
||||
case 'date':
|
||||
return this.dateValue
|
||||
case 'region':
|
||||
return this.regionValue.join(' ')
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value: {
|
||||
handler(newVal) {
|
||||
this.initValue(newVal)
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 初始化值
|
||||
initValue(value) {
|
||||
switch (this.mode) {
|
||||
case 'selector':
|
||||
if (typeof value === 'number') {
|
||||
this.selectorIndex = value
|
||||
} else if (typeof value === 'string' && this.range.length) {
|
||||
const index = this.range.findIndex(item => {
|
||||
return this.rangeKey ? item[this.rangeKey] === value : item === value
|
||||
})
|
||||
this.selectorIndex = index >= 0 ? index : 0
|
||||
}
|
||||
break
|
||||
case 'multiSelector':
|
||||
this.multiIndex = Array.isArray(value) ? [...value] : []
|
||||
break
|
||||
case 'time':
|
||||
this.timeValue = value || ''
|
||||
break
|
||||
case 'date':
|
||||
this.dateValue = value || ''
|
||||
break
|
||||
case 'region':
|
||||
this.regionValue = Array.isArray(value) ? [...value] : []
|
||||
break
|
||||
}
|
||||
},
|
||||
// 普通选择器改变
|
||||
handleSelectorChange(e) {
|
||||
const index = e.detail.value
|
||||
this.selectorIndex = index
|
||||
const item = this.range[index]
|
||||
const value = this.rangeKey ? item[this.rangeKey] : item
|
||||
this.$emit('change', {
|
||||
value: value,
|
||||
index: index,
|
||||
item: item
|
||||
})
|
||||
},
|
||||
// 多列选择器改变
|
||||
handleMultiChange(e) {
|
||||
const indexes = e.detail.value
|
||||
this.multiIndex = [...indexes]
|
||||
const values = indexes.map((index, columnIndex) => {
|
||||
const column = this.multiRange[columnIndex]
|
||||
if (column && column[index]) {
|
||||
const item = column[index]
|
||||
return this.rangeKey ? item[this.rangeKey] : item
|
||||
}
|
||||
return ''
|
||||
})
|
||||
this.$emit('change', {
|
||||
value: values,
|
||||
index: indexes
|
||||
})
|
||||
},
|
||||
// 多列选择器列改变
|
||||
handleColumnChange(e) {
|
||||
this.$emit('columnchange', e.detail)
|
||||
},
|
||||
// 时间选择器改变
|
||||
handleTimeChange(e) {
|
||||
const value = e.detail.value
|
||||
this.timeValue = value
|
||||
this.$emit('change', {
|
||||
value: value
|
||||
})
|
||||
},
|
||||
// 日期选择器改变
|
||||
handleDateChange(e) {
|
||||
const value = e.detail.value
|
||||
this.dateValue = value
|
||||
this.$emit('change', {
|
||||
value: value
|
||||
})
|
||||
},
|
||||
// 地区选择器改变
|
||||
handleRegionChange(e) {
|
||||
const value = e.detail.value
|
||||
this.regionValue = [...value]
|
||||
this.$emit('change', {
|
||||
value: value
|
||||
})
|
||||
},
|
||||
// 取消选择
|
||||
handleCancel() {
|
||||
this.$emit('cancel')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.picker-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.picker-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 24rpx 32rpx;
|
||||
background-color: #fff;
|
||||
border: 2rpx solid #e5e5e5;
|
||||
border-radius: 12rpx;
|
||||
min-height: 88rpx;
|
||||
box-sizing: border-box;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:active {
|
||||
border-color: #2e8b57;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
background-color: #f5f5f5;
|
||||
border-color: #d9d9d9;
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
.picker-text {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
line-height: 1.4;
|
||||
|
||||
&.placeholder {
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.picker-arrow {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
margin-left: 16rpx;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.picker-input:active .picker-arrow {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
</style>
|
||||
360
mini_program/common/components/search/search.vue
Normal file
360
mini_program/common/components/search/search.vue
Normal file
@@ -0,0 +1,360 @@
|
||||
<template>
|
||||
<view class="search-container" :class="containerClass">
|
||||
<view class="search-box" :class="searchBoxClass">
|
||||
<!-- 搜索图标 -->
|
||||
<view class="search-icon" v-if="showIcon">
|
||||
<text class="icon">🔍</text>
|
||||
</view>
|
||||
|
||||
<!-- 搜索输入框 -->
|
||||
<input
|
||||
class="search-input"
|
||||
:class="inputClass"
|
||||
:type="inputType"
|
||||
:value="inputValue"
|
||||
:placeholder="placeholder"
|
||||
:placeholder-style="placeholderStyle"
|
||||
:disabled="disabled"
|
||||
:maxlength="maxlength"
|
||||
:focus="focus"
|
||||
:confirm-type="confirmType"
|
||||
@input="handleInput"
|
||||
@focus="handleFocus"
|
||||
@blur="handleBlur"
|
||||
@confirm="handleConfirm"
|
||||
@keyboardheightchange="handleKeyboardHeightChange"
|
||||
/>
|
||||
|
||||
<!-- 清除按钮 -->
|
||||
<view
|
||||
class="clear-btn"
|
||||
v-if="showClear && inputValue"
|
||||
@click="handleClear"
|
||||
>
|
||||
<text class="icon">✕</text>
|
||||
</view>
|
||||
|
||||
<!-- 搜索按钮 -->
|
||||
<view
|
||||
class="search-btn"
|
||||
v-if="showSearchBtn"
|
||||
:class="{ disabled: !inputValue }"
|
||||
@click="handleSearch"
|
||||
>
|
||||
<text class="btn-text">{{ searchBtnText }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 取消按钮 -->
|
||||
<view
|
||||
class="cancel-btn"
|
||||
v-if="showCancel && (isFocused || inputValue)"
|
||||
@click="handleCancel"
|
||||
>
|
||||
<text class="cancel-text">取消</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Search',
|
||||
props: {
|
||||
// 输入框值
|
||||
value: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 占位符
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '请输入搜索内容'
|
||||
},
|
||||
// 占位符样式
|
||||
placeholderStyle: {
|
||||
type: String,
|
||||
default: 'color: #999'
|
||||
},
|
||||
// 输入框类型
|
||||
inputType: {
|
||||
type: String,
|
||||
default: 'text'
|
||||
},
|
||||
// 最大输入长度
|
||||
maxlength: {
|
||||
type: Number,
|
||||
default: 140
|
||||
},
|
||||
// 是否禁用
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 是否自动聚焦
|
||||
focus: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 确认按钮文字
|
||||
confirmType: {
|
||||
type: String,
|
||||
default: 'search'
|
||||
},
|
||||
// 是否显示搜索图标
|
||||
showIcon: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 是否显示清除按钮
|
||||
showClear: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 是否显示搜索按钮
|
||||
showSearchBtn: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 搜索按钮文字
|
||||
searchBtnText: {
|
||||
type: String,
|
||||
default: '搜索'
|
||||
},
|
||||
// 是否显示取消按钮
|
||||
showCancel: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 搜索框形状
|
||||
shape: {
|
||||
type: String,
|
||||
default: 'round', // round, square
|
||||
validator: (value) => ['round', 'square'].includes(value)
|
||||
},
|
||||
// 搜索框大小
|
||||
size: {
|
||||
type: String,
|
||||
default: 'normal', // large, normal, small
|
||||
validator: (value) => ['large', 'normal', 'small'].includes(value)
|
||||
},
|
||||
// 背景色
|
||||
backgroundColor: {
|
||||
type: String,
|
||||
default: '#f5f5f5'
|
||||
},
|
||||
// 自定义样式类
|
||||
customClass: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
inputValue: '',
|
||||
isFocused: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
containerClass() {
|
||||
return [
|
||||
this.customClass,
|
||||
`search-${this.size}`
|
||||
]
|
||||
},
|
||||
searchBoxClass() {
|
||||
return [
|
||||
`search-${this.shape}`,
|
||||
{ 'search-focused': this.isFocused },
|
||||
{ 'search-disabled': this.disabled }
|
||||
]
|
||||
},
|
||||
inputClass() {
|
||||
return {
|
||||
'input-with-icon': this.showIcon,
|
||||
'input-with-btn': this.showSearchBtn
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value: {
|
||||
handler(newVal) {
|
||||
this.inputValue = newVal
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 输入事件
|
||||
handleInput(e) {
|
||||
const value = e.detail.value
|
||||
this.inputValue = value
|
||||
this.$emit('input', value)
|
||||
this.$emit('change', value)
|
||||
},
|
||||
// 聚焦事件
|
||||
handleFocus(e) {
|
||||
this.isFocused = true
|
||||
this.$emit('focus', e)
|
||||
},
|
||||
// 失焦事件
|
||||
handleBlur(e) {
|
||||
this.isFocused = false
|
||||
this.$emit('blur', e)
|
||||
},
|
||||
// 确认事件
|
||||
handleConfirm(e) {
|
||||
const value = e.detail.value
|
||||
this.$emit('confirm', value)
|
||||
this.$emit('search', value)
|
||||
},
|
||||
// 键盘高度变化
|
||||
handleKeyboardHeightChange(e) {
|
||||
this.$emit('keyboardheightchange', e)
|
||||
},
|
||||
// 清除输入
|
||||
handleClear() {
|
||||
this.inputValue = ''
|
||||
this.$emit('input', '')
|
||||
this.$emit('change', '')
|
||||
this.$emit('clear')
|
||||
},
|
||||
// 搜索按钮点击
|
||||
handleSearch() {
|
||||
if (!this.inputValue) return
|
||||
this.$emit('search', this.inputValue)
|
||||
},
|
||||
// 取消按钮点击
|
||||
handleCancel() {
|
||||
this.inputValue = ''
|
||||
this.isFocused = false
|
||||
this.$emit('input', '')
|
||||
this.$emit('change', '')
|
||||
this.$emit('cancel')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.search-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
||||
&.search-large {
|
||||
.search-box {
|
||||
height: 88rpx;
|
||||
}
|
||||
.search-input {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
}
|
||||
|
||||
&.search-normal {
|
||||
.search-box {
|
||||
height: 72rpx;
|
||||
}
|
||||
.search-input {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
}
|
||||
|
||||
&.search-small {
|
||||
.search-box {
|
||||
height: 60rpx;
|
||||
}
|
||||
.search-input {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-box {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: #f5f5f5;
|
||||
padding: 0 24rpx;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&.search-round {
|
||||
border-radius: 36rpx;
|
||||
}
|
||||
|
||||
&.search-square {
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
&.search-focused {
|
||||
background-color: #fff;
|
||||
border: 2rpx solid #2e8b57;
|
||||
}
|
||||
|
||||
&.search-disabled {
|
||||
opacity: 0.6;
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
margin-right: 16rpx;
|
||||
|
||||
.icon {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
border: none;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
color: #333;
|
||||
|
||||
&.input-with-icon {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
&.input-with-btn {
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.clear-btn {
|
||||
margin-left: 16rpx;
|
||||
padding: 8rpx;
|
||||
|
||||
.icon {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.search-btn {
|
||||
margin-left: 16rpx;
|
||||
padding: 8rpx 16rpx;
|
||||
background-color: #2e8b57;
|
||||
border-radius: 8rpx;
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
font-size: 24rpx;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
margin-left: 24rpx;
|
||||
padding: 8rpx 16rpx;
|
||||
|
||||
.cancel-text {
|
||||
font-size: 28rpx;
|
||||
color: #2e8b57;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
494
mini_program/common/components/swiper/swiper.vue
Normal file
494
mini_program/common/components/swiper/swiper.vue
Normal file
@@ -0,0 +1,494 @@
|
||||
<template>
|
||||
<view class="swiper-container" :class="containerClass">
|
||||
<swiper
|
||||
class="swiper"
|
||||
:class="swiperClass"
|
||||
:indicator-dots="indicatorDots"
|
||||
:indicator-color="indicatorColor"
|
||||
:indicator-active-color="indicatorActiveColor"
|
||||
:autoplay="autoplay"
|
||||
:interval="interval"
|
||||
:duration="duration"
|
||||
:circular="circular"
|
||||
:vertical="vertical"
|
||||
:previous-margin="previousMargin"
|
||||
:next-margin="nextMargin"
|
||||
:display-multiple-items="displayMultipleItems"
|
||||
:skip-hidden-item-layout="skipHiddenItemLayout"
|
||||
:easing-function="easingFunction"
|
||||
@change="handleChange"
|
||||
@transition="handleTransition"
|
||||
@animationfinish="handleAnimationFinish"
|
||||
>
|
||||
<swiper-item
|
||||
v-for="(item, index) in list"
|
||||
:key="getItemKey(item, index)"
|
||||
:item-id="getItemId(item, index)"
|
||||
class="swiper-item"
|
||||
@click="handleItemClick(item, index)"
|
||||
>
|
||||
<!-- 自定义内容插槽 -->
|
||||
<slot name="item" :item="item" :index="index">
|
||||
<!-- 默认图片展示 -->
|
||||
<view class="swiper-item-default" v-if="item.image || item.src || item.url">
|
||||
<image
|
||||
class="swiper-image"
|
||||
:src="item.image || item.src || item.url"
|
||||
:mode="imageMode"
|
||||
:lazy-load="lazyLoad"
|
||||
@load="handleImageLoad"
|
||||
@error="handleImageError"
|
||||
/>
|
||||
<!-- 标题和描述 -->
|
||||
<view class="swiper-content" v-if="item.title || item.description">
|
||||
<view class="swiper-title" v-if="item.title">
|
||||
{{ item.title }}
|
||||
</view>
|
||||
<view class="swiper-desc" v-if="item.description">
|
||||
{{ item.description }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 纯文本内容 -->
|
||||
<view class="swiper-item-text" v-else>
|
||||
<view class="text-content">
|
||||
{{ item.title || item.text || item }}
|
||||
</view>
|
||||
</view>
|
||||
</slot>
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
|
||||
<!-- 自定义指示器 -->
|
||||
<view
|
||||
class="custom-indicators"
|
||||
v-if="showCustomIndicators"
|
||||
:class="indicatorsClass"
|
||||
>
|
||||
<view
|
||||
class="indicator-item"
|
||||
:class="{ active: index === currentIndex }"
|
||||
v-for="(item, index) in list"
|
||||
:key="index"
|
||||
@click="handleIndicatorClick(index)"
|
||||
>
|
||||
<view class="indicator-dot"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 左右箭头 -->
|
||||
<view class="swiper-arrows" v-if="showArrows">
|
||||
<view class="arrow arrow-left" @click="handlePrev">
|
||||
<text class="arrow-icon">‹</text>
|
||||
</view>
|
||||
<view class="arrow arrow-right" @click="handleNext">
|
||||
<text class="arrow-icon">›</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Swiper',
|
||||
props: {
|
||||
// 轮播数据
|
||||
list: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
// 是否显示指示点
|
||||
indicatorDots: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 指示点颜色
|
||||
indicatorColor: {
|
||||
type: String,
|
||||
default: 'rgba(0, 0, 0, .3)'
|
||||
},
|
||||
// 当前选中的指示点颜色
|
||||
indicatorActiveColor: {
|
||||
type: String,
|
||||
default: '#000000'
|
||||
},
|
||||
// 是否自动切换
|
||||
autoplay: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 自动切换时间间隔
|
||||
interval: {
|
||||
type: Number,
|
||||
default: 5000
|
||||
},
|
||||
// 滑动动画时长
|
||||
duration: {
|
||||
type: Number,
|
||||
default: 500
|
||||
},
|
||||
// 是否采用衔接滑动
|
||||
circular: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 滑动方向是否为纵向
|
||||
vertical: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 前边距
|
||||
previousMargin: {
|
||||
type: String,
|
||||
default: '0px'
|
||||
},
|
||||
// 后边距
|
||||
nextMargin: {
|
||||
type: String,
|
||||
default: '0px'
|
||||
},
|
||||
// 同时显示的滑块数量
|
||||
displayMultipleItems: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
// 是否跳过未显示的滑块布局
|
||||
skipHiddenItemLayout: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 指定swiper切换缓动动画类型
|
||||
easingFunction: {
|
||||
type: String,
|
||||
default: 'default'
|
||||
},
|
||||
// 图片裁剪模式
|
||||
imageMode: {
|
||||
type: String,
|
||||
default: 'aspectFill'
|
||||
},
|
||||
// 图片懒加载
|
||||
lazyLoad: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 轮播图高度
|
||||
height: {
|
||||
type: String,
|
||||
default: '300rpx'
|
||||
},
|
||||
// 圆角大小
|
||||
borderRadius: {
|
||||
type: String,
|
||||
default: '0'
|
||||
},
|
||||
// 是否显示自定义指示器
|
||||
showCustomIndicators: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 指示器位置
|
||||
indicatorPosition: {
|
||||
type: String,
|
||||
default: 'bottom', // top, bottom, left, right
|
||||
validator: (value) => ['top', 'bottom', 'left', 'right'].includes(value)
|
||||
},
|
||||
// 是否显示箭头
|
||||
showArrows: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 唯一标识字段
|
||||
keyField: {
|
||||
type: String,
|
||||
default: 'id'
|
||||
},
|
||||
// 自定义样式类
|
||||
customClass: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentIndex: 0
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
containerClass() {
|
||||
return [
|
||||
this.customClass,
|
||||
{ 'swiper-vertical': this.vertical }
|
||||
]
|
||||
},
|
||||
swiperClass() {
|
||||
return {
|
||||
'swiper-custom': this.showCustomIndicators || this.showArrows
|
||||
}
|
||||
},
|
||||
indicatorsClass() {
|
||||
return [
|
||||
`indicators-${this.indicatorPosition}`
|
||||
]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 获取项目唯一标识
|
||||
getItemKey(item, index) {
|
||||
if (typeof item === 'object' && item[this.keyField]) {
|
||||
return item[this.keyField]
|
||||
}
|
||||
return index
|
||||
},
|
||||
// 获取项目ID
|
||||
getItemId(item, index) {
|
||||
return this.getItemKey(item, index).toString()
|
||||
},
|
||||
// 轮播切换事件
|
||||
handleChange(e) {
|
||||
this.currentIndex = e.detail.current
|
||||
this.$emit('change', {
|
||||
current: e.detail.current,
|
||||
source: e.detail.source,
|
||||
item: this.list[e.detail.current]
|
||||
})
|
||||
},
|
||||
// 轮播过渡事件
|
||||
handleTransition(e) {
|
||||
this.$emit('transition', e.detail)
|
||||
},
|
||||
// 动画结束事件
|
||||
handleAnimationFinish(e) {
|
||||
this.$emit('animationfinish', e.detail)
|
||||
},
|
||||
// 项目点击事件
|
||||
handleItemClick(item, index) {
|
||||
this.$emit('click', { item, index })
|
||||
},
|
||||
// 图片加载成功
|
||||
handleImageLoad(e) {
|
||||
this.$emit('imageload', e)
|
||||
},
|
||||
// 图片加载失败
|
||||
handleImageError(e) {
|
||||
this.$emit('imageerror', e)
|
||||
},
|
||||
// 指示器点击
|
||||
handleIndicatorClick(index) {
|
||||
this.currentIndex = index
|
||||
this.$emit('change', {
|
||||
current: index,
|
||||
source: 'touch',
|
||||
item: this.list[index]
|
||||
})
|
||||
},
|
||||
// 上一张
|
||||
handlePrev() {
|
||||
const prevIndex = this.currentIndex > 0 ? this.currentIndex - 1 : this.list.length - 1
|
||||
this.handleIndicatorClick(prevIndex)
|
||||
},
|
||||
// 下一张
|
||||
handleNext() {
|
||||
const nextIndex = this.currentIndex < this.list.length - 1 ? this.currentIndex + 1 : 0
|
||||
this.handleIndicatorClick(nextIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.swiper-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: v-bind(height);
|
||||
overflow: hidden;
|
||||
border-radius: v-bind(borderRadius);
|
||||
|
||||
&.swiper-vertical {
|
||||
.custom-indicators {
|
||||
flex-direction: column;
|
||||
|
||||
&.indicators-left {
|
||||
left: 20rpx;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
&.indicators-right {
|
||||
right: 20rpx;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.swiper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
&.swiper-custom {
|
||||
.wx-swiper-dots {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.swiper-item {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.swiper-item-default {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.swiper-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.swiper-content {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
|
||||
padding: 40rpx 32rpx 32rpx;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.swiper-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8rpx;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.swiper-desc {
|
||||
font-size: 24rpx;
|
||||
opacity: 0.9;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.swiper-item-text {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #f5f5f5;
|
||||
|
||||
.text-content {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
padding: 32rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-indicators {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16rpx;
|
||||
z-index: 10;
|
||||
|
||||
&.indicators-top {
|
||||
top: 20rpx;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
&.indicators-bottom {
|
||||
bottom: 20rpx;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
&.indicators-left {
|
||||
left: 20rpx;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&.indicators-right {
|
||||
right: 20rpx;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.indicator-item {
|
||||
padding: 8rpx;
|
||||
cursor: pointer;
|
||||
|
||||
.indicator-dot {
|
||||
width: 16rpx;
|
||||
height: 16rpx;
|
||||
border-radius: 50%;
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
&.active .indicator-dot {
|
||||
background-color: #fff;
|
||||
transform: scale(1.2);
|
||||
}
|
||||
}
|
||||
|
||||
.swiper-arrows {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: auto;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
|
||||
&.arrow-left {
|
||||
left: 20rpx;
|
||||
}
|
||||
|
||||
&.arrow-right {
|
||||
right: 20rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.arrow-icon {
|
||||
font-size: 48rpx;
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
line-height: 1;
|
||||
}
|
||||
</style>
|
||||
936
mini_program/common/components/table/table.vue
Normal file
936
mini_program/common/components/table/table.vue
Normal file
@@ -0,0 +1,936 @@
|
||||
<template>
|
||||
<view class="table-container" :class="containerClass">
|
||||
<!-- 表格头部 -->
|
||||
<view v-if="showHeader" class="table-header">
|
||||
<slot name="header">
|
||||
<view class="table-title" v-if="title">{{ title }}</view>
|
||||
<view class="table-actions" v-if="$slots.actions">
|
||||
<slot name="actions"></slot>
|
||||
</view>
|
||||
</slot>
|
||||
</view>
|
||||
|
||||
<!-- 表格主体 -->
|
||||
<scroll-view
|
||||
class="table-scroll"
|
||||
:scroll-x="true"
|
||||
:scroll-y="height !== 'auto'"
|
||||
:style="scrollStyle"
|
||||
@scroll="handleScroll"
|
||||
>
|
||||
<view class="table" :class="tableClass">
|
||||
<!-- 表头 -->
|
||||
<view v-if="showTableHeader" class="table-thead">
|
||||
<view class="table-tr table-header-row">
|
||||
<!-- 选择列 -->
|
||||
<view v-if="selectable" class="table-th table-selection-column">
|
||||
<checkbox
|
||||
:checked="isAllSelected"
|
||||
:indeterminate="isIndeterminate"
|
||||
@change="handleSelectAll"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 序号列 -->
|
||||
<view v-if="showIndex" class="table-th table-index-column">
|
||||
{{ indexLabel }}
|
||||
</view>
|
||||
|
||||
<!-- 数据列 -->
|
||||
<view
|
||||
v-for="column in columns"
|
||||
:key="column.prop || column.key"
|
||||
class="table-th"
|
||||
:class="getColumnClass(column)"
|
||||
:style="getColumnStyle(column)"
|
||||
@click="handleSort(column)"
|
||||
>
|
||||
<view class="table-th-content">
|
||||
<text class="column-title">{{ column.label }}</text>
|
||||
|
||||
<!-- 排序图标 -->
|
||||
<view v-if="column.sortable" class="sort-icons">
|
||||
<text
|
||||
class="sort-icon sort-asc"
|
||||
:class="{ active: sortColumn === column.prop && sortOrder === 'asc' }"
|
||||
>↑</text>
|
||||
<text
|
||||
class="sort-icon sort-desc"
|
||||
:class="{ active: sortColumn === column.prop && sortOrder === 'desc' }"
|
||||
>↓</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 操作列 -->
|
||||
<view v-if="$slots.actions || actionColumn" class="table-th table-action-column">
|
||||
{{ actionColumn?.label || '操作' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 表体 -->
|
||||
<view class="table-tbody">
|
||||
<!-- 数据行 -->
|
||||
<view
|
||||
v-for="(row, rowIndex) in displayData"
|
||||
:key="getRowKey(row, rowIndex)"
|
||||
class="table-tr table-body-row"
|
||||
:class="getRowClass(row, rowIndex)"
|
||||
@click="handleRowClick(row, rowIndex)"
|
||||
>
|
||||
<!-- 选择列 -->
|
||||
<view v-if="selectable" class="table-td table-selection-column">
|
||||
<checkbox
|
||||
:checked="isRowSelected(row)"
|
||||
@change="handleRowSelect(row, $event)"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 序号列 -->
|
||||
<view v-if="showIndex" class="table-td table-index-column">
|
||||
{{ getRowIndex(rowIndex) }}
|
||||
</view>
|
||||
|
||||
<!-- 数据列 -->
|
||||
<view
|
||||
v-for="column in columns"
|
||||
:key="column.prop || column.key"
|
||||
class="table-td"
|
||||
:class="getColumnClass(column)"
|
||||
:style="getColumnStyle(column)"
|
||||
>
|
||||
<view class="table-td-content">
|
||||
<!-- 自定义渲染 -->
|
||||
<slot
|
||||
v-if="column.slot"
|
||||
:name="column.slot"
|
||||
:row="row"
|
||||
:column="column"
|
||||
:index="rowIndex"
|
||||
:value="getCellValue(row, column)"
|
||||
>
|
||||
</slot>
|
||||
|
||||
<!-- 默认渲染 -->
|
||||
<template v-else>
|
||||
{{ formatCellValue(getCellValue(row, column), column) }}
|
||||
</template>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 操作列 -->
|
||||
<view v-if="$slots.actions || actionColumn" class="table-td table-action-column">
|
||||
<slot name="actions" :row="row" :index="rowIndex">
|
||||
<view class="action-buttons">
|
||||
<button
|
||||
v-for="action in actionColumn?.actions || []"
|
||||
:key="action.key"
|
||||
class="action-btn"
|
||||
:class="action.class"
|
||||
:disabled="action.disabled && action.disabled(row)"
|
||||
@click.stop="handleAction(action, row, rowIndex)"
|
||||
>
|
||||
{{ action.label }}
|
||||
</button>
|
||||
</view>
|
||||
</slot>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空数据 -->
|
||||
<view v-if="displayData.length === 0" class="table-empty">
|
||||
<slot name="empty">
|
||||
<view class="empty-content">
|
||||
<image class="empty-icon" :src="emptyIcon" mode="aspectFit"></image>
|
||||
<text class="empty-text">{{ emptyText }}</text>
|
||||
</view>
|
||||
</slot>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 分页 -->
|
||||
<view v-if="pagination && data.length > 0" class="table-pagination">
|
||||
<slot name="pagination" :pagination="paginationData">
|
||||
<view class="pagination-info">
|
||||
共 {{ total }} 条记录,第 {{ currentPage }} / {{ totalPages }} 页
|
||||
</view>
|
||||
<view class="pagination-controls">
|
||||
<button
|
||||
class="pagination-btn"
|
||||
:disabled="currentPage <= 1"
|
||||
@click="handlePageChange(currentPage - 1)"
|
||||
>
|
||||
上一页
|
||||
</button>
|
||||
<button
|
||||
class="pagination-btn"
|
||||
:disabled="currentPage >= totalPages"
|
||||
@click="handlePageChange(currentPage + 1)"
|
||||
>
|
||||
下一页
|
||||
</button>
|
||||
</view>
|
||||
</slot>
|
||||
</view>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<view v-if="loading" class="table-loading">
|
||||
<uni-loading :size="24"></uni-loading>
|
||||
<text class="loading-text">{{ loadingText }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
|
||||
export default {
|
||||
name: 'Table',
|
||||
props: {
|
||||
// 表格数据
|
||||
data: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
|
||||
// 表格列配置
|
||||
columns: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
|
||||
// 表格标题
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
|
||||
// 表格高度
|
||||
height: {
|
||||
type: [String, Number],
|
||||
default: 'auto'
|
||||
},
|
||||
|
||||
// 是否显示表头
|
||||
showHeader: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
|
||||
// 是否显示表格头部
|
||||
showTableHeader: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
|
||||
// 是否显示序号列
|
||||
showIndex: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
|
||||
// 序号列标题
|
||||
indexLabel: {
|
||||
type: String,
|
||||
default: '#'
|
||||
},
|
||||
|
||||
// 是否可选择
|
||||
selectable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
|
||||
// 选中的行
|
||||
selectedRows: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
|
||||
// 行唯一标识字段
|
||||
rowKey: {
|
||||
type: String,
|
||||
default: 'id'
|
||||
},
|
||||
|
||||
// 是否斑马纹
|
||||
stripe: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
|
||||
// 是否显示边框
|
||||
border: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
|
||||
// 表格尺寸
|
||||
size: {
|
||||
type: String,
|
||||
default: 'medium',
|
||||
validator: (value) => ['small', 'medium', 'large'].includes(value)
|
||||
},
|
||||
|
||||
// 空数据文本
|
||||
emptyText: {
|
||||
type: String,
|
||||
default: '暂无数据'
|
||||
},
|
||||
|
||||
// 空数据图标
|
||||
emptyIcon: {
|
||||
type: String,
|
||||
default: '/static/images/empty.png'
|
||||
},
|
||||
|
||||
// 加载状态
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
|
||||
// 加载文本
|
||||
loadingText: {
|
||||
type: String,
|
||||
default: '加载中...'
|
||||
},
|
||||
|
||||
// 分页配置
|
||||
pagination: {
|
||||
type: [Boolean, Object],
|
||||
default: false
|
||||
},
|
||||
|
||||
// 操作列配置
|
||||
actionColumn: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
|
||||
// 排序配置
|
||||
defaultSort: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
|
||||
emits: [
|
||||
'row-click',
|
||||
'row-select',
|
||||
'select-all',
|
||||
'sort-change',
|
||||
'page-change',
|
||||
'action-click'
|
||||
],
|
||||
|
||||
setup(props, { emit }) {
|
||||
// 响应式数据
|
||||
const selectedRowKeys = ref([...props.selectedRows])
|
||||
const sortColumn = ref(props.defaultSort.prop || '')
|
||||
const sortOrder = ref(props.defaultSort.order || '')
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(10)
|
||||
|
||||
// 计算属性
|
||||
const containerClass = computed(() => {
|
||||
const classes = []
|
||||
|
||||
classes.push(`table-${props.size}`)
|
||||
|
||||
if (props.border) {
|
||||
classes.push('table-border')
|
||||
}
|
||||
|
||||
if (props.stripe) {
|
||||
classes.push('table-stripe')
|
||||
}
|
||||
|
||||
if (props.loading) {
|
||||
classes.push('table-loading-state')
|
||||
}
|
||||
|
||||
return classes
|
||||
})
|
||||
|
||||
const tableClass = computed(() => {
|
||||
const classes = ['table-main']
|
||||
|
||||
if (props.selectable) {
|
||||
classes.push('has-selection')
|
||||
}
|
||||
|
||||
if (props.showIndex) {
|
||||
classes.push('has-index')
|
||||
}
|
||||
|
||||
return classes
|
||||
})
|
||||
|
||||
const scrollStyle = computed(() => {
|
||||
const style = {}
|
||||
|
||||
if (props.height !== 'auto') {
|
||||
style.height = typeof props.height === 'number' ? `${props.height}px` : props.height
|
||||
}
|
||||
|
||||
return style
|
||||
})
|
||||
|
||||
// 分页数据
|
||||
const paginationData = computed(() => {
|
||||
if (!props.pagination) return null
|
||||
|
||||
const config = typeof props.pagination === 'object' ? props.pagination : {}
|
||||
|
||||
return {
|
||||
current: currentPage.value,
|
||||
pageSize: pageSize.value,
|
||||
total: total.value,
|
||||
showSizeChanger: config.showSizeChanger || false,
|
||||
showQuickJumper: config.showQuickJumper || false,
|
||||
...config
|
||||
}
|
||||
})
|
||||
|
||||
const total = computed(() => props.data.length)
|
||||
const totalPages = computed(() => Math.ceil(total.value / pageSize.value))
|
||||
|
||||
// 显示的数据
|
||||
const displayData = computed(() => {
|
||||
let result = [...props.data]
|
||||
|
||||
// 排序
|
||||
if (sortColumn.value && sortOrder.value) {
|
||||
const column = props.columns.find(col => col.prop === sortColumn.value)
|
||||
if (column) {
|
||||
result.sort((a, b) => {
|
||||
const aVal = getCellValue(a, column)
|
||||
const bVal = getCellValue(b, column)
|
||||
|
||||
let comparison = 0
|
||||
if (aVal > bVal) comparison = 1
|
||||
if (aVal < bVal) comparison = -1
|
||||
|
||||
return sortOrder.value === 'asc' ? comparison : -comparison
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 分页
|
||||
if (props.pagination) {
|
||||
const start = (currentPage.value - 1) * pageSize.value
|
||||
const end = start + pageSize.value
|
||||
result = result.slice(start, end)
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
// 选择相关计算属性
|
||||
const isAllSelected = computed(() => {
|
||||
return displayData.value.length > 0 &&
|
||||
displayData.value.every(row => isRowSelected(row))
|
||||
})
|
||||
|
||||
const isIndeterminate = computed(() => {
|
||||
const selectedCount = displayData.value.filter(row => isRowSelected(row)).length
|
||||
return selectedCount > 0 && selectedCount < displayData.value.length
|
||||
})
|
||||
|
||||
// 方法
|
||||
const getRowKey = (row, index) => {
|
||||
return row[props.rowKey] || index
|
||||
}
|
||||
|
||||
const getRowIndex = (index) => {
|
||||
if (props.pagination) {
|
||||
return (currentPage.value - 1) * pageSize.value + index + 1
|
||||
}
|
||||
return index + 1
|
||||
}
|
||||
|
||||
const getRowClass = (row, index) => {
|
||||
const classes = []
|
||||
|
||||
if (isRowSelected(row)) {
|
||||
classes.push('selected')
|
||||
}
|
||||
|
||||
if (props.stripe && index % 2 === 1) {
|
||||
classes.push('stripe')
|
||||
}
|
||||
|
||||
return classes
|
||||
}
|
||||
|
||||
const getColumnClass = (column) => {
|
||||
const classes = []
|
||||
|
||||
if (column.align) {
|
||||
classes.push(`text-${column.align}`)
|
||||
}
|
||||
|
||||
if (column.className) {
|
||||
classes.push(column.className)
|
||||
}
|
||||
|
||||
if (column.sortable) {
|
||||
classes.push('sortable')
|
||||
}
|
||||
|
||||
return classes
|
||||
}
|
||||
|
||||
const getColumnStyle = (column) => {
|
||||
const style = {}
|
||||
|
||||
if (column.width) {
|
||||
style.width = typeof column.width === 'number' ? `${column.width}px` : column.width
|
||||
style.minWidth = style.width
|
||||
}
|
||||
|
||||
if (column.minWidth) {
|
||||
style.minWidth = typeof column.minWidth === 'number' ? `${column.minWidth}px` : column.minWidth
|
||||
}
|
||||
|
||||
return style
|
||||
}
|
||||
|
||||
const getCellValue = (row, column) => {
|
||||
if (column.prop) {
|
||||
return row[column.prop]
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
const formatCellValue = (value, column) => {
|
||||
if (column.formatter && typeof column.formatter === 'function') {
|
||||
return column.formatter(value, column)
|
||||
}
|
||||
|
||||
if (value === null || value === undefined) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return String(value)
|
||||
}
|
||||
|
||||
const isRowSelected = (row) => {
|
||||
const key = getRowKey(row)
|
||||
return selectedRowKeys.value.includes(key)
|
||||
}
|
||||
|
||||
// 事件处理
|
||||
const handleRowClick = (row, index) => {
|
||||
emit('row-click', row, index)
|
||||
}
|
||||
|
||||
const handleRowSelect = (row, event) => {
|
||||
const key = getRowKey(row)
|
||||
const checked = event.detail.value.length > 0
|
||||
|
||||
if (checked) {
|
||||
if (!selectedRowKeys.value.includes(key)) {
|
||||
selectedRowKeys.value.push(key)
|
||||
}
|
||||
} else {
|
||||
const index = selectedRowKeys.value.indexOf(key)
|
||||
if (index > -1) {
|
||||
selectedRowKeys.value.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
emit('row-select', row, checked, selectedRowKeys.value)
|
||||
}
|
||||
|
||||
const handleSelectAll = (event) => {
|
||||
const checked = event.detail.value.length > 0
|
||||
|
||||
if (checked) {
|
||||
displayData.value.forEach(row => {
|
||||
const key = getRowKey(row)
|
||||
if (!selectedRowKeys.value.includes(key)) {
|
||||
selectedRowKeys.value.push(key)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
displayData.value.forEach(row => {
|
||||
const key = getRowKey(row)
|
||||
const index = selectedRowKeys.value.indexOf(key)
|
||||
if (index > -1) {
|
||||
selectedRowKeys.value.splice(index, 1)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
emit('select-all', checked, selectedRowKeys.value)
|
||||
}
|
||||
|
||||
const handleSort = (column) => {
|
||||
if (!column.sortable) return
|
||||
|
||||
if (sortColumn.value === column.prop) {
|
||||
// 切换排序方向
|
||||
if (sortOrder.value === 'asc') {
|
||||
sortOrder.value = 'desc'
|
||||
} else if (sortOrder.value === 'desc') {
|
||||
sortOrder.value = ''
|
||||
sortColumn.value = ''
|
||||
} else {
|
||||
sortOrder.value = 'asc'
|
||||
}
|
||||
} else {
|
||||
// 新的排序列
|
||||
sortColumn.value = column.prop
|
||||
sortOrder.value = 'asc'
|
||||
}
|
||||
|
||||
emit('sort-change', {
|
||||
column: sortColumn.value,
|
||||
order: sortOrder.value,
|
||||
prop: sortColumn.value
|
||||
})
|
||||
}
|
||||
|
||||
const handlePageChange = (page) => {
|
||||
if (page < 1 || page > totalPages.value) return
|
||||
|
||||
currentPage.value = page
|
||||
emit('page-change', {
|
||||
current: page,
|
||||
pageSize: pageSize.value,
|
||||
total: total.value
|
||||
})
|
||||
}
|
||||
|
||||
const handleAction = (action, row, index) => {
|
||||
if (action.handler && typeof action.handler === 'function') {
|
||||
action.handler(row, index)
|
||||
}
|
||||
|
||||
emit('action-click', action, row, index)
|
||||
}
|
||||
|
||||
const handleScroll = (event) => {
|
||||
// 处理滚动事件
|
||||
}
|
||||
|
||||
// 监听选中行变化
|
||||
watch(() => props.selectedRows, (newVal) => {
|
||||
selectedRowKeys.value = [...newVal]
|
||||
})
|
||||
|
||||
return {
|
||||
selectedRowKeys,
|
||||
sortColumn,
|
||||
sortOrder,
|
||||
currentPage,
|
||||
pageSize,
|
||||
containerClass,
|
||||
tableClass,
|
||||
scrollStyle,
|
||||
paginationData,
|
||||
total,
|
||||
totalPages,
|
||||
displayData,
|
||||
isAllSelected,
|
||||
isIndeterminate,
|
||||
getRowKey,
|
||||
getRowIndex,
|
||||
getRowClass,
|
||||
getColumnClass,
|
||||
getColumnStyle,
|
||||
getCellValue,
|
||||
formatCellValue,
|
||||
isRowSelected,
|
||||
handleRowClick,
|
||||
handleRowSelect,
|
||||
handleSelectAll,
|
||||
handleSort,
|
||||
handlePageChange,
|
||||
handleAction,
|
||||
handleScroll
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.table-container {
|
||||
position: relative;
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
|
||||
&.table-border {
|
||||
border: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
&.table-loading-state {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.table-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
|
||||
.table-title {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
|
||||
.table-scroll {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.table {
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
border-collapse: collapse;
|
||||
|
||||
.table-tr {
|
||||
display: flex;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.table-th,
|
||||
.table-td {
|
||||
flex: 1;
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
border-right: 1px solid #ebeef5;
|
||||
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
&.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.table-th {
|
||||
background: #fafafa;
|
||||
font-weight: 500;
|
||||
color: #909399;
|
||||
|
||||
&.sortable {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
}
|
||||
|
||||
.table-th-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.sort-icons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-left: 4px;
|
||||
|
||||
.sort-icon {
|
||||
font-size: 12px;
|
||||
color: #c0c4cc;
|
||||
line-height: 1;
|
||||
|
||||
&.active {
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
&.sort-asc {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table-td {
|
||||
color: #606266;
|
||||
|
||||
.table-td-content {
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
|
||||
.table-selection-column,
|
||||
.table-index-column {
|
||||
flex: 0 0 60px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.table-action-column {
|
||||
flex: 0 0 120px;
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
||||
.action-btn {
|
||||
padding: 4px 8px;
|
||||
font-size: 12px;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 3px;
|
||||
background: #fff;
|
||||
color: #606266;
|
||||
|
||||
&:hover {
|
||||
color: #409eff;
|
||||
border-color: #c6e2ff;
|
||||
background: #ecf5ff;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
color: #c0c4cc;
|
||||
cursor: not-allowed;
|
||||
background: #fff;
|
||||
border-color: #ebeef5;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table-body-row {
|
||||
&:hover {
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background: #ecf5ff;
|
||||
}
|
||||
|
||||
&.stripe {
|
||||
background: #fafafa;
|
||||
|
||||
&:hover {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background: #ecf5ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table-empty {
|
||||
padding: 40px 0;
|
||||
text-align: center;
|
||||
|
||||
.empty-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
.empty-icon {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table-pagination {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
border-top: 1px solid #ebeef5;
|
||||
|
||||
.pagination-info {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.pagination-controls {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
||||
.pagination-btn {
|
||||
padding: 6px 12px;
|
||||
font-size: 14px;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 3px;
|
||||
background: #fff;
|
||||
color: #606266;
|
||||
|
||||
&:hover {
|
||||
color: #409eff;
|
||||
border-color: #c6e2ff;
|
||||
background: #ecf5ff;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
color: #c0c4cc;
|
||||
cursor: not-allowed;
|
||||
background: #fff;
|
||||
border-color: #ebeef5;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table-loading {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
padding: 20px;
|
||||
border-radius: 4px;
|
||||
|
||||
.loading-text {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
// 尺寸变体
|
||||
.table-small {
|
||||
.table-th,
|
||||
.table-td {
|
||||
padding: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.table-large {
|
||||
.table-th,
|
||||
.table-td {
|
||||
padding: 16px;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
369
mini_program/common/components/tabs/tabs.vue
Normal file
369
mini_program/common/components/tabs/tabs.vue
Normal file
@@ -0,0 +1,369 @@
|
||||
<template>
|
||||
<view class="tabs-container" :class="containerClass">
|
||||
<!-- 标签栏 -->
|
||||
<scroll-view
|
||||
class="tabs-scroll"
|
||||
:class="tabsClass"
|
||||
scroll-x
|
||||
:scroll-left="scrollLeft"
|
||||
:show-scrollbar="false"
|
||||
>
|
||||
<view class="tabs-wrapper">
|
||||
<view
|
||||
class="tab-item"
|
||||
:class="getTabClass(index)"
|
||||
v-for="(tab, index) in tabs"
|
||||
:key="index"
|
||||
@click="handleTabClick(index)"
|
||||
>
|
||||
<!-- 图标 -->
|
||||
<view class="tab-icon" v-if="tab.icon">
|
||||
<text class="icon">{{ tab.icon }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 标题 -->
|
||||
<view class="tab-title">
|
||||
<text class="title-text">{{ tab.title || tab.name || tab.label }}</text>
|
||||
<!-- 徽标 -->
|
||||
<view class="tab-badge" v-if="tab.badge">
|
||||
<text class="badge-text">{{ tab.badge }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 下划线 -->
|
||||
<view class="tab-line" v-if="showLine && index === activeIndex"></view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<view class="tabs-content" v-if="showContent">
|
||||
<slot></slot>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Tabs',
|
||||
props: {
|
||||
// 标签数据
|
||||
tabs: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
required: true
|
||||
},
|
||||
// 当前激活的标签索引
|
||||
activeIndex: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
// 标签类型
|
||||
type: {
|
||||
type: String,
|
||||
default: 'line', // line, card, button
|
||||
validator: (value) => ['line', 'card', 'button'].includes(value)
|
||||
},
|
||||
// 标签大小
|
||||
size: {
|
||||
type: String,
|
||||
default: 'normal', // large, normal, small
|
||||
validator: (value) => ['large', 'normal', 'small'].includes(value)
|
||||
},
|
||||
// 是否显示下划线
|
||||
showLine: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 是否显示内容区域
|
||||
showContent: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 是否可滚动
|
||||
scrollable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 标签宽度(固定宽度时使用)
|
||||
tabWidth: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 激活颜色
|
||||
activeColor: {
|
||||
type: String,
|
||||
default: '#2e8b57'
|
||||
},
|
||||
// 非激活颜色
|
||||
inactiveColor: {
|
||||
type: String,
|
||||
default: '#666'
|
||||
},
|
||||
// 背景色
|
||||
backgroundColor: {
|
||||
type: String,
|
||||
default: '#fff'
|
||||
},
|
||||
// 自定义样式类
|
||||
customClass: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
scrollLeft: 0
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
containerClass() {
|
||||
return [
|
||||
this.customClass,
|
||||
`tabs-${this.type}`,
|
||||
`tabs-${this.size}`
|
||||
]
|
||||
},
|
||||
tabsClass() {
|
||||
return {
|
||||
'tabs-scrollable': this.scrollable,
|
||||
'tabs-fixed': !this.scrollable
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
activeIndex: {
|
||||
handler(newIndex) {
|
||||
this.scrollToActiveTab(newIndex)
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 获取标签样式类
|
||||
getTabClass(index) {
|
||||
return {
|
||||
'tab-active': index === this.activeIndex,
|
||||
'tab-inactive': index !== this.activeIndex
|
||||
}
|
||||
},
|
||||
// 标签点击事件
|
||||
handleTabClick(index) {
|
||||
if (index === this.activeIndex) return
|
||||
this.$emit('change', index)
|
||||
this.$emit('click', { index, tab: this.tabs[index] })
|
||||
},
|
||||
// 滚动到激活的标签
|
||||
scrollToActiveTab(index) {
|
||||
if (!this.scrollable) return
|
||||
|
||||
this.$nextTick(() => {
|
||||
const query = uni.createSelectorQuery().in(this)
|
||||
query.selectAll('.tab-item').boundingClientRect((rects) => {
|
||||
if (rects && rects[index]) {
|
||||
const tabRect = rects[index]
|
||||
const containerWidth = uni.getSystemInfoSync().windowWidth
|
||||
const scrollLeft = tabRect.left - containerWidth / 2 + tabRect.width / 2
|
||||
this.scrollLeft = Math.max(0, scrollLeft)
|
||||
}
|
||||
}).exec()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tabs-container {
|
||||
width: 100%;
|
||||
background-color: #fff;
|
||||
|
||||
// 类型样式
|
||||
&.tabs-line {
|
||||
.tab-item {
|
||||
border-bottom: 4rpx solid transparent;
|
||||
|
||||
&.tab-active {
|
||||
border-bottom-color: #2e8b57;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.tabs-card {
|
||||
.tabs-wrapper {
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 12rpx;
|
||||
padding: 8rpx;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
background-color: transparent;
|
||||
border-radius: 8rpx;
|
||||
|
||||
&.tab-active {
|
||||
background-color: #fff;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.tabs-button {
|
||||
.tab-item {
|
||||
border: 2rpx solid #e5e5e5;
|
||||
border-radius: 8rpx;
|
||||
margin-right: 16rpx;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
&.tab-active {
|
||||
border-color: #2e8b57;
|
||||
background-color: #2e8b57;
|
||||
|
||||
.title-text {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 大小样式
|
||||
&.tabs-large {
|
||||
.tab-item {
|
||||
height: 96rpx;
|
||||
padding: 0 32rpx;
|
||||
}
|
||||
|
||||
.title-text {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
}
|
||||
|
||||
&.tabs-normal {
|
||||
.tab-item {
|
||||
height: 80rpx;
|
||||
padding: 0 24rpx;
|
||||
}
|
||||
|
||||
.title-text {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
}
|
||||
|
||||
&.tabs-small {
|
||||
.tab-item {
|
||||
height: 64rpx;
|
||||
padding: 0 20rpx;
|
||||
}
|
||||
|
||||
.title-text {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tabs-scroll {
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
|
||||
&.tabs-scrollable {
|
||||
.tabs-wrapper {
|
||||
display: inline-flex;
|
||||
min-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&.tabs-fixed {
|
||||
.tabs-wrapper {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tabs-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
transition: all 0.3s ease;
|
||||
white-space: nowrap;
|
||||
|
||||
&.tab-active {
|
||||
.title-text {
|
||||
color: #2e8b57;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
&.tab-inactive {
|
||||
.title-text {
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tab-icon {
|
||||
margin-right: 8rpx;
|
||||
|
||||
.icon {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.tab-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.title-text {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.tab-badge {
|
||||
position: absolute;
|
||||
top: -16rpx;
|
||||
right: -24rpx;
|
||||
min-width: 32rpx;
|
||||
height: 32rpx;
|
||||
background-color: #ff4757;
|
||||
border-radius: 16rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.badge-text {
|
||||
font-size: 20rpx;
|
||||
color: #fff;
|
||||
line-height: 1;
|
||||
padding: 0 8rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.tab-line {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 60rpx;
|
||||
height: 4rpx;
|
||||
background-color: #2e8b57;
|
||||
border-radius: 2rpx;
|
||||
}
|
||||
|
||||
.tabs-content {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
878
mini_program/common/components/upload/upload.vue
Normal file
878
mini_program/common/components/upload/upload.vue
Normal file
@@ -0,0 +1,878 @@
|
||||
<template>
|
||||
<view class="upload-container" :class="containerClass">
|
||||
<!-- 上传区域 -->
|
||||
<view
|
||||
class="upload-area"
|
||||
:class="uploadAreaClass"
|
||||
@click="handleClick"
|
||||
>
|
||||
<!-- 拖拽上传 -->
|
||||
<view v-if="drag" class="upload-drag">
|
||||
<view class="drag-content">
|
||||
<image class="drag-icon" :src="dragIcon" mode="aspectFit"></image>
|
||||
<text class="drag-text">{{ dragText }}</text>
|
||||
<text class="drag-hint">{{ dragHint }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 按钮上传 -->
|
||||
<view v-else class="upload-button">
|
||||
<button class="upload-btn" :class="buttonClass" :disabled="disabled">
|
||||
<text class="btn-text">{{ buttonText }}</text>
|
||||
</button>
|
||||
<text v-if="tip" class="upload-tip">{{ tip }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 文件列表 -->
|
||||
<view v-if="showFileList && fileList.length > 0" class="file-list">
|
||||
<view
|
||||
v-for="(file, index) in fileList"
|
||||
:key="file.uid || index"
|
||||
class="file-item"
|
||||
:class="getFileItemClass(file)"
|
||||
>
|
||||
<!-- 文件图标 -->
|
||||
<view class="file-icon">
|
||||
<image
|
||||
v-if="isImage(file)"
|
||||
class="file-preview"
|
||||
:src="file.url || file.path"
|
||||
mode="aspectFill"
|
||||
@click="handlePreview(file, index)"
|
||||
></image>
|
||||
<image
|
||||
v-else
|
||||
class="file-type-icon"
|
||||
:src="getFileTypeIcon(file)"
|
||||
mode="aspectFit"
|
||||
></image>
|
||||
</view>
|
||||
|
||||
<!-- 文件信息 -->
|
||||
<view class="file-info">
|
||||
<text class="file-name">{{ file.name }}</text>
|
||||
<text v-if="file.size" class="file-size">{{ formatFileSize(file.size) }}</text>
|
||||
|
||||
<!-- 上传进度 -->
|
||||
<view v-if="file.status === 'uploading'" class="upload-progress">
|
||||
<view class="progress-bar">
|
||||
<view
|
||||
class="progress-fill"
|
||||
:style="{ width: `${file.percent || 0}%` }"
|
||||
></view>
|
||||
</view>
|
||||
<text class="progress-text">{{ file.percent || 0 }}%</text>
|
||||
</view>
|
||||
|
||||
<!-- 上传状态 -->
|
||||
<view v-if="file.status === 'error'" class="upload-error">
|
||||
<text class="error-text">{{ file.error || '上传失败' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<view class="file-actions">
|
||||
<button
|
||||
v-if="file.status === 'success' && !disabled"
|
||||
class="action-btn preview-btn"
|
||||
@click="handlePreview(file, index)"
|
||||
>
|
||||
预览
|
||||
</button>
|
||||
<button
|
||||
v-if="!disabled"
|
||||
class="action-btn remove-btn"
|
||||
@click="handleRemove(file, index)"
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
<button
|
||||
v-if="file.status === 'error'"
|
||||
class="action-btn retry-btn"
|
||||
@click="handleRetry(file, index)"
|
||||
>
|
||||
重试
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 上传限制提示 -->
|
||||
<view v-if="showLimit" class="upload-limit">
|
||||
<text class="limit-text">
|
||||
已上传 {{ fileList.length }} / {{ limit }} 个文件
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { commonApi } from '@/common/api'
|
||||
|
||||
export default {
|
||||
name: 'Upload',
|
||||
props: {
|
||||
// 上传地址
|
||||
action: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
|
||||
// 文件列表
|
||||
fileList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
|
||||
// 是否支持多选
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
|
||||
// 文件数量限制
|
||||
limit: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
|
||||
// 文件大小限制(MB)
|
||||
maxSize: {
|
||||
type: Number,
|
||||
default: 10
|
||||
},
|
||||
|
||||
// 允许的文件类型
|
||||
accept: {
|
||||
type: String,
|
||||
default: '*'
|
||||
},
|
||||
|
||||
// 是否禁用
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
|
||||
// 是否显示文件列表
|
||||
showFileList: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
|
||||
// 是否支持拖拽上传
|
||||
drag: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
|
||||
// 按钮文本
|
||||
buttonText: {
|
||||
type: String,
|
||||
default: '选择文件'
|
||||
},
|
||||
|
||||
// 拖拽文本
|
||||
dragText: {
|
||||
type: String,
|
||||
default: '将文件拖到此处'
|
||||
},
|
||||
|
||||
// 拖拽提示
|
||||
dragHint: {
|
||||
type: String,
|
||||
default: '或点击选择文件'
|
||||
},
|
||||
|
||||
// 拖拽图标
|
||||
dragIcon: {
|
||||
type: String,
|
||||
default: '/static/images/upload.png'
|
||||
},
|
||||
|
||||
// 提示文本
|
||||
tip: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
|
||||
// 上传前钩子
|
||||
beforeUpload: {
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
|
||||
// 上传成功钩子
|
||||
onSuccess: {
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
|
||||
// 上传失败钩子
|
||||
onError: {
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
|
||||
// 上传进度钩子
|
||||
onProgress: {
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
|
||||
// 文件移除钩子
|
||||
onRemove: {
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
|
||||
// 文件预览钩子
|
||||
onPreview: {
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
|
||||
// 自定义上传
|
||||
customRequest: {
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
|
||||
// 额外的上传参数
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
|
||||
// 请求头
|
||||
headers: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
|
||||
// 上传的文件字段名
|
||||
name: {
|
||||
type: String,
|
||||
default: 'file'
|
||||
},
|
||||
|
||||
// 是否自动上传
|
||||
autoUpload: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
|
||||
emits: [
|
||||
'change',
|
||||
'success',
|
||||
'error',
|
||||
'progress',
|
||||
'remove',
|
||||
'preview',
|
||||
'exceed'
|
||||
],
|
||||
|
||||
setup(props, { emit }) {
|
||||
// 响应式数据
|
||||
const uploading = ref(false)
|
||||
const dragOver = ref(false)
|
||||
|
||||
// 计算属性
|
||||
const containerClass = computed(() => {
|
||||
const classes = []
|
||||
|
||||
if (props.disabled) {
|
||||
classes.push('upload-disabled')
|
||||
}
|
||||
|
||||
if (uploading.value) {
|
||||
classes.push('upload-loading')
|
||||
}
|
||||
|
||||
return classes
|
||||
})
|
||||
|
||||
const uploadAreaClass = computed(() => {
|
||||
const classes = []
|
||||
|
||||
if (props.drag) {
|
||||
classes.push('upload-drag-area')
|
||||
|
||||
if (dragOver.value) {
|
||||
classes.push('drag-over')
|
||||
}
|
||||
}
|
||||
|
||||
if (props.disabled) {
|
||||
classes.push('disabled')
|
||||
}
|
||||
|
||||
return classes
|
||||
})
|
||||
|
||||
const buttonClass = computed(() => {
|
||||
const classes = ['upload-button-main']
|
||||
|
||||
if (props.disabled) {
|
||||
classes.push('disabled')
|
||||
}
|
||||
|
||||
return classes
|
||||
})
|
||||
|
||||
const showLimit = computed(() => {
|
||||
return props.limit > 0 && props.showFileList
|
||||
})
|
||||
|
||||
// 方法
|
||||
const handleClick = () => {
|
||||
if (props.disabled) return
|
||||
|
||||
// 检查文件数量限制
|
||||
if (props.limit > 0 && props.fileList.length >= props.limit) {
|
||||
emit('exceed', props.fileList, props.limit)
|
||||
return
|
||||
}
|
||||
|
||||
selectFiles()
|
||||
}
|
||||
|
||||
const selectFiles = () => {
|
||||
uni.chooseFile({
|
||||
count: props.multiple ? (props.limit > 0 ? props.limit - props.fileList.length : 9) : 1,
|
||||
type: getFileType(),
|
||||
success: (res) => {
|
||||
handleFiles(res.tempFiles)
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('选择文件失败:', err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const getFileType = () => {
|
||||
if (props.accept === '*') return 'all'
|
||||
if (props.accept.includes('image')) return 'image'
|
||||
if (props.accept.includes('video')) return 'video'
|
||||
return 'all'
|
||||
}
|
||||
|
||||
const handleFiles = async (files) => {
|
||||
const fileList = Array.from(files).map(file => ({
|
||||
uid: Date.now() + Math.random(),
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
path: file.path,
|
||||
file: file,
|
||||
status: 'ready',
|
||||
percent: 0
|
||||
}))
|
||||
|
||||
// 文件验证
|
||||
const validFiles = []
|
||||
for (const file of fileList) {
|
||||
if (await validateFile(file)) {
|
||||
validFiles.push(file)
|
||||
}
|
||||
}
|
||||
|
||||
if (validFiles.length === 0) return
|
||||
|
||||
// 添加到文件列表
|
||||
const newFileList = [...props.fileList, ...validFiles]
|
||||
emit('change', newFileList)
|
||||
|
||||
// 自动上传
|
||||
if (props.autoUpload) {
|
||||
validFiles.forEach(file => {
|
||||
uploadFile(file)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const validateFile = async (file) => {
|
||||
// 文件大小验证
|
||||
if (props.maxSize > 0 && file.size > props.maxSize * 1024 * 1024) {
|
||||
uni.showToast({
|
||||
title: `文件大小不能超过 ${props.maxSize}MB`,
|
||||
icon: 'none'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
// 文件类型验证
|
||||
if (props.accept !== '*' && !isAcceptedFileType(file)) {
|
||||
uni.showToast({
|
||||
title: '文件类型不支持',
|
||||
icon: 'none'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
// 自定义验证
|
||||
if (props.beforeUpload && typeof props.beforeUpload === 'function') {
|
||||
try {
|
||||
const result = await props.beforeUpload(file)
|
||||
if (result === false) {
|
||||
return false
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('文件验证失败:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const isAcceptedFileType = (file) => {
|
||||
if (props.accept === '*') return true
|
||||
|
||||
const acceptTypes = props.accept.split(',').map(type => type.trim())
|
||||
const fileName = file.name.toLowerCase()
|
||||
|
||||
return acceptTypes.some(type => {
|
||||
if (type.startsWith('.')) {
|
||||
return fileName.endsWith(type)
|
||||
}
|
||||
if (type.includes('/')) {
|
||||
// MIME type
|
||||
return file.type && file.type.includes(type.split('/')[0])
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
const uploadFile = async (file) => {
|
||||
const fileIndex = props.fileList.findIndex(f => f.uid === file.uid)
|
||||
if (fileIndex === -1) return
|
||||
|
||||
// 更新状态
|
||||
updateFileStatus(file.uid, 'uploading', 0)
|
||||
uploading.value = true
|
||||
|
||||
try {
|
||||
let result
|
||||
|
||||
if (props.customRequest && typeof props.customRequest === 'function') {
|
||||
// 自定义上传
|
||||
result = await props.customRequest({
|
||||
file: file.file,
|
||||
data: props.data,
|
||||
headers: props.headers,
|
||||
onProgress: (percent) => {
|
||||
updateFileStatus(file.uid, 'uploading', percent)
|
||||
if (props.onProgress) {
|
||||
props.onProgress(percent, file)
|
||||
}
|
||||
emit('progress', percent, file)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// 默认上传
|
||||
result = await commonApi.uploadFile(file.file, {
|
||||
...props.data,
|
||||
name: props.name
|
||||
})
|
||||
}
|
||||
|
||||
// 上传成功
|
||||
updateFileStatus(file.uid, 'success', 100, result.data.url)
|
||||
|
||||
if (props.onSuccess) {
|
||||
props.onSuccess(result, file)
|
||||
}
|
||||
emit('success', result, file)
|
||||
|
||||
} catch (error) {
|
||||
// 上传失败
|
||||
updateFileStatus(file.uid, 'error', 0, null, error.message)
|
||||
|
||||
if (props.onError) {
|
||||
props.onError(error, file)
|
||||
}
|
||||
emit('error', error, file)
|
||||
|
||||
} finally {
|
||||
uploading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const updateFileStatus = (uid, status, percent = 0, url = null, error = null) => {
|
||||
const fileIndex = props.fileList.findIndex(f => f.uid === uid)
|
||||
if (fileIndex > -1) {
|
||||
const updatedFile = {
|
||||
...props.fileList[fileIndex],
|
||||
status,
|
||||
percent,
|
||||
url: url || props.fileList[fileIndex].url,
|
||||
error
|
||||
}
|
||||
|
||||
const newFileList = [...props.fileList]
|
||||
newFileList[fileIndex] = updatedFile
|
||||
|
||||
emit('change', newFileList)
|
||||
}
|
||||
}
|
||||
|
||||
const handleRemove = (file, index) => {
|
||||
if (props.onRemove && typeof props.onRemove === 'function') {
|
||||
const result = props.onRemove(file, index)
|
||||
if (result === false) return
|
||||
}
|
||||
|
||||
const newFileList = props.fileList.filter(f => f.uid !== file.uid)
|
||||
emit('change', newFileList)
|
||||
emit('remove', file, index)
|
||||
}
|
||||
|
||||
const handlePreview = (file, index) => {
|
||||
if (props.onPreview && typeof props.onPreview === 'function') {
|
||||
props.onPreview(file, index)
|
||||
} else {
|
||||
// 默认预览行为
|
||||
if (isImage(file)) {
|
||||
uni.previewImage({
|
||||
urls: [file.url || file.path],
|
||||
current: 0
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
emit('preview', file, index)
|
||||
}
|
||||
|
||||
const handleRetry = (file, index) => {
|
||||
uploadFile(file)
|
||||
}
|
||||
|
||||
const getFileItemClass = (file) => {
|
||||
const classes = []
|
||||
|
||||
classes.push(`file-${file.status}`)
|
||||
|
||||
if (isImage(file)) {
|
||||
classes.push('file-image')
|
||||
}
|
||||
|
||||
return classes
|
||||
}
|
||||
|
||||
const isImage = (file) => {
|
||||
const imageTypes = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp']
|
||||
const ext = getFileExtension(file.name)
|
||||
return imageTypes.includes(ext.toLowerCase())
|
||||
}
|
||||
|
||||
const getFileExtension = (filename) => {
|
||||
return filename.split('.').pop() || ''
|
||||
}
|
||||
|
||||
const getFileTypeIcon = (file) => {
|
||||
const ext = getFileExtension(file.name).toLowerCase()
|
||||
|
||||
const iconMap = {
|
||||
pdf: '/static/images/file-pdf.png',
|
||||
doc: '/static/images/file-doc.png',
|
||||
docx: '/static/images/file-doc.png',
|
||||
xls: '/static/images/file-excel.png',
|
||||
xlsx: '/static/images/file-excel.png',
|
||||
ppt: '/static/images/file-ppt.png',
|
||||
pptx: '/static/images/file-ppt.png',
|
||||
txt: '/static/images/file-txt.png',
|
||||
zip: '/static/images/file-zip.png',
|
||||
rar: '/static/images/file-zip.png'
|
||||
}
|
||||
|
||||
return iconMap[ext] || '/static/images/file-default.png'
|
||||
}
|
||||
|
||||
const formatFileSize = (size) => {
|
||||
if (size < 1024) {
|
||||
return `${size} B`
|
||||
} else if (size < 1024 * 1024) {
|
||||
return `${(size / 1024).toFixed(1)} KB`
|
||||
} else if (size < 1024 * 1024 * 1024) {
|
||||
return `${(size / (1024 * 1024)).toFixed(1)} MB`
|
||||
} else {
|
||||
return `${(size / (1024 * 1024 * 1024)).toFixed(1)} GB`
|
||||
}
|
||||
}
|
||||
|
||||
// 公开方法
|
||||
const submit = () => {
|
||||
const readyFiles = props.fileList.filter(file => file.status === 'ready')
|
||||
readyFiles.forEach(file => {
|
||||
uploadFile(file)
|
||||
})
|
||||
}
|
||||
|
||||
const abort = (file) => {
|
||||
// 取消上传(uni-app 暂不支持)
|
||||
console.warn('uni-app 暂不支持取消上传')
|
||||
}
|
||||
|
||||
return {
|
||||
uploading,
|
||||
dragOver,
|
||||
containerClass,
|
||||
uploadAreaClass,
|
||||
buttonClass,
|
||||
showLimit,
|
||||
handleClick,
|
||||
handleRemove,
|
||||
handlePreview,
|
||||
handleRetry,
|
||||
getFileItemClass,
|
||||
isImage,
|
||||
getFileTypeIcon,
|
||||
formatFileSize,
|
||||
submit,
|
||||
abort
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.upload-container {
|
||||
&.upload-disabled {
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&.upload-loading {
|
||||
.upload-area {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.upload-area {
|
||||
&.upload-drag-area {
|
||||
border: 2px dashed #d9d9d9;
|
||||
border-radius: 6px;
|
||||
background: #fafafa;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.3s;
|
||||
|
||||
&:hover {
|
||||
border-color: #409eff;
|
||||
}
|
||||
|
||||
&.drag-over {
|
||||
border-color: #409eff;
|
||||
background: #f0f9ff;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
cursor: not-allowed;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.upload-drag {
|
||||
padding: 40px 20px;
|
||||
|
||||
.drag-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
.drag-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.drag-text {
|
||||
font-size: 16px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.drag-hint {
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.upload-button {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
|
||||
.upload-btn {
|
||||
padding: 8px 16px;
|
||||
font-size: 14px;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
color: #606266;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: #409eff;
|
||||
border-color: #c6e2ff;
|
||||
background: #ecf5ff;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
color: #c0c4cc;
|
||||
cursor: not-allowed;
|
||||
background: #fff;
|
||||
border-color: #ebeef5;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-tip {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
|
||||
.file-list {
|
||||
margin-top: 16px;
|
||||
|
||||
.file-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&.file-error {
|
||||
.file-name {
|
||||
color: #f56c6c;
|
||||
}
|
||||
}
|
||||
|
||||
&.file-success {
|
||||
.file-name {
|
||||
color: #67c23a;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
flex-shrink: 0;
|
||||
margin-right: 12px;
|
||||
|
||||
.file-preview {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.file-type-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.file-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.file-name {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
margin-bottom: 4px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.file-size {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.upload-progress {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 4px;
|
||||
|
||||
.progress-bar {
|
||||
flex: 1;
|
||||
height: 4px;
|
||||
background: #f0f0f0;
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: #409eff;
|
||||
transition: width 0.3s;
|
||||
}
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
font-size: 12px;
|
||||
color: #409eff;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-error {
|
||||
margin-top: 4px;
|
||||
|
||||
.error-text {
|
||||
font-size: 12px;
|
||||
color: #f56c6c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.file-actions {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
||||
.action-btn {
|
||||
padding: 4px 8px;
|
||||
font-size: 12px;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
|
||||
&.preview-btn {
|
||||
background: #ecf5ff;
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
&.remove-btn {
|
||||
background: #fef0f0;
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
&.retry-btn {
|
||||
background: #fdf6ec;
|
||||
color: #e6a23c;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.upload-limit {
|
||||
margin-top: 8px;
|
||||
text-align: right;
|
||||
|
||||
.limit-text {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
157
mini_program/common/config/api.js
Normal file
157
mini_program/common/config/api.js
Normal file
@@ -0,0 +1,157 @@
|
||||
// API接口配置
|
||||
const API_CONFIG = {
|
||||
// 基础配置
|
||||
BASE_URL: {
|
||||
development: 'https://dev-api.xlxumu.com',
|
||||
testing: 'https://test-api.xlxumu.com',
|
||||
production: 'https://api.xlxumu.com'
|
||||
},
|
||||
|
||||
// 超时时间
|
||||
TIMEOUT: 10000,
|
||||
|
||||
// 请求头
|
||||
HEADERS: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
}
|
||||
|
||||
// API接口地址
|
||||
const API_ENDPOINTS = {
|
||||
// 认证相关
|
||||
AUTH: {
|
||||
LOGIN: '/auth/login',
|
||||
LOGOUT: '/auth/logout',
|
||||
REGISTER: '/auth/register',
|
||||
REFRESH_TOKEN: '/auth/refresh',
|
||||
VALIDATE_TOKEN: '/auth/validate',
|
||||
RESET_PASSWORD: '/auth/reset-password',
|
||||
CHANGE_PASSWORD: '/auth/change-password'
|
||||
},
|
||||
|
||||
// 用户相关
|
||||
USER: {
|
||||
PROFILE: '/user/profile',
|
||||
UPDATE_PROFILE: '/user/profile',
|
||||
AVATAR_UPLOAD: '/user/avatar',
|
||||
SETTINGS: '/user/settings'
|
||||
},
|
||||
|
||||
// 养殖管理
|
||||
FARMING: {
|
||||
STATS: '/farming/stats',
|
||||
ACTIVITIES: '/farming/activities',
|
||||
FARMS: '/farming/farms',
|
||||
FARM_DETAIL: '/farming/farms/:id',
|
||||
ANIMALS: '/farming/animals',
|
||||
ANIMAL_DETAIL: '/farming/animals/:id',
|
||||
HEALTH_RECORDS: '/farming/health-records',
|
||||
FEED_RECORDS: '/farming/feed-records',
|
||||
PRODUCTION_RECORDS: '/farming/production-records'
|
||||
},
|
||||
|
||||
// 牛只交易
|
||||
TRADING: {
|
||||
PRODUCTS: '/trading/products',
|
||||
PRODUCT_DETAIL: '/trading/products/:id',
|
||||
HOT_PRODUCTS: '/trading/products/hot',
|
||||
LATEST_PRODUCTS: '/trading/products/latest',
|
||||
CATEGORIES: '/trading/categories',
|
||||
SEARCH: '/trading/search',
|
||||
PUBLISH: '/trading/publish',
|
||||
ORDERS: '/trading/orders',
|
||||
ORDER_DETAIL: '/trading/orders/:id'
|
||||
},
|
||||
|
||||
// 牛肉商城
|
||||
MALL: {
|
||||
PRODUCTS: '/mall/products',
|
||||
PRODUCT_DETAIL: '/mall/products/:id',
|
||||
CATEGORIES: '/mall/categories',
|
||||
CART: '/mall/cart',
|
||||
ORDERS: '/mall/orders',
|
||||
ORDER_DETAIL: '/mall/orders/:id',
|
||||
ADDRESSES: '/mall/addresses',
|
||||
PAYMENT: '/mall/payment'
|
||||
},
|
||||
|
||||
// 银行监管
|
||||
BANK: {
|
||||
DASHBOARD: '/bank/dashboard',
|
||||
LOANS: '/bank/loans',
|
||||
LOAN_DETAIL: '/bank/loans/:id',
|
||||
LOAN_APPROVE: '/bank/loans/:id/approve',
|
||||
RISK_ASSESSMENT: '/bank/risk/assessment',
|
||||
RISK_MONITOR: '/bank/risk/monitor',
|
||||
COMPLIANCE_CHECK: '/bank/compliance/check',
|
||||
AUDIT_REPORTS: '/bank/audit/reports'
|
||||
},
|
||||
|
||||
// 保险监管
|
||||
INSURANCE: {
|
||||
DASHBOARD: '/insurance/dashboard',
|
||||
POLICIES: '/insurance/policies',
|
||||
POLICY_DETAIL: '/insurance/policies/:id',
|
||||
CLAIMS: '/insurance/claims',
|
||||
CLAIM_DETAIL: '/insurance/claims/:id',
|
||||
CLAIM_PROCESS: '/insurance/claims/:id/process',
|
||||
RISK_ASSESSMENT: '/insurance/risk/assessment',
|
||||
COMPLIANCE_CHECK: '/insurance/compliance/check',
|
||||
CLAIM_ALERTS: '/insurance/claim-alerts'
|
||||
},
|
||||
|
||||
// 通用接口
|
||||
COMMON: {
|
||||
UPLOAD: '/common/upload',
|
||||
WEATHER: '/common/weather',
|
||||
REGIONS: '/common/regions',
|
||||
DICTIONARIES: '/common/dictionaries'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前环境
|
||||
const getCurrentEnv = () => {
|
||||
// #ifdef MP-WEIXIN
|
||||
return 'production'
|
||||
// #endif
|
||||
|
||||
// #ifdef H5
|
||||
const hostname = window.location.hostname
|
||||
if (hostname.includes('localhost') || hostname.includes('127.0.0.1')) {
|
||||
return 'development'
|
||||
} else if (hostname.includes('test')) {
|
||||
return 'testing'
|
||||
} else {
|
||||
return 'production'
|
||||
}
|
||||
// #endif
|
||||
|
||||
return 'production'
|
||||
}
|
||||
|
||||
// 获取基础URL
|
||||
const getBaseURL = () => {
|
||||
const env = getCurrentEnv()
|
||||
return API_CONFIG.BASE_URL[env]
|
||||
}
|
||||
|
||||
// 构建完整URL
|
||||
const buildURL = (endpoint, params = {}) => {
|
||||
let url = endpoint
|
||||
|
||||
// 替换路径参数
|
||||
Object.keys(params).forEach(key => {
|
||||
url = url.replace(`:${key}`, params[key])
|
||||
})
|
||||
|
||||
return url
|
||||
}
|
||||
|
||||
export {
|
||||
API_CONFIG,
|
||||
API_ENDPOINTS,
|
||||
getCurrentEnv,
|
||||
getBaseURL,
|
||||
buildURL
|
||||
}
|
||||
527
mini_program/common/config/app.config.js
Normal file
527
mini_program/common/config/app.config.js
Normal file
@@ -0,0 +1,527 @@
|
||||
/**
|
||||
* 应用配置文件
|
||||
* 包含应用的基础配置、API配置、功能开关等
|
||||
*/
|
||||
|
||||
// 环境配置
|
||||
export const ENV_CONFIG = {
|
||||
// 开发环境
|
||||
development: {
|
||||
API_BASE_URL: 'https://dev-api.xlxumu.com',
|
||||
WS_BASE_URL: 'wss://dev-ws.xlxumu.com',
|
||||
CDN_BASE_URL: 'https://dev-cdn.xlxumu.com',
|
||||
DEBUG: true,
|
||||
LOG_LEVEL: 'debug'
|
||||
},
|
||||
|
||||
// 测试环境
|
||||
testing: {
|
||||
API_BASE_URL: 'https://test-api.xlxumu.com',
|
||||
WS_BASE_URL: 'wss://test-ws.xlxumu.com',
|
||||
CDN_BASE_URL: 'https://test-cdn.xlxumu.com',
|
||||
DEBUG: true,
|
||||
LOG_LEVEL: 'info'
|
||||
},
|
||||
|
||||
// 生产环境
|
||||
production: {
|
||||
API_BASE_URL: 'https://api.xlxumu.com',
|
||||
WS_BASE_URL: 'wss://ws.xlxumu.com',
|
||||
CDN_BASE_URL: 'https://cdn.xlxumu.com',
|
||||
DEBUG: false,
|
||||
LOG_LEVEL: 'error'
|
||||
}
|
||||
}
|
||||
|
||||
// 当前环境
|
||||
export const CURRENT_ENV = process.env.NODE_ENV || 'development'
|
||||
|
||||
// 当前环境配置
|
||||
export const CONFIG = ENV_CONFIG[CURRENT_ENV]
|
||||
|
||||
// 应用基础信息
|
||||
export const APP_INFO = {
|
||||
NAME: '畜牧管理系统',
|
||||
VERSION: '1.0.0',
|
||||
BUILD_TIME: '2024-01-01 00:00:00',
|
||||
AUTHOR: 'XLXUMU Team',
|
||||
DESCRIPTION: '智慧畜牧业管理平台',
|
||||
KEYWORDS: ['畜牧', '养殖', '管理', '智慧农业'],
|
||||
|
||||
// 小程序信息
|
||||
MINI_PROGRAMS: {
|
||||
'farming-manager': {
|
||||
name: '养殖管理',
|
||||
appId: 'wx1234567890abcdef',
|
||||
version: '1.0.0',
|
||||
description: '专业的养殖场管理工具'
|
||||
},
|
||||
'cattle-trading': {
|
||||
name: '牲畜交易',
|
||||
appId: 'wx2345678901bcdefg',
|
||||
version: '1.0.0',
|
||||
description: '安全便捷的牲畜交易平台'
|
||||
},
|
||||
'beef-mall': {
|
||||
name: '牛肉商城',
|
||||
appId: 'wx3456789012cdefgh',
|
||||
version: '1.0.0',
|
||||
description: '优质牛肉在线购买平台'
|
||||
},
|
||||
'bank-supervision': {
|
||||
name: '银行监管',
|
||||
appId: 'wx4567890123defghi',
|
||||
version: '1.0.0',
|
||||
description: '畜牧业金融监管服务'
|
||||
},
|
||||
'insurance-supervision': {
|
||||
name: '保险监管',
|
||||
appId: 'wx5678901234efghij',
|
||||
version: '1.0.0',
|
||||
description: '畜牧业保险监管平台'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// API配置
|
||||
export const API_CONFIG = {
|
||||
// 请求超时时间
|
||||
TIMEOUT: 10000,
|
||||
|
||||
// 重试次数
|
||||
RETRY_COUNT: 3,
|
||||
|
||||
// 重试间隔(毫秒)
|
||||
RETRY_INTERVAL: 1000,
|
||||
|
||||
// 请求头配置
|
||||
HEADERS: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
|
||||
// 状态码配置
|
||||
STATUS_CODES: {
|
||||
SUCCESS: 200,
|
||||
UNAUTHORIZED: 401,
|
||||
FORBIDDEN: 403,
|
||||
NOT_FOUND: 404,
|
||||
SERVER_ERROR: 500,
|
||||
NETWORK_ERROR: -1
|
||||
},
|
||||
|
||||
// 错误消息配置
|
||||
ERROR_MESSAGES: {
|
||||
NETWORK_ERROR: '网络连接失败,请检查网络设置',
|
||||
TIMEOUT_ERROR: '请求超时,请稍后重试',
|
||||
SERVER_ERROR: '服务器错误,请稍后重试',
|
||||
UNAUTHORIZED: '登录已过期,请重新登录',
|
||||
FORBIDDEN: '没有权限访问该资源',
|
||||
NOT_FOUND: '请求的资源不存在',
|
||||
UNKNOWN_ERROR: '未知错误,请稍后重试'
|
||||
}
|
||||
}
|
||||
|
||||
// 存储配置
|
||||
export const STORAGE_CONFIG = {
|
||||
// 存储键名前缀
|
||||
KEY_PREFIX: 'xlxumu_',
|
||||
|
||||
// 默认过期时间(毫秒)
|
||||
DEFAULT_EXPIRE_TIME: 7 * 24 * 60 * 60 * 1000, // 7天
|
||||
|
||||
// 最大存储大小(字节)
|
||||
MAX_SIZE: 10 * 1024 * 1024, // 10MB
|
||||
|
||||
// 缓存配置
|
||||
CACHE: {
|
||||
API_CACHE_TIME: 5 * 60 * 1000, // API缓存5分钟
|
||||
IMAGE_CACHE_TIME: 24 * 60 * 60 * 1000, // 图片缓存24小时
|
||||
USER_CACHE_TIME: 30 * 24 * 60 * 60 * 1000 // 用户信息缓存30天
|
||||
}
|
||||
}
|
||||
|
||||
// 上传配置
|
||||
export const UPLOAD_CONFIG = {
|
||||
// 最大文件大小(字节)
|
||||
MAX_FILE_SIZE: 10 * 1024 * 1024, // 10MB
|
||||
|
||||
// 允许的文件类型
|
||||
ALLOWED_TYPES: {
|
||||
image: ['jpg', 'jpeg', 'png', 'gif', 'webp'],
|
||||
video: ['mp4', 'avi', 'mov', 'wmv'],
|
||||
document: ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'],
|
||||
audio: ['mp3', 'wav', 'aac', 'm4a']
|
||||
},
|
||||
|
||||
// 上传路径配置
|
||||
UPLOAD_PATHS: {
|
||||
avatar: '/uploads/avatars/',
|
||||
livestock: '/uploads/livestock/',
|
||||
documents: '/uploads/documents/',
|
||||
reports: '/uploads/reports/',
|
||||
certificates: '/uploads/certificates/'
|
||||
},
|
||||
|
||||
// 图片压缩配置
|
||||
IMAGE_COMPRESS: {
|
||||
quality: 0.8,
|
||||
maxWidth: 1920,
|
||||
maxHeight: 1080
|
||||
}
|
||||
}
|
||||
|
||||
// 地图配置
|
||||
export const MAP_CONFIG = {
|
||||
// 默认中心点(北京)
|
||||
DEFAULT_CENTER: {
|
||||
longitude: 116.397428,
|
||||
latitude: 39.90923
|
||||
},
|
||||
|
||||
// 默认缩放级别
|
||||
DEFAULT_SCALE: 16,
|
||||
|
||||
// 地图类型
|
||||
MAP_TYPES: {
|
||||
NORMAL: 'normal',
|
||||
SATELLITE: 'satellite',
|
||||
HYBRID: 'hybrid'
|
||||
},
|
||||
|
||||
// 定位配置
|
||||
LOCATION: {
|
||||
type: 'gcj02',
|
||||
altitude: true,
|
||||
geocode: true,
|
||||
timeout: 10000
|
||||
}
|
||||
}
|
||||
|
||||
// 支付配置
|
||||
export const PAYMENT_CONFIG = {
|
||||
// 支付方式
|
||||
PAYMENT_METHODS: {
|
||||
WECHAT: 'wechat',
|
||||
ALIPAY: 'alipay',
|
||||
BANK_CARD: 'bank_card',
|
||||
BALANCE: 'balance'
|
||||
},
|
||||
|
||||
// 支付环境
|
||||
PAYMENT_ENV: CURRENT_ENV === 'production' ? 'production' : 'sandbox',
|
||||
|
||||
// 支付超时时间(秒)
|
||||
PAYMENT_TIMEOUT: 300,
|
||||
|
||||
// 最小支付金额(分)
|
||||
MIN_PAYMENT_AMOUNT: 1,
|
||||
|
||||
// 最大支付金额(分)
|
||||
MAX_PAYMENT_AMOUNT: 100000000 // 100万元
|
||||
}
|
||||
|
||||
// 推送配置
|
||||
export const PUSH_CONFIG = {
|
||||
// 推送类型
|
||||
PUSH_TYPES: {
|
||||
SYSTEM: 'system',
|
||||
BUSINESS: 'business',
|
||||
MARKETING: 'marketing'
|
||||
},
|
||||
|
||||
// 推送渠道
|
||||
PUSH_CHANNELS: {
|
||||
WECHAT: 'wechat',
|
||||
SMS: 'sms',
|
||||
EMAIL: 'email',
|
||||
APP: 'app'
|
||||
},
|
||||
|
||||
// 推送模板
|
||||
PUSH_TEMPLATES: {
|
||||
LOGIN_SUCCESS: 'login_success',
|
||||
ORDER_CREATED: 'order_created',
|
||||
ORDER_PAID: 'order_paid',
|
||||
ORDER_SHIPPED: 'order_shipped',
|
||||
ORDER_DELIVERED: 'order_delivered',
|
||||
HEALTH_ALERT: 'health_alert',
|
||||
BREEDING_REMINDER: 'breeding_reminder',
|
||||
FEEDING_REMINDER: 'feeding_reminder'
|
||||
}
|
||||
}
|
||||
|
||||
// 功能开关配置
|
||||
export const FEATURE_FLAGS = {
|
||||
// 基础功能
|
||||
USER_REGISTRATION: true,
|
||||
USER_LOGIN: true,
|
||||
PASSWORD_RESET: true,
|
||||
|
||||
// 业务功能
|
||||
LIVESTOCK_MANAGEMENT: true,
|
||||
HEALTH_MONITORING: true,
|
||||
BREEDING_MANAGEMENT: true,
|
||||
FEEDING_MANAGEMENT: true,
|
||||
PRODUCTION_TRACKING: true,
|
||||
|
||||
// 交易功能
|
||||
CATTLE_TRADING: true,
|
||||
ONLINE_PAYMENT: true,
|
||||
ORDER_TRACKING: true,
|
||||
|
||||
// 商城功能
|
||||
BEEF_MALL: true,
|
||||
PRODUCT_CATALOG: true,
|
||||
SHOPPING_CART: true,
|
||||
ORDER_MANAGEMENT: true,
|
||||
|
||||
// 金融功能
|
||||
LOAN_APPLICATION: true,
|
||||
INSURANCE_PURCHASE: true,
|
||||
FINANCIAL_REPORTS: true,
|
||||
|
||||
// 高级功能
|
||||
AI_ANALYSIS: false,
|
||||
IOT_INTEGRATION: false,
|
||||
BLOCKCHAIN_TRACING: false,
|
||||
|
||||
// 实验性功能
|
||||
VOICE_CONTROL: false,
|
||||
AR_VISUALIZATION: false,
|
||||
PREDICTIVE_ANALYTICS: false
|
||||
}
|
||||
|
||||
// 权限配置
|
||||
export const PERMISSION_CONFIG = {
|
||||
// 角色定义
|
||||
ROLES: {
|
||||
SUPER_ADMIN: 'super_admin',
|
||||
ADMIN: 'admin',
|
||||
MANAGER: 'manager',
|
||||
OPERATOR: 'operator',
|
||||
VIEWER: 'viewer',
|
||||
USER: 'user'
|
||||
},
|
||||
|
||||
// 权限定义
|
||||
PERMISSIONS: {
|
||||
// 用户管理
|
||||
USER_CREATE: 'user:create',
|
||||
USER_READ: 'user:read',
|
||||
USER_UPDATE: 'user:update',
|
||||
USER_DELETE: 'user:delete',
|
||||
|
||||
// 牲畜管理
|
||||
LIVESTOCK_CREATE: 'livestock:create',
|
||||
LIVESTOCK_READ: 'livestock:read',
|
||||
LIVESTOCK_UPDATE: 'livestock:update',
|
||||
LIVESTOCK_DELETE: 'livestock:delete',
|
||||
|
||||
// 健康管理
|
||||
HEALTH_CREATE: 'health:create',
|
||||
HEALTH_READ: 'health:read',
|
||||
HEALTH_UPDATE: 'health:update',
|
||||
HEALTH_DELETE: 'health:delete',
|
||||
|
||||
// 交易管理
|
||||
TRADE_CREATE: 'trade:create',
|
||||
TRADE_READ: 'trade:read',
|
||||
TRADE_UPDATE: 'trade:update',
|
||||
TRADE_DELETE: 'trade:delete',
|
||||
|
||||
// 财务管理
|
||||
FINANCE_CREATE: 'finance:create',
|
||||
FINANCE_READ: 'finance:read',
|
||||
FINANCE_UPDATE: 'finance:update',
|
||||
FINANCE_DELETE: 'finance:delete',
|
||||
|
||||
// 报表管理
|
||||
REPORT_CREATE: 'report:create',
|
||||
REPORT_READ: 'report:read',
|
||||
REPORT_EXPORT: 'report:export',
|
||||
|
||||
// 系统管理
|
||||
SYSTEM_CONFIG: 'system:config',
|
||||
SYSTEM_LOG: 'system:log',
|
||||
SYSTEM_BACKUP: 'system:backup'
|
||||
},
|
||||
|
||||
// 角色权限映射
|
||||
ROLE_PERMISSIONS: {
|
||||
super_admin: ['*'], // 所有权限
|
||||
admin: [
|
||||
'user:*', 'livestock:*', 'health:*', 'trade:*',
|
||||
'finance:*', 'report:*', 'system:config', 'system:log'
|
||||
],
|
||||
manager: [
|
||||
'user:read', 'user:update', 'livestock:*', 'health:*',
|
||||
'trade:*', 'finance:read', 'report:*'
|
||||
],
|
||||
operator: [
|
||||
'livestock:create', 'livestock:read', 'livestock:update',
|
||||
'health:*', 'trade:read', 'report:read'
|
||||
],
|
||||
viewer: [
|
||||
'livestock:read', 'health:read', 'trade:read',
|
||||
'finance:read', 'report:read'
|
||||
],
|
||||
user: [
|
||||
'livestock:read', 'health:read', 'report:read'
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// 主题配置
|
||||
export const THEME_CONFIG = {
|
||||
// 默认主题
|
||||
DEFAULT_THEME: 'light',
|
||||
|
||||
// 主题列表
|
||||
THEMES: {
|
||||
light: {
|
||||
name: '浅色主题',
|
||||
colors: {
|
||||
primary: '#2e8b57',
|
||||
secondary: '#ff4757',
|
||||
success: '#52c41a',
|
||||
warning: '#faad14',
|
||||
error: '#ff4757',
|
||||
info: '#1890ff',
|
||||
background: '#ffffff',
|
||||
surface: '#f8f9fa',
|
||||
text: '#333333',
|
||||
textSecondary: '#666666',
|
||||
border: '#e5e5e5'
|
||||
}
|
||||
},
|
||||
dark: {
|
||||
name: '深色主题',
|
||||
colors: {
|
||||
primary: '#52c41a',
|
||||
secondary: '#ff7875',
|
||||
success: '#73d13d',
|
||||
warning: '#ffc53d',
|
||||
error: '#ff7875',
|
||||
info: '#40a9ff',
|
||||
background: '#1f1f1f',
|
||||
surface: '#2f2f2f',
|
||||
text: '#ffffff',
|
||||
textSecondary: '#cccccc',
|
||||
border: '#404040'
|
||||
}
|
||||
},
|
||||
green: {
|
||||
name: '绿色主题',
|
||||
colors: {
|
||||
primary: '#389e0d',
|
||||
secondary: '#fa8c16',
|
||||
success: '#52c41a',
|
||||
warning: '#faad14',
|
||||
error: '#f5222d',
|
||||
info: '#1890ff',
|
||||
background: '#ffffff',
|
||||
surface: '#f6ffed',
|
||||
text: '#333333',
|
||||
textSecondary: '#666666',
|
||||
border: '#d9f7be'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 国际化配置
|
||||
export const I18N_CONFIG = {
|
||||
// 默认语言
|
||||
DEFAULT_LOCALE: 'zh-CN',
|
||||
|
||||
// 支持的语言
|
||||
SUPPORTED_LOCALES: {
|
||||
'zh-CN': '简体中文',
|
||||
'zh-TW': '繁體中文',
|
||||
'en-US': 'English',
|
||||
'ja-JP': '日本語',
|
||||
'ko-KR': '한국어'
|
||||
},
|
||||
|
||||
// 语言包加载配置
|
||||
LAZY_LOAD: true,
|
||||
|
||||
// 回退语言
|
||||
FALLBACK_LOCALE: 'zh-CN'
|
||||
}
|
||||
|
||||
// 日志配置
|
||||
export const LOG_CONFIG = {
|
||||
// 日志级别
|
||||
LEVELS: {
|
||||
ERROR: 0,
|
||||
WARN: 1,
|
||||
INFO: 2,
|
||||
DEBUG: 3
|
||||
},
|
||||
|
||||
// 当前日志级别
|
||||
CURRENT_LEVEL: CONFIG.LOG_LEVEL || 'info',
|
||||
|
||||
// 日志输出配置
|
||||
OUTPUT: {
|
||||
console: true,
|
||||
file: false,
|
||||
remote: CURRENT_ENV === 'production'
|
||||
},
|
||||
|
||||
// 远程日志配置
|
||||
REMOTE: {
|
||||
url: `${CONFIG.API_BASE_URL}/api/logs`,
|
||||
batchSize: 10,
|
||||
flushInterval: 30000 // 30秒
|
||||
}
|
||||
}
|
||||
|
||||
// 性能监控配置
|
||||
export const PERFORMANCE_CONFIG = {
|
||||
// 是否启用性能监控
|
||||
ENABLED: CURRENT_ENV === 'production',
|
||||
|
||||
// 采样率
|
||||
SAMPLE_RATE: 0.1,
|
||||
|
||||
// 监控指标
|
||||
METRICS: {
|
||||
PAGE_LOAD_TIME: true,
|
||||
API_RESPONSE_TIME: true,
|
||||
ERROR_RATE: true,
|
||||
CRASH_RATE: true,
|
||||
MEMORY_USAGE: false,
|
||||
CPU_USAGE: false
|
||||
},
|
||||
|
||||
// 上报配置
|
||||
REPORT: {
|
||||
url: `${CONFIG.API_BASE_URL}/api/performance`,
|
||||
interval: 60000, // 1分钟
|
||||
batchSize: 50
|
||||
}
|
||||
}
|
||||
|
||||
// 导出默认配置
|
||||
export default {
|
||||
ENV_CONFIG,
|
||||
CURRENT_ENV,
|
||||
CONFIG,
|
||||
APP_INFO,
|
||||
API_CONFIG,
|
||||
STORAGE_CONFIG,
|
||||
UPLOAD_CONFIG,
|
||||
MAP_CONFIG,
|
||||
PAYMENT_CONFIG,
|
||||
PUSH_CONFIG,
|
||||
FEATURE_FLAGS,
|
||||
PERMISSION_CONFIG,
|
||||
THEME_CONFIG,
|
||||
I18N_CONFIG,
|
||||
LOG_CONFIG,
|
||||
PERFORMANCE_CONFIG
|
||||
}
|
||||
336
mini_program/common/config/index.js
Normal file
336
mini_program/common/config/index.js
Normal file
@@ -0,0 +1,336 @@
|
||||
/**
|
||||
* 全局配置文件
|
||||
* 统一管理应用配置信息
|
||||
*/
|
||||
|
||||
// 环境配置
|
||||
const ENV_CONFIG = {
|
||||
development: {
|
||||
baseURL: 'https://dev-api.xlxumu.com',
|
||||
wsURL: 'wss://dev-ws.xlxumu.com',
|
||||
debug: true,
|
||||
logLevel: 'debug'
|
||||
},
|
||||
testing: {
|
||||
baseURL: 'https://test-api.xlxumu.com',
|
||||
wsURL: 'wss://test-ws.xlxumu.com',
|
||||
debug: true,
|
||||
logLevel: 'info'
|
||||
},
|
||||
production: {
|
||||
baseURL: 'https://api.xlxumu.com',
|
||||
wsURL: 'wss://ws.xlxumu.com',
|
||||
debug: false,
|
||||
logLevel: 'error'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前环境
|
||||
const getCurrentEnv = () => {
|
||||
// #ifdef MP-WEIXIN
|
||||
return process.env.NODE_ENV || 'production'
|
||||
// #endif
|
||||
|
||||
// #ifdef H5
|
||||
return process.env.NODE_ENV || 'production'
|
||||
// #endif
|
||||
|
||||
// #ifdef APP-PLUS
|
||||
return process.env.NODE_ENV || 'production'
|
||||
// #endif
|
||||
|
||||
return 'production'
|
||||
}
|
||||
|
||||
// 当前环境配置
|
||||
const currentEnv = getCurrentEnv()
|
||||
const config = ENV_CONFIG[currentEnv] || ENV_CONFIG.production
|
||||
|
||||
// 应用配置
|
||||
export const APP_CONFIG = {
|
||||
// 应用信息
|
||||
name: '智慧畜牧业小程序矩阵',
|
||||
version: '1.0.0',
|
||||
description: '基于uni-app开发的智慧畜牧业管理系统',
|
||||
|
||||
// 环境配置
|
||||
env: currentEnv,
|
||||
debug: config.debug,
|
||||
logLevel: config.logLevel,
|
||||
|
||||
// API配置
|
||||
api: {
|
||||
baseURL: config.baseURL,
|
||||
timeout: 10000,
|
||||
retryCount: 3,
|
||||
retryDelay: 1000
|
||||
},
|
||||
|
||||
// WebSocket配置
|
||||
websocket: {
|
||||
url: config.wsURL,
|
||||
heartbeatInterval: 30000,
|
||||
reconnectInterval: 5000,
|
||||
maxReconnectAttempts: 5
|
||||
},
|
||||
|
||||
// 存储配置
|
||||
storage: {
|
||||
prefix: 'xlxumu_',
|
||||
tokenKey: 'access_token',
|
||||
userKey: 'user_info',
|
||||
settingsKey: 'app_settings'
|
||||
},
|
||||
|
||||
// 分页配置
|
||||
pagination: {
|
||||
defaultPageSize: 20,
|
||||
maxPageSize: 100
|
||||
},
|
||||
|
||||
// 上传配置
|
||||
upload: {
|
||||
maxSize: 10 * 1024 * 1024, // 10MB
|
||||
allowedTypes: ['image/jpeg', 'image/png', 'image/gif'],
|
||||
quality: 0.8
|
||||
},
|
||||
|
||||
// 地图配置
|
||||
map: {
|
||||
key: 'your-map-key',
|
||||
defaultLocation: {
|
||||
latitude: 39.908823,
|
||||
longitude: 116.397470
|
||||
}
|
||||
},
|
||||
|
||||
// 主题配置
|
||||
theme: {
|
||||
primaryColor: '#2E8B57',
|
||||
secondaryColor: '#32CD32',
|
||||
successColor: '#67C23A',
|
||||
warningColor: '#E6A23C',
|
||||
errorColor: '#F56C6C',
|
||||
infoColor: '#909399'
|
||||
},
|
||||
|
||||
// 缓存配置
|
||||
cache: {
|
||||
defaultExpire: 30 * 60 * 1000, // 30分钟
|
||||
maxSize: 50 * 1024 * 1024 // 50MB
|
||||
}
|
||||
}
|
||||
|
||||
// 小程序特定配置
|
||||
export const MINIPROGRAM_CONFIG = {
|
||||
// 养殖管理小程序
|
||||
'farming-manager': {
|
||||
appId: 'wx1234567890abcdef',
|
||||
name: '养殖管理',
|
||||
description: '专业的畜牧养殖管理工具',
|
||||
features: ['养殖记录', '健康监测', '饲料管理', '疫苗管理']
|
||||
},
|
||||
|
||||
// 牛只交易小程序
|
||||
'cattle-trading': {
|
||||
appId: 'wx2345678901bcdefg',
|
||||
name: '牛只交易',
|
||||
description: '安全可靠的牛只交易平台',
|
||||
features: ['交易发布', '价格查询', '交易撮合', '支付结算']
|
||||
},
|
||||
|
||||
// 牛肉商城小程序
|
||||
'beef-mall': {
|
||||
appId: 'wx3456789012cdefgh',
|
||||
name: '牛肉商城',
|
||||
description: '优质牛肉在线购买平台',
|
||||
features: ['商品展示', '在线下单', '配送跟踪', '售后服务']
|
||||
},
|
||||
|
||||
// 银行监管小程序
|
||||
'bank-supervision': {
|
||||
appId: 'wx4567890123defghi',
|
||||
name: '银行监管',
|
||||
description: '畜牧业金融监管服务平台',
|
||||
features: ['贷款申请', '风险评估', '资金监管', '数据报告']
|
||||
},
|
||||
|
||||
// 保险监管小程序
|
||||
'insurance-supervision': {
|
||||
appId: 'wx5678901234efghij',
|
||||
name: '保险监管',
|
||||
description: '畜牧业保险监管服务平台',
|
||||
features: ['保险申请', '理赔处理', '风险评估', '监管报告']
|
||||
}
|
||||
}
|
||||
|
||||
// API接口配置
|
||||
export const API_CONFIG = {
|
||||
// 用户相关
|
||||
user: {
|
||||
login: '/auth/login',
|
||||
logout: '/auth/logout',
|
||||
register: '/auth/register',
|
||||
profile: '/user/profile',
|
||||
updateProfile: '/user/profile'
|
||||
},
|
||||
|
||||
// 养殖管理
|
||||
farming: {
|
||||
list: '/farming/list',
|
||||
detail: '/farming/detail',
|
||||
create: '/farming/create',
|
||||
update: '/farming/update',
|
||||
delete: '/farming/delete',
|
||||
records: '/farming/records',
|
||||
health: '/farming/health',
|
||||
feed: '/farming/feed',
|
||||
vaccine: '/farming/vaccine'
|
||||
},
|
||||
|
||||
// 交易相关
|
||||
trading: {
|
||||
list: '/trading/list',
|
||||
detail: '/trading/detail',
|
||||
publish: '/trading/publish',
|
||||
bid: '/trading/bid',
|
||||
orders: '/trading/orders',
|
||||
payment: '/trading/payment'
|
||||
},
|
||||
|
||||
// 商城相关
|
||||
mall: {
|
||||
products: '/mall/products',
|
||||
categories: '/mall/categories',
|
||||
cart: '/mall/cart',
|
||||
orders: '/mall/orders',
|
||||
payment: '/mall/payment'
|
||||
},
|
||||
|
||||
// 金融相关
|
||||
finance: {
|
||||
loans: '/finance/loans',
|
||||
apply: '/finance/apply',
|
||||
repayment: '/finance/repayment',
|
||||
records: '/finance/records'
|
||||
},
|
||||
|
||||
// 保险相关
|
||||
insurance: {
|
||||
policies: '/insurance/policies',
|
||||
apply: '/insurance/apply',
|
||||
claims: '/insurance/claims',
|
||||
records: '/insurance/records'
|
||||
},
|
||||
|
||||
// 文件上传
|
||||
upload: {
|
||||
image: '/upload/image',
|
||||
file: '/upload/file',
|
||||
video: '/upload/video'
|
||||
},
|
||||
|
||||
// 系统相关
|
||||
system: {
|
||||
config: '/system/config',
|
||||
version: '/system/version',
|
||||
feedback: '/system/feedback'
|
||||
}
|
||||
}
|
||||
|
||||
// 错误码配置
|
||||
export const ERROR_CODES = {
|
||||
// 通用错误
|
||||
SUCCESS: 0,
|
||||
UNKNOWN_ERROR: -1,
|
||||
NETWORK_ERROR: -2,
|
||||
TIMEOUT_ERROR: -3,
|
||||
|
||||
// 认证错误
|
||||
AUTH_FAILED: 1001,
|
||||
TOKEN_EXPIRED: 1002,
|
||||
PERMISSION_DENIED: 1003,
|
||||
|
||||
// 参数错误
|
||||
INVALID_PARAMS: 2001,
|
||||
MISSING_PARAMS: 2002,
|
||||
|
||||
// 业务错误
|
||||
USER_NOT_FOUND: 3001,
|
||||
USER_ALREADY_EXISTS: 3002,
|
||||
INSUFFICIENT_BALANCE: 3003,
|
||||
ORDER_NOT_FOUND: 3004,
|
||||
PRODUCT_OUT_OF_STOCK: 3005
|
||||
}
|
||||
|
||||
// 错误消息配置
|
||||
export const ERROR_MESSAGES = {
|
||||
[ERROR_CODES.SUCCESS]: '操作成功',
|
||||
[ERROR_CODES.UNKNOWN_ERROR]: '未知错误',
|
||||
[ERROR_CODES.NETWORK_ERROR]: '网络连接失败',
|
||||
[ERROR_CODES.TIMEOUT_ERROR]: '请求超时',
|
||||
[ERROR_CODES.AUTH_FAILED]: '认证失败',
|
||||
[ERROR_CODES.TOKEN_EXPIRED]: '登录已过期,请重新登录',
|
||||
[ERROR_CODES.PERMISSION_DENIED]: '权限不足',
|
||||
[ERROR_CODES.INVALID_PARAMS]: '参数错误',
|
||||
[ERROR_CODES.MISSING_PARAMS]: '缺少必要参数',
|
||||
[ERROR_CODES.USER_NOT_FOUND]: '用户不存在',
|
||||
[ERROR_CODES.USER_ALREADY_EXISTS]: '用户已存在',
|
||||
[ERROR_CODES.INSUFFICIENT_BALANCE]: '余额不足',
|
||||
[ERROR_CODES.ORDER_NOT_FOUND]: '订单不存在',
|
||||
[ERROR_CODES.PRODUCT_OUT_OF_STOCK]: '商品库存不足'
|
||||
}
|
||||
|
||||
// 常量配置
|
||||
export const CONSTANTS = {
|
||||
// 性别
|
||||
GENDER: {
|
||||
MALE: 1,
|
||||
FEMALE: 2,
|
||||
UNKNOWN: 0
|
||||
},
|
||||
|
||||
// 状态
|
||||
STATUS: {
|
||||
ACTIVE: 1,
|
||||
INACTIVE: 0,
|
||||
PENDING: 2,
|
||||
DELETED: -1
|
||||
},
|
||||
|
||||
// 订单状态
|
||||
ORDER_STATUS: {
|
||||
PENDING: 1,
|
||||
PAID: 2,
|
||||
SHIPPED: 3,
|
||||
DELIVERED: 4,
|
||||
CANCELLED: -1,
|
||||
REFUNDED: -2
|
||||
},
|
||||
|
||||
// 支付方式
|
||||
PAYMENT_METHOD: {
|
||||
WECHAT: 1,
|
||||
ALIPAY: 2,
|
||||
BANK_CARD: 3,
|
||||
CASH: 4
|
||||
},
|
||||
|
||||
// 文件类型
|
||||
FILE_TYPE: {
|
||||
IMAGE: 1,
|
||||
VIDEO: 2,
|
||||
DOCUMENT: 3,
|
||||
AUDIO: 4
|
||||
}
|
||||
}
|
||||
|
||||
// 导出默认配置
|
||||
export default {
|
||||
APP_CONFIG,
|
||||
MINIPROGRAM_CONFIG,
|
||||
API_CONFIG,
|
||||
ERROR_CODES,
|
||||
ERROR_MESSAGES,
|
||||
CONSTANTS
|
||||
}
|
||||
444
mini_program/common/mixins/page.js
Normal file
444
mini_program/common/mixins/page.js
Normal file
@@ -0,0 +1,444 @@
|
||||
// 页面通用混入
|
||||
import auth from '@/common/utils/auth'
|
||||
import permission from '@/common/utils/permission'
|
||||
import { formatTime, formatDate, formatNumber } from '@/common/utils/format'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
// 页面加载状态
|
||||
pageLoading: false,
|
||||
// 数据加载状态
|
||||
dataLoading: false,
|
||||
// 页面错误状态
|
||||
pageError: null,
|
||||
// 当前页码
|
||||
currentPage: 1,
|
||||
// 每页数量
|
||||
pageSize: 20,
|
||||
// 是否还有更多数据
|
||||
hasMore: true,
|
||||
// 搜索关键词
|
||||
searchKeyword: '',
|
||||
// 搜索定时器
|
||||
searchTimer: null
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
// 是否已登录
|
||||
isLoggedIn() {
|
||||
return auth.isLoggedIn()
|
||||
},
|
||||
|
||||
// 当前用户信息
|
||||
currentUser() {
|
||||
return auth.getUserInfo()
|
||||
},
|
||||
|
||||
// 用户头像
|
||||
userAvatar() {
|
||||
return auth.getUserAvatar()
|
||||
},
|
||||
|
||||
// 用户昵称
|
||||
userNickname() {
|
||||
return auth.getUserNickname()
|
||||
}
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
// 页面参数
|
||||
this.pageOptions = options || {}
|
||||
|
||||
// 检查页面访问权限
|
||||
if (this.pageConfig) {
|
||||
if (!permission.checkPageAccess(this.pageConfig)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化页面数据
|
||||
this.initPageData()
|
||||
},
|
||||
|
||||
onShow() {
|
||||
// 页面显示时的处理
|
||||
this.onPageShow()
|
||||
},
|
||||
|
||||
onHide() {
|
||||
// 页面隐藏时的处理
|
||||
this.onPageHide()
|
||||
},
|
||||
|
||||
onUnload() {
|
||||
// 页面卸载时的处理
|
||||
this.onPageUnload()
|
||||
},
|
||||
|
||||
onPullDownRefresh() {
|
||||
// 下拉刷新
|
||||
this.refreshData()
|
||||
},
|
||||
|
||||
onReachBottom() {
|
||||
// 上拉加载更多
|
||||
if (this.hasMore && !this.dataLoading) {
|
||||
this.loadMoreData()
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
// 初始化页面数据
|
||||
initPageData() {
|
||||
// 子类可以重写此方法
|
||||
this.loadData()
|
||||
},
|
||||
|
||||
// 页面显示处理
|
||||
onPageShow() {
|
||||
// 子类可以重写此方法
|
||||
},
|
||||
|
||||
// 页面隐藏处理
|
||||
onPageHide() {
|
||||
// 清除搜索定时器
|
||||
if (this.searchTimer) {
|
||||
clearTimeout(this.searchTimer)
|
||||
this.searchTimer = null
|
||||
}
|
||||
},
|
||||
|
||||
// 页面卸载处理
|
||||
onPageUnload() {
|
||||
// 清除定时器
|
||||
if (this.searchTimer) {
|
||||
clearTimeout(this.searchTimer)
|
||||
this.searchTimer = null
|
||||
}
|
||||
},
|
||||
|
||||
// 加载数据
|
||||
async loadData(refresh = false) {
|
||||
// 子类需要重写此方法
|
||||
console.warn('loadData method should be implemented in child component')
|
||||
},
|
||||
|
||||
// 刷新数据
|
||||
async refreshData() {
|
||||
try {
|
||||
this.currentPage = 1
|
||||
this.hasMore = true
|
||||
await this.loadData(true)
|
||||
} finally {
|
||||
uni.stopPullDownRefresh()
|
||||
}
|
||||
},
|
||||
|
||||
// 加载更多数据
|
||||
async loadMoreData() {
|
||||
if (!this.hasMore || this.dataLoading) {
|
||||
return
|
||||
}
|
||||
|
||||
this.currentPage++
|
||||
await this.loadData(false)
|
||||
},
|
||||
|
||||
// 搜索处理
|
||||
onSearch(keyword) {
|
||||
this.searchKeyword = keyword
|
||||
|
||||
// 防抖处理
|
||||
if (this.searchTimer) {
|
||||
clearTimeout(this.searchTimer)
|
||||
}
|
||||
|
||||
this.searchTimer = setTimeout(() => {
|
||||
this.refreshData()
|
||||
}, 500)
|
||||
},
|
||||
|
||||
// 显示加载提示
|
||||
showLoading(title = '加载中...') {
|
||||
uni.showLoading({
|
||||
title,
|
||||
mask: true
|
||||
})
|
||||
},
|
||||
|
||||
// 隐藏加载提示
|
||||
hideLoading() {
|
||||
uni.hideLoading()
|
||||
},
|
||||
|
||||
// 显示成功提示
|
||||
showSuccess(title, duration = 2000) {
|
||||
uni.showToast({
|
||||
title,
|
||||
icon: 'success',
|
||||
duration
|
||||
})
|
||||
},
|
||||
|
||||
// 显示错误提示
|
||||
showError(title, duration = 2000) {
|
||||
uni.showToast({
|
||||
title,
|
||||
icon: 'none',
|
||||
duration
|
||||
})
|
||||
},
|
||||
|
||||
// 显示确认对话框
|
||||
showConfirm(content, title = '提示') {
|
||||
return new Promise((resolve) => {
|
||||
uni.showModal({
|
||||
title,
|
||||
content,
|
||||
success: (res) => {
|
||||
resolve(res.confirm)
|
||||
},
|
||||
fail: () => {
|
||||
resolve(false)
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
// 显示操作菜单
|
||||
showActionSheet(itemList) {
|
||||
return new Promise((resolve) => {
|
||||
uni.showActionSheet({
|
||||
itemList,
|
||||
success: (res) => {
|
||||
resolve(res.tapIndex)
|
||||
},
|
||||
fail: () => {
|
||||
resolve(-1)
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
// 跳转页面
|
||||
navigateTo(url, params = {}) {
|
||||
// 构建URL参数
|
||||
const query = Object.keys(params)
|
||||
.map(key => `${key}=${encodeURIComponent(params[key])}`)
|
||||
.join('&')
|
||||
|
||||
const fullUrl = query ? `${url}?${query}` : url
|
||||
|
||||
uni.navigateTo({
|
||||
url: fullUrl,
|
||||
fail: (error) => {
|
||||
console.error('页面跳转失败:', error)
|
||||
this.showError('页面跳转失败')
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 替换页面
|
||||
redirectTo(url, params = {}) {
|
||||
const query = Object.keys(params)
|
||||
.map(key => `${key}=${encodeURIComponent(params[key])}`)
|
||||
.join('&')
|
||||
|
||||
const fullUrl = query ? `${url}?${query}` : url
|
||||
|
||||
uni.redirectTo({
|
||||
url: fullUrl,
|
||||
fail: (error) => {
|
||||
console.error('页面替换失败:', error)
|
||||
this.showError('页面跳转失败')
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 切换标签页
|
||||
switchTab(url) {
|
||||
uni.switchTab({
|
||||
url,
|
||||
fail: (error) => {
|
||||
console.error('标签页切换失败:', error)
|
||||
this.showError('页面跳转失败')
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 返回上一页
|
||||
navigateBack(delta = 1) {
|
||||
uni.navigateBack({
|
||||
delta,
|
||||
fail: (error) => {
|
||||
console.error('页面返回失败:', error)
|
||||
// 如果返回失败,跳转到首页
|
||||
this.switchTab('/pages/index/index')
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 检查权限
|
||||
hasPermission(permission) {
|
||||
return auth.hasPermission(permission)
|
||||
},
|
||||
|
||||
// 检查角色
|
||||
hasRole(role) {
|
||||
return auth.hasRole(role)
|
||||
},
|
||||
|
||||
// 检查登录状态
|
||||
checkLogin() {
|
||||
if (!this.isLoggedIn) {
|
||||
this.navigateTo('/pages/auth/login')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
|
||||
// 格式化时间
|
||||
formatTime(timestamp, format = 'YYYY-MM-DD HH:mm:ss') {
|
||||
return formatTime(timestamp, format)
|
||||
},
|
||||
|
||||
// 格式化日期
|
||||
formatDate(timestamp, format = 'YYYY-MM-DD') {
|
||||
return formatDate(timestamp, format)
|
||||
},
|
||||
|
||||
// 格式化数字
|
||||
formatNumber(number, decimals = 2) {
|
||||
return formatNumber(number, decimals)
|
||||
},
|
||||
|
||||
// 复制到剪贴板
|
||||
copyToClipboard(text) {
|
||||
uni.setClipboardData({
|
||||
data: text,
|
||||
success: () => {
|
||||
this.showSuccess('已复制到剪贴板')
|
||||
},
|
||||
fail: () => {
|
||||
this.showError('复制失败')
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 拨打电话
|
||||
makePhoneCall(phoneNumber) {
|
||||
uni.makePhoneCall({
|
||||
phoneNumber,
|
||||
fail: () => {
|
||||
this.showError('拨打电话失败')
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 预览图片
|
||||
previewImage(urls, current = 0) {
|
||||
uni.previewImage({
|
||||
urls,
|
||||
current: typeof current === 'number' ? urls[current] : current,
|
||||
fail: () => {
|
||||
this.showError('预览图片失败')
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 选择图片
|
||||
chooseImage(count = 1, sizeType = ['compressed'], sourceType = ['album', 'camera']) {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.chooseImage({
|
||||
count,
|
||||
sizeType,
|
||||
sourceType,
|
||||
success: (res) => {
|
||||
resolve(res.tempFilePaths)
|
||||
},
|
||||
fail: (error) => {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
// 上传文件
|
||||
async uploadFile(filePath, name = 'file', formData = {}) {
|
||||
try {
|
||||
const res = await uni.uploadFile({
|
||||
url: this.$config.uploadUrl,
|
||||
filePath,
|
||||
name,
|
||||
formData,
|
||||
header: {
|
||||
'Authorization': `Bearer ${auth.getToken()}`
|
||||
}
|
||||
})
|
||||
|
||||
const data = JSON.parse(res.data)
|
||||
if (data.code === 200) {
|
||||
return data.data
|
||||
} else {
|
||||
throw new Error(data.message || '上传失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('文件上传失败:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
// 获取位置信息
|
||||
getLocation() {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.getLocation({
|
||||
type: 'gcj02',
|
||||
success: (res) => {
|
||||
resolve({
|
||||
latitude: res.latitude,
|
||||
longitude: res.longitude,
|
||||
address: res.address
|
||||
})
|
||||
},
|
||||
fail: (error) => {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
// 扫码
|
||||
scanCode() {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.scanCode({
|
||||
success: (res) => {
|
||||
resolve(res.result)
|
||||
},
|
||||
fail: (error) => {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
// 分享
|
||||
onShareAppMessage() {
|
||||
return {
|
||||
title: this.shareTitle || '智慧畜牧管理系统',
|
||||
path: this.sharePath || '/pages/index/index',
|
||||
imageUrl: this.shareImage || '/static/images/share-default.jpg'
|
||||
}
|
||||
},
|
||||
|
||||
// 分享到朋友圈
|
||||
onShareTimeline() {
|
||||
return {
|
||||
title: this.shareTitle || '智慧畜牧管理系统',
|
||||
query: this.shareQuery || '',
|
||||
imageUrl: this.shareImage || '/static/images/share-default.jpg'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
580
mini_program/common/mixins/page.mixin.js
Normal file
580
mini_program/common/mixins/page.mixin.js
Normal file
@@ -0,0 +1,580 @@
|
||||
/**
|
||||
* 页面通用混入
|
||||
* 提供页面级别的通用功能和生命周期管理
|
||||
*/
|
||||
|
||||
import { ref, reactive, onMounted, onUnmounted, onShow, onHide } from 'vue'
|
||||
import { useUserStore } from '@/common/store/modules/user.js'
|
||||
import { createLogger } from '@/common/utils/logger.js'
|
||||
import { permission } from '@/common/utils/permission.js'
|
||||
|
||||
const logger = createLogger('PageMixin')
|
||||
|
||||
/**
|
||||
* 页面基础混入
|
||||
*/
|
||||
export const pageBaseMixin = {
|
||||
setup() {
|
||||
const userStore = useUserStore()
|
||||
const loading = ref(false)
|
||||
const refreshing = ref(false)
|
||||
const pageData = reactive({})
|
||||
|
||||
// 页面加载状态管理
|
||||
const setLoading = (state) => {
|
||||
loading.value = state
|
||||
if (state) {
|
||||
uni.showLoading({ title: '加载中...' })
|
||||
} else {
|
||||
uni.hideLoading()
|
||||
}
|
||||
}
|
||||
|
||||
// 页面刷新状态管理
|
||||
const setRefreshing = (state) => {
|
||||
refreshing.value = state
|
||||
}
|
||||
|
||||
// 显示成功提示
|
||||
const showSuccess = (message, duration = 2000) => {
|
||||
uni.showToast({
|
||||
title: message,
|
||||
icon: 'success',
|
||||
duration
|
||||
})
|
||||
}
|
||||
|
||||
// 显示错误提示
|
||||
const showError = (message, duration = 2000) => {
|
||||
uni.showToast({
|
||||
title: message,
|
||||
icon: 'none',
|
||||
duration
|
||||
})
|
||||
}
|
||||
|
||||
// 显示确认对话框
|
||||
const showConfirm = (options) => {
|
||||
return new Promise((resolve) => {
|
||||
uni.showModal({
|
||||
title: options.title || '提示',
|
||||
content: options.content || '',
|
||||
confirmText: options.confirmText || '确定',
|
||||
cancelText: options.cancelText || '取消',
|
||||
success: (res) => {
|
||||
resolve(res.confirm)
|
||||
},
|
||||
fail: () => {
|
||||
resolve(false)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 页面跳转
|
||||
const navigateTo = (url, params = {}) => {
|
||||
const query = Object.keys(params).length > 0
|
||||
? '?' + Object.keys(params).map(key => `${key}=${encodeURIComponent(params[key])}`).join('&')
|
||||
: ''
|
||||
|
||||
uni.navigateTo({
|
||||
url: url + query,
|
||||
fail: (error) => {
|
||||
logger.error('Navigation failed', { url, params }, error)
|
||||
showError('页面跳转失败')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 页面返回
|
||||
const navigateBack = (delta = 1) => {
|
||||
uni.navigateBack({
|
||||
delta,
|
||||
fail: () => {
|
||||
uni.switchTab({ url: '/pages/index/index' })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 页面重定向
|
||||
const redirectTo = (url, params = {}) => {
|
||||
const query = Object.keys(params).length > 0
|
||||
? '?' + Object.keys(params).map(key => `${key}=${encodeURIComponent(params[key])}`).join('&')
|
||||
: ''
|
||||
|
||||
uni.redirectTo({
|
||||
url: url + query,
|
||||
fail: (error) => {
|
||||
logger.error('Redirect failed', { url, params }, error)
|
||||
showError('页面跳转失败')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 切换到标签页
|
||||
const switchTab = (url) => {
|
||||
uni.switchTab({
|
||||
url,
|
||||
fail: (error) => {
|
||||
logger.error('Switch tab failed', { url }, error)
|
||||
showError('页面切换失败')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 分享页面
|
||||
const sharePage = (options = {}) => {
|
||||
return {
|
||||
title: options.title || '畜牧管理系统',
|
||||
path: options.path || '/pages/index/index',
|
||||
imageUrl: options.imageUrl || ''
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
userStore,
|
||||
loading,
|
||||
refreshing,
|
||||
pageData,
|
||||
setLoading,
|
||||
setRefreshing,
|
||||
showSuccess,
|
||||
showError,
|
||||
showConfirm,
|
||||
navigateTo,
|
||||
navigateBack,
|
||||
redirectTo,
|
||||
switchTab,
|
||||
sharePage
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表页面混入
|
||||
*/
|
||||
export const listPageMixin = {
|
||||
setup() {
|
||||
const list = ref([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(20)
|
||||
const hasMore = ref(true)
|
||||
const searchKeyword = ref('')
|
||||
const filters = reactive({})
|
||||
const sortBy = ref('')
|
||||
const sortOrder = ref('desc')
|
||||
|
||||
// 重置列表
|
||||
const resetList = () => {
|
||||
list.value = []
|
||||
total.value = 0
|
||||
page.value = 1
|
||||
hasMore.value = true
|
||||
}
|
||||
|
||||
// 加载更多
|
||||
const loadMore = async (loadFunction) => {
|
||||
if (!hasMore.value) return
|
||||
|
||||
try {
|
||||
const params = {
|
||||
page: page.value,
|
||||
pageSize: pageSize.value,
|
||||
keyword: searchKeyword.value,
|
||||
sortBy: sortBy.value,
|
||||
sortOrder: sortOrder.value,
|
||||
...filters
|
||||
}
|
||||
|
||||
const response = await loadFunction(params)
|
||||
const newList = response.data.list || []
|
||||
|
||||
if (page.value === 1) {
|
||||
list.value = newList
|
||||
} else {
|
||||
list.value.push(...newList)
|
||||
}
|
||||
|
||||
total.value = response.data.total || 0
|
||||
hasMore.value = newList.length === pageSize.value
|
||||
page.value++
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
logger.error('Load more failed', { page: page.value }, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新列表
|
||||
const refreshList = async (loadFunction) => {
|
||||
resetList()
|
||||
return await loadMore(loadFunction)
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const search = async (keyword, loadFunction) => {
|
||||
searchKeyword.value = keyword
|
||||
return await refreshList(loadFunction)
|
||||
}
|
||||
|
||||
// 筛选
|
||||
const filter = async (filterData, loadFunction) => {
|
||||
Object.assign(filters, filterData)
|
||||
return await refreshList(loadFunction)
|
||||
}
|
||||
|
||||
// 排序
|
||||
const sort = async (field, order, loadFunction) => {
|
||||
sortBy.value = field
|
||||
sortOrder.value = order
|
||||
return await refreshList(loadFunction)
|
||||
}
|
||||
|
||||
return {
|
||||
list,
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
hasMore,
|
||||
searchKeyword,
|
||||
filters,
|
||||
sortBy,
|
||||
sortOrder,
|
||||
resetList,
|
||||
loadMore,
|
||||
refreshList,
|
||||
search,
|
||||
filter,
|
||||
sort
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 表单页面混入
|
||||
*/
|
||||
export const formPageMixin = {
|
||||
setup() {
|
||||
const formData = reactive({})
|
||||
const errors = ref({})
|
||||
const submitting = ref(false)
|
||||
const isDirty = ref(false)
|
||||
|
||||
// 设置表单数据
|
||||
const setFormData = (data) => {
|
||||
Object.assign(formData, data)
|
||||
isDirty.value = false
|
||||
}
|
||||
|
||||
// 更新表单字段
|
||||
const updateField = (field, value) => {
|
||||
formData[field] = value
|
||||
isDirty.value = true
|
||||
|
||||
// 清除该字段的错误
|
||||
if (errors.value[field]) {
|
||||
delete errors.value[field]
|
||||
errors.value = { ...errors.value }
|
||||
}
|
||||
}
|
||||
|
||||
// 设置错误信息
|
||||
const setErrors = (errorData) => {
|
||||
errors.value = errorData
|
||||
}
|
||||
|
||||
// 清除错误信息
|
||||
const clearErrors = () => {
|
||||
errors.value = {}
|
||||
}
|
||||
|
||||
// 验证表单
|
||||
const validateForm = (rules) => {
|
||||
const newErrors = {}
|
||||
|
||||
Object.keys(rules).forEach(field => {
|
||||
const fieldRules = rules[field]
|
||||
const value = formData[field]
|
||||
|
||||
for (const rule of fieldRules) {
|
||||
if (rule.required && (!value || value === '')) {
|
||||
newErrors[field] = rule.message || `${field}不能为空`
|
||||
break
|
||||
}
|
||||
|
||||
if (rule.pattern && value && !rule.pattern.test(value)) {
|
||||
newErrors[field] = rule.message || `${field}格式不正确`
|
||||
break
|
||||
}
|
||||
|
||||
if (rule.minLength && value && value.length < rule.minLength) {
|
||||
newErrors[field] = rule.message || `${field}长度不能少于${rule.minLength}个字符`
|
||||
break
|
||||
}
|
||||
|
||||
if (rule.maxLength && value && value.length > rule.maxLength) {
|
||||
newErrors[field] = rule.message || `${field}长度不能超过${rule.maxLength}个字符`
|
||||
break
|
||||
}
|
||||
|
||||
if (rule.validator && typeof rule.validator === 'function') {
|
||||
const result = rule.validator(value, formData)
|
||||
if (result !== true) {
|
||||
newErrors[field] = result || rule.message || `${field}验证失败`
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
setErrors(newErrors)
|
||||
return Object.keys(newErrors).length === 0
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const submitForm = async (submitFunction, rules = null) => {
|
||||
if (submitting.value) return
|
||||
|
||||
if (rules && !validateForm(rules)) {
|
||||
uni.showToast({
|
||||
title: '请填写正确信息',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
submitting.value = true
|
||||
const result = await submitFunction(formData)
|
||||
isDirty.value = false
|
||||
return result
|
||||
} catch (error) {
|
||||
logger.error('Form submit failed', { formData }, error)
|
||||
throw error
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
Object.keys(formData).forEach(key => {
|
||||
delete formData[key]
|
||||
})
|
||||
clearErrors()
|
||||
isDirty.value = false
|
||||
}
|
||||
|
||||
return {
|
||||
formData,
|
||||
errors,
|
||||
submitting,
|
||||
isDirty,
|
||||
setFormData,
|
||||
updateField,
|
||||
setErrors,
|
||||
clearErrors,
|
||||
validateForm,
|
||||
submitForm,
|
||||
resetForm
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 权限检查混入
|
||||
*/
|
||||
export const permissionMixin = {
|
||||
setup() {
|
||||
// 检查权限
|
||||
const hasPermission = (permissions, operator = 'and') => {
|
||||
return permission.check(permissions, operator)
|
||||
}
|
||||
|
||||
// 检查角色
|
||||
const hasRole = (roles, operator = 'or') => {
|
||||
return permission.checkRole(roles, operator)
|
||||
}
|
||||
|
||||
// 是否为管理员
|
||||
const isAdmin = () => {
|
||||
return permission.checkRole(['super_admin', 'admin'], 'or')
|
||||
}
|
||||
|
||||
// 权限守卫
|
||||
const requirePermission = (permissions, operator = 'and') => {
|
||||
if (!hasPermission(permissions, operator)) {
|
||||
uni.showToast({
|
||||
title: '权限不足',
|
||||
icon: 'none'
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
}, 1500)
|
||||
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
return {
|
||||
hasPermission,
|
||||
hasRole,
|
||||
isAdmin,
|
||||
requirePermission
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 页面生命周期混入
|
||||
*/
|
||||
export const lifecycleMixin = {
|
||||
setup() {
|
||||
const pageStartTime = Date.now()
|
||||
|
||||
onMounted(() => {
|
||||
const loadTime = Date.now() - pageStartTime
|
||||
logger.performance('Page Mount', loadTime)
|
||||
})
|
||||
|
||||
onShow(() => {
|
||||
logger.pageView(getCurrentPages().pop()?.route || 'Unknown')
|
||||
})
|
||||
|
||||
onHide(() => {
|
||||
logger.debug('Page hidden')
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
logger.debug('Page unmounted')
|
||||
})
|
||||
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下拉刷新混入
|
||||
*/
|
||||
export const pullRefreshMixin = {
|
||||
setup() {
|
||||
const refreshing = ref(false)
|
||||
|
||||
// 开始下拉刷新
|
||||
const onPullDownRefresh = async (refreshFunction) => {
|
||||
if (refreshing.value) return
|
||||
|
||||
try {
|
||||
refreshing.value = true
|
||||
await refreshFunction()
|
||||
} catch (error) {
|
||||
logger.error('Pull refresh failed', null, error)
|
||||
uni.showToast({
|
||||
title: '刷新失败',
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
refreshing.value = false
|
||||
uni.stopPullDownRefresh()
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
refreshing,
|
||||
onPullDownRefresh
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 触底加载混入
|
||||
*/
|
||||
export const reachBottomMixin = {
|
||||
setup() {
|
||||
const loading = ref(false)
|
||||
|
||||
// 触底加载更多
|
||||
const onReachBottom = async (loadMoreFunction) => {
|
||||
if (loading.value) return
|
||||
|
||||
try {
|
||||
loading.value = true
|
||||
await loadMoreFunction()
|
||||
} catch (error) {
|
||||
logger.error('Reach bottom load failed', null, error)
|
||||
uni.showToast({
|
||||
title: '加载失败',
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
loading,
|
||||
onReachBottom
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分享功能混入
|
||||
*/
|
||||
export const shareMixin = {
|
||||
setup() {
|
||||
// 分享给朋友
|
||||
const onShareAppMessage = (options = {}) => {
|
||||
const pages = getCurrentPages()
|
||||
const currentPage = pages[pages.length - 1]
|
||||
|
||||
return {
|
||||
title: options.title || '畜牧管理系统',
|
||||
path: options.path || `/${currentPage.route}`,
|
||||
imageUrl: options.imageUrl || ''
|
||||
}
|
||||
}
|
||||
|
||||
// 分享到朋友圈
|
||||
const onShareTimeline = (options = {}) => {
|
||||
return {
|
||||
title: options.title || '畜牧管理系统',
|
||||
imageUrl: options.imageUrl || ''
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
onShareAppMessage,
|
||||
onShareTimeline
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 组合所有混入
|
||||
*/
|
||||
export const commonMixin = {
|
||||
mixins: [
|
||||
pageBaseMixin,
|
||||
permissionMixin,
|
||||
lifecycleMixin,
|
||||
shareMixin
|
||||
]
|
||||
}
|
||||
|
||||
// 导出所有混入
|
||||
export default {
|
||||
pageBaseMixin,
|
||||
listPageMixin,
|
||||
formPageMixin,
|
||||
permissionMixin,
|
||||
lifecycleMixin,
|
||||
pullRefreshMixin,
|
||||
reachBottomMixin,
|
||||
shareMixin,
|
||||
commonMixin
|
||||
}
|
||||
527
mini_program/common/plugins/index.js
Normal file
527
mini_program/common/plugins/index.js
Normal file
@@ -0,0 +1,527 @@
|
||||
/**
|
||||
* 插件系统
|
||||
* 统一管理和注册所有插件
|
||||
*/
|
||||
|
||||
import { createLogger } from '@/common/utils/logger.js'
|
||||
|
||||
const logger = createLogger('Plugins')
|
||||
|
||||
// 插件注册表
|
||||
const plugins = new Map()
|
||||
|
||||
/**
|
||||
* 插件基类
|
||||
*/
|
||||
export class Plugin {
|
||||
constructor(name, options = {}) {
|
||||
this.name = name
|
||||
this.options = options
|
||||
this.installed = false
|
||||
}
|
||||
|
||||
/**
|
||||
* 安装插件
|
||||
* @param {Object} app - 应用实例
|
||||
*/
|
||||
install(app) {
|
||||
if (this.installed) {
|
||||
logger.warn(`Plugin ${this.name} already installed`)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
this._install(app)
|
||||
this.installed = true
|
||||
logger.info(`Plugin ${this.name} installed successfully`)
|
||||
} catch (error) {
|
||||
logger.error(`Failed to install plugin ${this.name}`, null, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 卸载插件
|
||||
* @param {Object} app - 应用实例
|
||||
*/
|
||||
uninstall(app) {
|
||||
if (!this.installed) {
|
||||
logger.warn(`Plugin ${this.name} not installed`)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
this._uninstall(app)
|
||||
this.installed = false
|
||||
logger.info(`Plugin ${this.name} uninstalled successfully`)
|
||||
} catch (error) {
|
||||
logger.error(`Failed to uninstall plugin ${this.name}`, null, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 插件安装逻辑(子类实现)
|
||||
* @protected
|
||||
*/
|
||||
_install(app) {
|
||||
throw new Error(`Plugin ${this.name} must implement _install method`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 插件卸载逻辑(子类实现)
|
||||
* @protected
|
||||
*/
|
||||
_uninstall(app) {
|
||||
// 默认空实现
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求拦截器插件
|
||||
*/
|
||||
export class RequestInterceptorPlugin extends Plugin {
|
||||
constructor(options = {}) {
|
||||
super('RequestInterceptor', options)
|
||||
}
|
||||
|
||||
_install(app) {
|
||||
const { request, response, error } = this.options
|
||||
|
||||
// 请求拦截器
|
||||
if (request) {
|
||||
uni.addInterceptor('request', {
|
||||
invoke: (args) => {
|
||||
logger.debug('Request interceptor invoke', args)
|
||||
return request(args) || args
|
||||
},
|
||||
success: (res) => {
|
||||
logger.debug('Request interceptor success', res)
|
||||
return res
|
||||
},
|
||||
fail: (err) => {
|
||||
logger.error('Request interceptor fail', null, err)
|
||||
if (error) {
|
||||
error(err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 响应拦截器
|
||||
if (response) {
|
||||
uni.addInterceptor('request', {
|
||||
returnValue: (res) => {
|
||||
logger.debug('Response interceptor', res)
|
||||
return response(res) || res
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
_uninstall(app) {
|
||||
uni.removeInterceptor('request')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 路由拦截器插件
|
||||
*/
|
||||
export class RouteInterceptorPlugin extends Plugin {
|
||||
constructor(options = {}) {
|
||||
super('RouteInterceptor', options)
|
||||
}
|
||||
|
||||
_install(app) {
|
||||
const { beforeEach, afterEach } = this.options
|
||||
|
||||
// 页面跳转拦截
|
||||
const routeMethods = ['navigateTo', 'redirectTo', 'switchTab', 'reLaunch', 'navigateBack']
|
||||
|
||||
routeMethods.forEach(method => {
|
||||
uni.addInterceptor(method, {
|
||||
invoke: (args) => {
|
||||
logger.debug(`Route interceptor ${method} invoke`, args)
|
||||
|
||||
if (beforeEach) {
|
||||
const result = beforeEach(args, method)
|
||||
if (result === false) {
|
||||
return false
|
||||
}
|
||||
if (result && typeof result === 'object') {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
return args
|
||||
},
|
||||
success: (res) => {
|
||||
logger.debug(`Route interceptor ${method} success`, res)
|
||||
|
||||
if (afterEach) {
|
||||
afterEach(res, method)
|
||||
}
|
||||
|
||||
return res
|
||||
},
|
||||
fail: (err) => {
|
||||
logger.error(`Route interceptor ${method} fail`, null, err)
|
||||
return err
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
_uninstall(app) {
|
||||
const routeMethods = ['navigateTo', 'redirectTo', 'switchTab', 'reLaunch', 'navigateBack']
|
||||
routeMethods.forEach(method => {
|
||||
uni.removeInterceptor(method)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 全局错误处理插件
|
||||
*/
|
||||
export class ErrorHandlerPlugin extends Plugin {
|
||||
constructor(options = {}) {
|
||||
super('ErrorHandler', options)
|
||||
}
|
||||
|
||||
_install(app) {
|
||||
const { onError, onUnhandledRejection } = this.options
|
||||
|
||||
// 全局错误处理
|
||||
if (onError) {
|
||||
uni.onError((error) => {
|
||||
logger.error('Global error', null, new Error(error))
|
||||
onError(error)
|
||||
})
|
||||
}
|
||||
|
||||
// 未处理的 Promise 拒绝
|
||||
if (onUnhandledRejection) {
|
||||
uni.onUnhandledRejection((event) => {
|
||||
logger.error('Unhandled promise rejection', { reason: event.reason })
|
||||
onUnhandledRejection(event)
|
||||
})
|
||||
}
|
||||
|
||||
// 页面错误处理
|
||||
const originalOnError = app.config.errorHandler
|
||||
app.config.errorHandler = (error, instance, info) => {
|
||||
logger.error('Vue error', { info }, error)
|
||||
|
||||
if (onError) {
|
||||
onError(error, instance, info)
|
||||
}
|
||||
|
||||
if (originalOnError) {
|
||||
originalOnError(error, instance, info)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 性能监控插件
|
||||
*/
|
||||
export class PerformancePlugin extends Plugin {
|
||||
constructor(options = {}) {
|
||||
super('Performance', options)
|
||||
}
|
||||
|
||||
_install(app) {
|
||||
const { enabled = true, sampleRate = 0.1 } = this.options
|
||||
|
||||
if (!enabled || Math.random() > sampleRate) {
|
||||
return
|
||||
}
|
||||
|
||||
// 页面性能监控
|
||||
const pageStartTimes = new Map()
|
||||
|
||||
// 监控页面加载时间
|
||||
uni.addInterceptor('navigateTo', {
|
||||
invoke: (args) => {
|
||||
pageStartTimes.set(args.url, Date.now())
|
||||
return args
|
||||
}
|
||||
})
|
||||
|
||||
// 监控页面渲染完成时间
|
||||
const originalOnReady = app.config.globalProperties.$mp?.onReady
|
||||
if (originalOnReady) {
|
||||
app.config.globalProperties.$mp.onReady = function() {
|
||||
const currentPage = getCurrentPages().pop()
|
||||
const startTime = pageStartTimes.get(currentPage?.route)
|
||||
|
||||
if (startTime) {
|
||||
const loadTime = Date.now() - startTime
|
||||
logger.performance('Page Load', loadTime, {
|
||||
page: currentPage?.route
|
||||
})
|
||||
pageStartTimes.delete(currentPage?.route)
|
||||
}
|
||||
|
||||
return originalOnReady.call(this)
|
||||
}
|
||||
}
|
||||
|
||||
// 监控 API 请求性能
|
||||
uni.addInterceptor('request', {
|
||||
invoke: (args) => {
|
||||
args._startTime = Date.now()
|
||||
return args
|
||||
},
|
||||
success: (res) => {
|
||||
if (res.config && res.config._startTime) {
|
||||
const duration = Date.now() - res.config._startTime
|
||||
logger.performance('API Request', duration, {
|
||||
url: res.config.url,
|
||||
method: res.config.method,
|
||||
status: res.statusCode
|
||||
})
|
||||
}
|
||||
return res
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存插件
|
||||
*/
|
||||
export class CachePlugin extends Plugin {
|
||||
constructor(options = {}) {
|
||||
super('Cache', options)
|
||||
}
|
||||
|
||||
_install(app) {
|
||||
const {
|
||||
defaultExpire = 24 * 60 * 60 * 1000, // 24小时
|
||||
maxSize = 50 * 1024 * 1024, // 50MB
|
||||
cleanupInterval = 60 * 60 * 1000 // 1小时
|
||||
} = this.options
|
||||
|
||||
const cache = new Map()
|
||||
let currentSize = 0
|
||||
|
||||
// 缓存管理器
|
||||
const cacheManager = {
|
||||
set(key, value, expire = defaultExpire) {
|
||||
const item = {
|
||||
value,
|
||||
expire: Date.now() + expire,
|
||||
size: JSON.stringify(value).length
|
||||
}
|
||||
|
||||
// 检查缓存大小
|
||||
if (currentSize + item.size > maxSize) {
|
||||
this.cleanup()
|
||||
}
|
||||
|
||||
// 如果还是超出大小限制,删除最旧的项
|
||||
while (currentSize + item.size > maxSize && cache.size > 0) {
|
||||
const oldestKey = cache.keys().next().value
|
||||
this.delete(oldestKey)
|
||||
}
|
||||
|
||||
cache.set(key, item)
|
||||
currentSize += item.size
|
||||
|
||||
logger.debug('Cache set', { key, size: item.size, expire })
|
||||
},
|
||||
|
||||
get(key) {
|
||||
const item = cache.get(key)
|
||||
|
||||
if (!item) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (Date.now() > item.expire) {
|
||||
this.delete(key)
|
||||
return null
|
||||
}
|
||||
|
||||
logger.debug('Cache hit', { key })
|
||||
return item.value
|
||||
},
|
||||
|
||||
delete(key) {
|
||||
const item = cache.get(key)
|
||||
if (item) {
|
||||
cache.delete(key)
|
||||
currentSize -= item.size
|
||||
logger.debug('Cache delete', { key, size: item.size })
|
||||
}
|
||||
},
|
||||
|
||||
clear() {
|
||||
cache.clear()
|
||||
currentSize = 0
|
||||
logger.debug('Cache cleared')
|
||||
},
|
||||
|
||||
cleanup() {
|
||||
const now = Date.now()
|
||||
const keysToDelete = []
|
||||
|
||||
for (const [key, item] of cache) {
|
||||
if (now > item.expire) {
|
||||
keysToDelete.push(key)
|
||||
}
|
||||
}
|
||||
|
||||
keysToDelete.forEach(key => this.delete(key))
|
||||
logger.debug('Cache cleanup', { deleted: keysToDelete.length })
|
||||
},
|
||||
|
||||
size() {
|
||||
return cache.size
|
||||
},
|
||||
|
||||
memoryUsage() {
|
||||
return currentSize
|
||||
}
|
||||
}
|
||||
|
||||
// 定期清理过期缓存
|
||||
setInterval(() => {
|
||||
cacheManager.cleanup()
|
||||
}, cleanupInterval)
|
||||
|
||||
// 将缓存管理器添加到全局
|
||||
app.config.globalProperties.$cache = cacheManager
|
||||
|
||||
// 也可以通过 uni 访问
|
||||
uni.$cache = cacheManager
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 插件管理器
|
||||
*/
|
||||
export class PluginManager {
|
||||
constructor() {
|
||||
this.plugins = new Map()
|
||||
this.app = null
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册插件
|
||||
* @param {Plugin} plugin - 插件实例
|
||||
*/
|
||||
register(plugin) {
|
||||
if (!(plugin instanceof Plugin)) {
|
||||
throw new Error('Plugin must be an instance of Plugin class')
|
||||
}
|
||||
|
||||
if (this.plugins.has(plugin.name)) {
|
||||
logger.warn(`Plugin ${plugin.name} already registered`)
|
||||
return
|
||||
}
|
||||
|
||||
this.plugins.set(plugin.name, plugin)
|
||||
logger.info(`Plugin ${plugin.name} registered`)
|
||||
|
||||
// 如果应用已经初始化,立即安装插件
|
||||
if (this.app) {
|
||||
plugin.install(this.app)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 卸载插件
|
||||
* @param {string} name - 插件名称
|
||||
*/
|
||||
unregister(name) {
|
||||
const plugin = this.plugins.get(name)
|
||||
if (!plugin) {
|
||||
logger.warn(`Plugin ${name} not found`)
|
||||
return
|
||||
}
|
||||
|
||||
if (plugin.installed && this.app) {
|
||||
plugin.uninstall(this.app)
|
||||
}
|
||||
|
||||
this.plugins.delete(name)
|
||||
logger.info(`Plugin ${name} unregistered`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取插件
|
||||
* @param {string} name - 插件名称
|
||||
* @returns {Plugin|null} 插件实例
|
||||
*/
|
||||
get(name) {
|
||||
return this.plugins.get(name) || null
|
||||
}
|
||||
|
||||
/**
|
||||
* 安装所有插件
|
||||
* @param {Object} app - 应用实例
|
||||
*/
|
||||
installAll(app) {
|
||||
this.app = app
|
||||
|
||||
for (const plugin of this.plugins.values()) {
|
||||
if (!plugin.installed) {
|
||||
plugin.install(app)
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`Installed ${this.plugins.size} plugins`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 卸载所有插件
|
||||
*/
|
||||
uninstallAll() {
|
||||
for (const plugin of this.plugins.values()) {
|
||||
if (plugin.installed && this.app) {
|
||||
plugin.uninstall(this.app)
|
||||
}
|
||||
}
|
||||
|
||||
logger.info('All plugins uninstalled')
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取已安装的插件列表
|
||||
* @returns {Array} 插件名称列表
|
||||
*/
|
||||
getInstalledPlugins() {
|
||||
return Array.from(this.plugins.values())
|
||||
.filter(plugin => plugin.installed)
|
||||
.map(plugin => plugin.name)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有插件列表
|
||||
* @returns {Array} 插件名称列表
|
||||
*/
|
||||
getAllPlugins() {
|
||||
return Array.from(this.plugins.keys())
|
||||
}
|
||||
}
|
||||
|
||||
// 创建全局插件管理器实例
|
||||
const pluginManager = new PluginManager()
|
||||
|
||||
// 导出插件管理器和插件类
|
||||
export {
|
||||
pluginManager,
|
||||
Plugin,
|
||||
RequestInterceptorPlugin,
|
||||
RouteInterceptorPlugin,
|
||||
ErrorHandlerPlugin,
|
||||
PerformancePlugin,
|
||||
CachePlugin
|
||||
}
|
||||
|
||||
// 导出默认插件管理器
|
||||
export default pluginManager
|
||||
38
mini_program/common/store/index.js
Normal file
38
mini_program/common/store/index.js
Normal file
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Pinia 状态管理入口文件
|
||||
* 统一管理所有的 store 模块
|
||||
*/
|
||||
|
||||
import { createPinia } from 'pinia'
|
||||
import { createPersistedState } from 'pinia-plugin-persistedstate'
|
||||
|
||||
// 创建 pinia 实例
|
||||
const pinia = createPinia()
|
||||
|
||||
// 添加持久化插件
|
||||
pinia.use(
|
||||
createPersistedState({
|
||||
storage: {
|
||||
getItem: (key) => {
|
||||
return uni.getStorageSync(key)
|
||||
},
|
||||
setItem: (key, value) => {
|
||||
uni.setStorageSync(key, value)
|
||||
},
|
||||
removeItem: (key) => {
|
||||
uni.removeStorageSync(key)
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
export default pinia
|
||||
|
||||
// 导出所有 store 模块
|
||||
export { useUserStore } from './modules/user'
|
||||
export { useAppStore } from './modules/app'
|
||||
export { useFarmingStore } from './modules/farming'
|
||||
export { useTradingStore } from './modules/trading'
|
||||
export { useMallStore } from './modules/mall'
|
||||
export { useFinanceStore } from './modules/finance'
|
||||
export { useInsuranceStore } from './modules/insurance'
|
||||
556
mini_program/common/store/modules/app.js
Normal file
556
mini_program/common/store/modules/app.js
Normal file
@@ -0,0 +1,556 @@
|
||||
/**
|
||||
* 应用状态管理模块
|
||||
* 管理应用全局状态、配置、系统信息等
|
||||
*/
|
||||
|
||||
import { defineStore } from 'pinia'
|
||||
import { APP_CONFIG } from '@/common/config'
|
||||
|
||||
export const useAppStore = defineStore('app', {
|
||||
state: () => ({
|
||||
// 应用信息
|
||||
appInfo: {
|
||||
name: APP_CONFIG.name,
|
||||
version: APP_CONFIG.version,
|
||||
description: APP_CONFIG.description
|
||||
},
|
||||
|
||||
// 系统信息
|
||||
systemInfo: null,
|
||||
|
||||
// 网络状态
|
||||
networkType: 'unknown',
|
||||
isOnline: true,
|
||||
|
||||
// 应用状态
|
||||
isLoading: false,
|
||||
loadingText: '加载中...',
|
||||
|
||||
// 主题设置
|
||||
theme: 'light',
|
||||
|
||||
// 语言设置
|
||||
language: 'zh-CN',
|
||||
|
||||
// 页面栈信息
|
||||
pageStack: [],
|
||||
|
||||
// 全局配置
|
||||
globalConfig: {
|
||||
showTabBar: true,
|
||||
enablePullRefresh: true,
|
||||
enableReachBottom: true,
|
||||
backgroundColor: '#f5f5f5'
|
||||
},
|
||||
|
||||
// 缓存管理
|
||||
cache: {
|
||||
maxSize: APP_CONFIG.cache.maxSize,
|
||||
currentSize: 0,
|
||||
items: new Map()
|
||||
},
|
||||
|
||||
// 错误信息
|
||||
errors: [],
|
||||
|
||||
// 通知消息
|
||||
notifications: [],
|
||||
|
||||
// 更新信息
|
||||
updateInfo: {
|
||||
hasUpdate: false,
|
||||
version: null,
|
||||
description: null,
|
||||
forceUpdate: false
|
||||
}
|
||||
}),
|
||||
|
||||
getters: {
|
||||
// 获取当前页面
|
||||
currentPage: (state) => {
|
||||
const pages = getCurrentPages()
|
||||
return pages[pages.length - 1]
|
||||
},
|
||||
|
||||
// 获取页面栈深度
|
||||
pageStackDepth: (state) => {
|
||||
return getCurrentPages().length
|
||||
},
|
||||
|
||||
// 检查是否为首页
|
||||
isHomePage: (state) => {
|
||||
const pages = getCurrentPages()
|
||||
return pages.length === 1
|
||||
},
|
||||
|
||||
// 获取设备信息
|
||||
deviceInfo: (state) => {
|
||||
if (!state.systemInfo) return null
|
||||
|
||||
return {
|
||||
platform: state.systemInfo.platform,
|
||||
system: state.systemInfo.system,
|
||||
model: state.systemInfo.model,
|
||||
brand: state.systemInfo.brand,
|
||||
screenWidth: state.systemInfo.screenWidth,
|
||||
screenHeight: state.systemInfo.screenHeight,
|
||||
windowWidth: state.systemInfo.windowWidth,
|
||||
windowHeight: state.systemInfo.windowHeight,
|
||||
statusBarHeight: state.systemInfo.statusBarHeight,
|
||||
safeArea: state.systemInfo.safeArea
|
||||
}
|
||||
},
|
||||
|
||||
// 检查是否为移动设备
|
||||
isMobile: (state) => {
|
||||
if (!state.systemInfo) return true
|
||||
return state.systemInfo.platform !== 'devtools'
|
||||
},
|
||||
|
||||
// 检查是否为开发工具
|
||||
isDevTools: (state) => {
|
||||
if (!state.systemInfo) return false
|
||||
return state.systemInfo.platform === 'devtools'
|
||||
},
|
||||
|
||||
// 获取缓存使用率
|
||||
cacheUsageRate: (state) => {
|
||||
return (state.cache.currentSize / state.cache.maxSize * 100).toFixed(2)
|
||||
},
|
||||
|
||||
// 检查缓存是否已满
|
||||
isCacheFull: (state) => {
|
||||
return state.cache.currentSize >= state.cache.maxSize
|
||||
},
|
||||
|
||||
// 获取未读通知数量
|
||||
unreadNotificationCount: (state) => {
|
||||
return state.notifications.filter(n => !n.read).length
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
/**
|
||||
* 初始化应用
|
||||
*/
|
||||
async initApp() {
|
||||
try {
|
||||
// 获取系统信息
|
||||
await this.getSystemInfo()
|
||||
|
||||
// 获取网络状态
|
||||
await this.getNetworkType()
|
||||
|
||||
// 监听网络状态变化
|
||||
this.watchNetworkStatus()
|
||||
|
||||
// 检查应用更新
|
||||
await this.checkUpdate()
|
||||
|
||||
console.log('应用初始化完成')
|
||||
} catch (error) {
|
||||
console.error('应用初始化失败:', error)
|
||||
this.addError('应用初始化失败', error)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取系统信息
|
||||
*/
|
||||
async getSystemInfo() {
|
||||
try {
|
||||
const systemInfo = await new Promise((resolve, reject) => {
|
||||
uni.getSystemInfo({
|
||||
success: resolve,
|
||||
fail: reject
|
||||
})
|
||||
})
|
||||
|
||||
this.systemInfo = systemInfo
|
||||
return systemInfo
|
||||
} catch (error) {
|
||||
console.error('获取系统信息失败:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取网络类型
|
||||
*/
|
||||
async getNetworkType() {
|
||||
try {
|
||||
const networkInfo = await new Promise((resolve, reject) => {
|
||||
uni.getNetworkType({
|
||||
success: resolve,
|
||||
fail: reject
|
||||
})
|
||||
})
|
||||
|
||||
this.networkType = networkInfo.networkType
|
||||
this.isOnline = networkInfo.networkType !== 'none'
|
||||
|
||||
return networkInfo
|
||||
} catch (error) {
|
||||
console.error('获取网络状态失败:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 监听网络状态变化
|
||||
*/
|
||||
watchNetworkStatus() {
|
||||
uni.onNetworkStatusChange((res) => {
|
||||
this.networkType = res.networkType
|
||||
this.isOnline = res.isConnected
|
||||
|
||||
if (!res.isConnected) {
|
||||
this.showToast('网络连接已断开', 'error')
|
||||
} else {
|
||||
this.showToast('网络连接已恢复', 'success')
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 显示加载状态
|
||||
* @param {string} text 加载文本
|
||||
*/
|
||||
showLoading(text = '加载中...') {
|
||||
this.isLoading = true
|
||||
this.loadingText = text
|
||||
|
||||
uni.showLoading({
|
||||
title: text,
|
||||
mask: true
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 隐藏加载状态
|
||||
*/
|
||||
hideLoading() {
|
||||
this.isLoading = false
|
||||
uni.hideLoading()
|
||||
},
|
||||
|
||||
/**
|
||||
* 显示提示消息
|
||||
* @param {string} title 提示内容
|
||||
* @param {string} icon 图标类型
|
||||
* @param {number} duration 显示时长
|
||||
*/
|
||||
showToast(title, icon = 'none', duration = 2000) {
|
||||
uni.showToast({
|
||||
title,
|
||||
icon,
|
||||
duration,
|
||||
mask: false
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 显示模态对话框
|
||||
* @param {Object} options 对话框选项
|
||||
*/
|
||||
async showModal(options) {
|
||||
const defaultOptions = {
|
||||
title: '提示',
|
||||
content: '',
|
||||
showCancel: true,
|
||||
cancelText: '取消',
|
||||
confirmText: '确定'
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
uni.showModal({
|
||||
...defaultOptions,
|
||||
...options,
|
||||
success: resolve,
|
||||
fail: resolve
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 设置主题
|
||||
* @param {string} theme 主题名称
|
||||
*/
|
||||
setTheme(theme) {
|
||||
this.theme = theme
|
||||
|
||||
// 应用主题样式
|
||||
const themeClass = `theme-${theme}`
|
||||
const body = document.body || document.documentElement
|
||||
|
||||
// 移除旧主题类
|
||||
body.classList.remove('theme-light', 'theme-dark')
|
||||
// 添加新主题类
|
||||
body.classList.add(themeClass)
|
||||
|
||||
// 保存到本地存储
|
||||
uni.setStorageSync('app-theme', theme)
|
||||
},
|
||||
|
||||
/**
|
||||
* 设置语言
|
||||
* @param {string} language 语言代码
|
||||
*/
|
||||
setLanguage(language) {
|
||||
this.language = language
|
||||
|
||||
// 保存到本地存储
|
||||
uni.setStorageSync('app-language', language)
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新页面栈信息
|
||||
*/
|
||||
updatePageStack() {
|
||||
const pages = getCurrentPages()
|
||||
this.pageStack = pages.map(page => ({
|
||||
route: page.route,
|
||||
options: page.options
|
||||
}))
|
||||
},
|
||||
|
||||
/**
|
||||
* 添加缓存项
|
||||
* @param {string} key 缓存键
|
||||
* @param {any} value 缓存值
|
||||
* @param {number} expire 过期时间(毫秒)
|
||||
*/
|
||||
setCache(key, value, expire = APP_CONFIG.cache.defaultExpire) {
|
||||
const item = {
|
||||
value,
|
||||
expire: Date.now() + expire,
|
||||
size: JSON.stringify(value).length
|
||||
}
|
||||
|
||||
// 检查缓存空间
|
||||
if (this.cache.currentSize + item.size > this.cache.maxSize) {
|
||||
this.clearExpiredCache()
|
||||
|
||||
// 如果还是不够空间,清除最旧的缓存
|
||||
if (this.cache.currentSize + item.size > this.cache.maxSize) {
|
||||
this.clearOldestCache()
|
||||
}
|
||||
}
|
||||
|
||||
// 如果已存在,先移除旧的
|
||||
if (this.cache.items.has(key)) {
|
||||
const oldItem = this.cache.items.get(key)
|
||||
this.cache.currentSize -= oldItem.size
|
||||
}
|
||||
|
||||
// 添加新缓存
|
||||
this.cache.items.set(key, item)
|
||||
this.cache.currentSize += item.size
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取缓存项
|
||||
* @param {string} key 缓存键
|
||||
*/
|
||||
getCache(key) {
|
||||
const item = this.cache.items.get(key)
|
||||
|
||||
if (!item) return null
|
||||
|
||||
// 检查是否过期
|
||||
if (Date.now() > item.expire) {
|
||||
this.removeCache(key)
|
||||
return null
|
||||
}
|
||||
|
||||
return item.value
|
||||
},
|
||||
|
||||
/**
|
||||
* 移除缓存项
|
||||
* @param {string} key 缓存键
|
||||
*/
|
||||
removeCache(key) {
|
||||
const item = this.cache.items.get(key)
|
||||
if (item) {
|
||||
this.cache.items.delete(key)
|
||||
this.cache.currentSize -= item.size
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 清除过期缓存
|
||||
*/
|
||||
clearExpiredCache() {
|
||||
const now = Date.now()
|
||||
const expiredKeys = []
|
||||
|
||||
for (const [key, item] of this.cache.items) {
|
||||
if (now > item.expire) {
|
||||
expiredKeys.push(key)
|
||||
}
|
||||
}
|
||||
|
||||
expiredKeys.forEach(key => this.removeCache(key))
|
||||
},
|
||||
|
||||
/**
|
||||
* 清除最旧的缓存
|
||||
*/
|
||||
clearOldestCache() {
|
||||
if (this.cache.items.size === 0) return
|
||||
|
||||
const oldestKey = this.cache.items.keys().next().value
|
||||
this.removeCache(oldestKey)
|
||||
},
|
||||
|
||||
/**
|
||||
* 清除所有缓存
|
||||
*/
|
||||
clearAllCache() {
|
||||
this.cache.items.clear()
|
||||
this.cache.currentSize = 0
|
||||
},
|
||||
|
||||
/**
|
||||
* 添加错误信息
|
||||
* @param {string} message 错误消息
|
||||
* @param {Error} error 错误对象
|
||||
*/
|
||||
addError(message, error = null) {
|
||||
const errorInfo = {
|
||||
id: Date.now(),
|
||||
message,
|
||||
error: error ? error.toString() : null,
|
||||
stack: error ? error.stack : null,
|
||||
timestamp: new Date().toISOString(),
|
||||
url: this.currentPage?.route || 'unknown'
|
||||
}
|
||||
|
||||
this.errors.unshift(errorInfo)
|
||||
|
||||
// 限制错误数量
|
||||
if (this.errors.length > 100) {
|
||||
this.errors = this.errors.slice(0, 100)
|
||||
}
|
||||
|
||||
console.error('应用错误:', errorInfo)
|
||||
},
|
||||
|
||||
/**
|
||||
* 清除错误信息
|
||||
*/
|
||||
clearErrors() {
|
||||
this.errors = []
|
||||
},
|
||||
|
||||
/**
|
||||
* 添加通知
|
||||
* @param {Object} notification 通知对象
|
||||
*/
|
||||
addNotification(notification) {
|
||||
const notificationInfo = {
|
||||
id: Date.now(),
|
||||
title: notification.title,
|
||||
content: notification.content,
|
||||
type: notification.type || 'info',
|
||||
read: false,
|
||||
timestamp: new Date().toISOString(),
|
||||
...notification
|
||||
}
|
||||
|
||||
this.notifications.unshift(notificationInfo)
|
||||
|
||||
// 限制通知数量
|
||||
if (this.notifications.length > 50) {
|
||||
this.notifications = this.notifications.slice(0, 50)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 标记通知为已读
|
||||
* @param {number} id 通知ID
|
||||
*/
|
||||
markNotificationAsRead(id) {
|
||||
const notification = this.notifications.find(n => n.id === id)
|
||||
if (notification) {
|
||||
notification.read = true
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 清除通知
|
||||
* @param {number} id 通知ID
|
||||
*/
|
||||
removeNotification(id) {
|
||||
const index = this.notifications.findIndex(n => n.id === id)
|
||||
if (index > -1) {
|
||||
this.notifications.splice(index, 1)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 清除所有通知
|
||||
*/
|
||||
clearNotifications() {
|
||||
this.notifications = []
|
||||
},
|
||||
|
||||
/**
|
||||
* 检查应用更新
|
||||
*/
|
||||
async checkUpdate() {
|
||||
try {
|
||||
// #ifdef MP-WEIXIN
|
||||
const updateManager = uni.getUpdateManager()
|
||||
|
||||
updateManager.onCheckForUpdate((res) => {
|
||||
if (res.hasUpdate) {
|
||||
this.updateInfo.hasUpdate = true
|
||||
console.log('发现新版本')
|
||||
}
|
||||
})
|
||||
|
||||
updateManager.onUpdateReady(() => {
|
||||
this.showModal({
|
||||
title: '更新提示',
|
||||
content: '新版本已准备好,是否重启应用?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
updateManager.applyUpdate()
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
updateManager.onUpdateFailed(() => {
|
||||
this.showToast('更新失败,请稍后重试', 'error')
|
||||
})
|
||||
// #endif
|
||||
} catch (error) {
|
||||
console.error('检查更新失败:', error)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 重置应用状态
|
||||
*/
|
||||
resetApp() {
|
||||
this.$reset()
|
||||
this.clearAllCache()
|
||||
this.clearErrors()
|
||||
this.clearNotifications()
|
||||
}
|
||||
},
|
||||
|
||||
// 持久化配置
|
||||
persist: {
|
||||
key: 'app-store',
|
||||
storage: {
|
||||
getItem: (key) => uni.getStorageSync(key),
|
||||
setItem: (key, value) => uni.setStorageSync(key, value),
|
||||
removeItem: (key) => uni.removeStorageSync(key)
|
||||
},
|
||||
paths: ['theme', 'language', 'globalConfig']
|
||||
}
|
||||
})
|
||||
581
mini_program/common/store/modules/farming.js
Normal file
581
mini_program/common/store/modules/farming.js
Normal file
@@ -0,0 +1,581 @@
|
||||
/**
|
||||
* 养殖管理状态管理模块
|
||||
* 管理养殖相关的数据和状态
|
||||
*/
|
||||
|
||||
import { defineStore } from 'pinia'
|
||||
import { farmingApi } from '@/common/api/farming'
|
||||
|
||||
export const useFarmingStore = defineStore('farming', {
|
||||
state: () => ({
|
||||
// 养殖场列表
|
||||
farmList: [],
|
||||
|
||||
// 当前选中的养殖场
|
||||
currentFarm: null,
|
||||
|
||||
// 牲畜列表
|
||||
livestockList: [],
|
||||
|
||||
// 当前选中的牲畜
|
||||
currentLivestock: null,
|
||||
|
||||
// 养殖记录
|
||||
records: [],
|
||||
|
||||
// 健康监测数据
|
||||
healthData: [],
|
||||
|
||||
// 饲料管理数据
|
||||
feedData: [],
|
||||
|
||||
// 疫苗管理数据
|
||||
vaccineData: [],
|
||||
|
||||
// 统计数据
|
||||
statistics: {
|
||||
totalFarms: 0,
|
||||
totalLivestock: 0,
|
||||
healthyCount: 0,
|
||||
sickCount: 0,
|
||||
feedConsumption: 0,
|
||||
vaccineCount: 0
|
||||
},
|
||||
|
||||
// 加载状态
|
||||
loading: {
|
||||
farmList: false,
|
||||
livestockList: false,
|
||||
records: false,
|
||||
healthData: false,
|
||||
feedData: false,
|
||||
vaccineData: false
|
||||
},
|
||||
|
||||
// 分页信息
|
||||
pagination: {
|
||||
farmList: { page: 1, size: 20, total: 0 },
|
||||
livestockList: { page: 1, size: 20, total: 0 },
|
||||
records: { page: 1, size: 20, total: 0 }
|
||||
},
|
||||
|
||||
// 筛选条件
|
||||
filters: {
|
||||
farmType: '',
|
||||
livestockType: '',
|
||||
healthStatus: '',
|
||||
dateRange: []
|
||||
}
|
||||
}),
|
||||
|
||||
getters: {
|
||||
// 获取健康牲畜列表
|
||||
healthyLivestock: (state) => {
|
||||
return state.livestockList.filter(item => item.healthStatus === 'healthy')
|
||||
},
|
||||
|
||||
// 获取生病牲畜列表
|
||||
sickLivestock: (state) => {
|
||||
return state.livestockList.filter(item => item.healthStatus === 'sick')
|
||||
},
|
||||
|
||||
// 获取需要疫苗的牲畜列表
|
||||
needVaccineLivestock: (state) => {
|
||||
return state.livestockList.filter(item => {
|
||||
const lastVaccine = item.lastVaccineDate
|
||||
if (!lastVaccine) return true
|
||||
|
||||
const daysSinceVaccine = (Date.now() - new Date(lastVaccine).getTime()) / (1000 * 60 * 60 * 24)
|
||||
return daysSinceVaccine > 180 // 6个月
|
||||
})
|
||||
},
|
||||
|
||||
// 获取今日饲料消耗
|
||||
todayFeedConsumption: (state) => {
|
||||
const today = new Date().toDateString()
|
||||
return state.feedData
|
||||
.filter(item => new Date(item.date).toDateString() === today)
|
||||
.reduce((total, item) => total + item.amount, 0)
|
||||
},
|
||||
|
||||
// 获取本月疫苗接种数量
|
||||
monthlyVaccineCount: (state) => {
|
||||
const currentMonth = new Date().getMonth()
|
||||
const currentYear = new Date().getFullYear()
|
||||
|
||||
return state.vaccineData.filter(item => {
|
||||
const vaccineDate = new Date(item.date)
|
||||
return vaccineDate.getMonth() === currentMonth &&
|
||||
vaccineDate.getFullYear() === currentYear
|
||||
}).length
|
||||
},
|
||||
|
||||
// 获取养殖场类型统计
|
||||
farmTypeStats: (state) => {
|
||||
const stats = {}
|
||||
state.farmList.forEach(farm => {
|
||||
stats[farm.type] = (stats[farm.type] || 0) + 1
|
||||
})
|
||||
return stats
|
||||
},
|
||||
|
||||
// 获取牲畜类型统计
|
||||
livestockTypeStats: (state) => {
|
||||
const stats = {}
|
||||
state.livestockList.forEach(livestock => {
|
||||
stats[livestock.type] = (stats[livestock.type] || 0) + 1
|
||||
})
|
||||
return stats
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
/**
|
||||
* 获取养殖场列表
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
async fetchFarmList(params = {}) {
|
||||
this.loading.farmList = true
|
||||
|
||||
try {
|
||||
const response = await farmingApi.getFarmList({
|
||||
page: this.pagination.farmList.page,
|
||||
size: this.pagination.farmList.size,
|
||||
...params
|
||||
})
|
||||
|
||||
const { data, total } = response.data
|
||||
|
||||
if (params.page === 1) {
|
||||
this.farmList = data
|
||||
} else {
|
||||
this.farmList.push(...data)
|
||||
}
|
||||
|
||||
this.pagination.farmList.total = total
|
||||
this.statistics.totalFarms = total
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取养殖场列表失败:', error)
|
||||
throw error
|
||||
} finally {
|
||||
this.loading.farmList = false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取牲畜列表
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
async fetchLivestockList(params = {}) {
|
||||
this.loading.livestockList = true
|
||||
|
||||
try {
|
||||
const response = await farmingApi.getLivestockList({
|
||||
page: this.pagination.livestockList.page,
|
||||
size: this.pagination.livestockList.size,
|
||||
farmId: this.currentFarm?.id,
|
||||
...params
|
||||
})
|
||||
|
||||
const { data, total } = response.data
|
||||
|
||||
if (params.page === 1) {
|
||||
this.livestockList = data
|
||||
} else {
|
||||
this.livestockList.push(...data)
|
||||
}
|
||||
|
||||
this.pagination.livestockList.total = total
|
||||
this.statistics.totalLivestock = total
|
||||
|
||||
// 更新健康统计
|
||||
this.updateHealthStats()
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取牲畜列表失败:', error)
|
||||
throw error
|
||||
} finally {
|
||||
this.loading.livestockList = false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取养殖记录
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
async fetchRecords(params = {}) {
|
||||
this.loading.records = true
|
||||
|
||||
try {
|
||||
const response = await farmingApi.getRecords({
|
||||
page: this.pagination.records.page,
|
||||
size: this.pagination.records.size,
|
||||
...params
|
||||
})
|
||||
|
||||
const { data, total } = response.data
|
||||
|
||||
if (params.page === 1) {
|
||||
this.records = data
|
||||
} else {
|
||||
this.records.push(...data)
|
||||
}
|
||||
|
||||
this.pagination.records.total = total
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取养殖记录失败:', error)
|
||||
throw error
|
||||
} finally {
|
||||
this.loading.records = false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取健康监测数据
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
async fetchHealthData(params = {}) {
|
||||
this.loading.healthData = true
|
||||
|
||||
try {
|
||||
const response = await farmingApi.getHealthData(params)
|
||||
this.healthData = response.data
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取健康监测数据失败:', error)
|
||||
throw error
|
||||
} finally {
|
||||
this.loading.healthData = false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取饲料数据
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
async fetchFeedData(params = {}) {
|
||||
this.loading.feedData = true
|
||||
|
||||
try {
|
||||
const response = await farmingApi.getFeedData(params)
|
||||
this.feedData = response.data
|
||||
|
||||
// 更新饲料消耗统计
|
||||
this.updateFeedStats()
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取饲料数据失败:', error)
|
||||
throw error
|
||||
} finally {
|
||||
this.loading.feedData = false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取疫苗数据
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
async fetchVaccineData(params = {}) {
|
||||
this.loading.vaccineData = true
|
||||
|
||||
try {
|
||||
const response = await farmingApi.getVaccineData(params)
|
||||
this.vaccineData = response.data
|
||||
|
||||
// 更新疫苗统计
|
||||
this.updateVaccineStats()
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取疫苗数据失败:', error)
|
||||
throw error
|
||||
} finally {
|
||||
this.loading.vaccineData = false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 添加养殖场
|
||||
* @param {Object} farmData 养殖场数据
|
||||
*/
|
||||
async addFarm(farmData) {
|
||||
try {
|
||||
const response = await farmingApi.createFarm(farmData)
|
||||
const newFarm = response.data
|
||||
|
||||
this.farmList.unshift(newFarm)
|
||||
this.statistics.totalFarms++
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('添加养殖场失败:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新养殖场
|
||||
* @param {number} id 养殖场ID
|
||||
* @param {Object} farmData 养殖场数据
|
||||
*/
|
||||
async updateFarm(id, farmData) {
|
||||
try {
|
||||
const response = await farmingApi.updateFarm(id, farmData)
|
||||
const updatedFarm = response.data
|
||||
|
||||
const index = this.farmList.findIndex(farm => farm.id === id)
|
||||
if (index > -1) {
|
||||
this.farmList[index] = updatedFarm
|
||||
}
|
||||
|
||||
if (this.currentFarm?.id === id) {
|
||||
this.currentFarm = updatedFarm
|
||||
}
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('更新养殖场失败:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 删除养殖场
|
||||
* @param {number} id 养殖场ID
|
||||
*/
|
||||
async deleteFarm(id) {
|
||||
try {
|
||||
const response = await farmingApi.deleteFarm(id)
|
||||
|
||||
const index = this.farmList.findIndex(farm => farm.id === id)
|
||||
if (index > -1) {
|
||||
this.farmList.splice(index, 1)
|
||||
this.statistics.totalFarms--
|
||||
}
|
||||
|
||||
if (this.currentFarm?.id === id) {
|
||||
this.currentFarm = null
|
||||
}
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('删除养殖场失败:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 添加牲畜
|
||||
* @param {Object} livestockData 牲畜数据
|
||||
*/
|
||||
async addLivestock(livestockData) {
|
||||
try {
|
||||
const response = await farmingApi.createLivestock(livestockData)
|
||||
const newLivestock = response.data
|
||||
|
||||
this.livestockList.unshift(newLivestock)
|
||||
this.statistics.totalLivestock++
|
||||
this.updateHealthStats()
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('添加牲畜失败:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新牲畜信息
|
||||
* @param {number} id 牲畜ID
|
||||
* @param {Object} livestockData 牲畜数据
|
||||
*/
|
||||
async updateLivestock(id, livestockData) {
|
||||
try {
|
||||
const response = await farmingApi.updateLivestock(id, livestockData)
|
||||
const updatedLivestock = response.data
|
||||
|
||||
const index = this.livestockList.findIndex(livestock => livestock.id === id)
|
||||
if (index > -1) {
|
||||
this.livestockList[index] = updatedLivestock
|
||||
}
|
||||
|
||||
if (this.currentLivestock?.id === id) {
|
||||
this.currentLivestock = updatedLivestock
|
||||
}
|
||||
|
||||
this.updateHealthStats()
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('更新牲畜信息失败:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 删除牲畜
|
||||
* @param {number} id 牲畜ID
|
||||
*/
|
||||
async deleteLivestock(id) {
|
||||
try {
|
||||
const response = await farmingApi.deleteLivestock(id)
|
||||
|
||||
const index = this.livestockList.findIndex(livestock => livestock.id === id)
|
||||
if (index > -1) {
|
||||
this.livestockList.splice(index, 1)
|
||||
this.statistics.totalLivestock--
|
||||
}
|
||||
|
||||
if (this.currentLivestock?.id === id) {
|
||||
this.currentLivestock = null
|
||||
}
|
||||
|
||||
this.updateHealthStats()
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('删除牲畜失败:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 添加养殖记录
|
||||
* @param {Object} recordData 记录数据
|
||||
*/
|
||||
async addRecord(recordData) {
|
||||
try {
|
||||
const response = await farmingApi.createRecord(recordData)
|
||||
const newRecord = response.data
|
||||
|
||||
this.records.unshift(newRecord)
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('添加养殖记录失败:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 设置当前养殖场
|
||||
* @param {Object} farm 养殖场对象
|
||||
*/
|
||||
setCurrentFarm(farm) {
|
||||
this.currentFarm = farm
|
||||
|
||||
// 重置牲畜列表分页
|
||||
this.pagination.livestockList.page = 1
|
||||
|
||||
// 获取该养殖场的牲畜列表
|
||||
if (farm) {
|
||||
this.fetchLivestockList({ farmId: farm.id })
|
||||
} else {
|
||||
this.livestockList = []
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 设置当前牲畜
|
||||
* @param {Object} livestock 牲畜对象
|
||||
*/
|
||||
setCurrentLivestock(livestock) {
|
||||
this.currentLivestock = livestock
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新健康统计
|
||||
*/
|
||||
updateHealthStats() {
|
||||
this.statistics.healthyCount = this.healthyLivestock.length
|
||||
this.statistics.sickCount = this.sickLivestock.length
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新饲料统计
|
||||
*/
|
||||
updateFeedStats() {
|
||||
this.statistics.feedConsumption = this.feedData.reduce((total, item) => total + item.amount, 0)
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新疫苗统计
|
||||
*/
|
||||
updateVaccineStats() {
|
||||
this.statistics.vaccineCount = this.vaccineData.length
|
||||
},
|
||||
|
||||
/**
|
||||
* 设置筛选条件
|
||||
* @param {Object} filters 筛选条件
|
||||
*/
|
||||
setFilters(filters) {
|
||||
this.filters = { ...this.filters, ...filters }
|
||||
},
|
||||
|
||||
/**
|
||||
* 重置筛选条件
|
||||
*/
|
||||
resetFilters() {
|
||||
this.filters = {
|
||||
farmType: '',
|
||||
livestockType: '',
|
||||
healthStatus: '',
|
||||
dateRange: []
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 刷新数据
|
||||
*/
|
||||
async refreshData() {
|
||||
try {
|
||||
await Promise.all([
|
||||
this.fetchFarmList({ page: 1 }),
|
||||
this.currentFarm ? this.fetchLivestockList({ page: 1, farmId: this.currentFarm.id }) : Promise.resolve(),
|
||||
this.fetchRecords({ page: 1 })
|
||||
])
|
||||
} catch (error) {
|
||||
console.error('刷新数据失败:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 清除数据
|
||||
*/
|
||||
clearData() {
|
||||
this.farmList = []
|
||||
this.currentFarm = null
|
||||
this.livestockList = []
|
||||
this.currentLivestock = null
|
||||
this.records = []
|
||||
this.healthData = []
|
||||
this.feedData = []
|
||||
this.vaccineData = []
|
||||
|
||||
// 重置分页
|
||||
Object.keys(this.pagination).forEach(key => {
|
||||
this.pagination[key].page = 1
|
||||
this.pagination[key].total = 0
|
||||
})
|
||||
|
||||
// 重置统计
|
||||
this.statistics = {
|
||||
totalFarms: 0,
|
||||
totalLivestock: 0,
|
||||
healthyCount: 0,
|
||||
sickCount: 0,
|
||||
feedConsumption: 0,
|
||||
vaccineCount: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
823
mini_program/common/store/modules/mall.js
Normal file
823
mini_program/common/store/modules/mall.js
Normal file
@@ -0,0 +1,823 @@
|
||||
/**
|
||||
* 牛肉商城状态管理模块
|
||||
* 管理商城相关的数据和状态
|
||||
*/
|
||||
|
||||
import { defineStore } from 'pinia'
|
||||
import { mallApi } from '@/common/api/mall'
|
||||
|
||||
export const useMallStore = defineStore('mall', {
|
||||
state: () => ({
|
||||
// 商品列表
|
||||
productList: [],
|
||||
|
||||
// 当前商品详情
|
||||
currentProduct: null,
|
||||
|
||||
// 商品分类
|
||||
categories: [],
|
||||
|
||||
// 购物车商品
|
||||
cartItems: [],
|
||||
|
||||
// 订单列表
|
||||
orderList: [],
|
||||
|
||||
// 当前订单详情
|
||||
currentOrder: null,
|
||||
|
||||
// 收货地址列表
|
||||
addressList: [],
|
||||
|
||||
// 默认收货地址
|
||||
defaultAddress: null,
|
||||
|
||||
// 优惠券列表
|
||||
couponList: [],
|
||||
|
||||
// 可用优惠券
|
||||
availableCoupons: [],
|
||||
|
||||
// 商城统计数据
|
||||
mallStats: {
|
||||
totalProducts: 0,
|
||||
totalOrders: 0,
|
||||
totalSales: 0,
|
||||
todayOrders: 0
|
||||
},
|
||||
|
||||
// 推荐商品
|
||||
recommendProducts: [],
|
||||
|
||||
// 热销商品
|
||||
hotProducts: [],
|
||||
|
||||
// 新品推荐
|
||||
newProducts: [],
|
||||
|
||||
// 加载状态
|
||||
loading: {
|
||||
productList: false,
|
||||
cartItems: false,
|
||||
orderList: false,
|
||||
addressList: false,
|
||||
couponList: false
|
||||
},
|
||||
|
||||
// 分页信息
|
||||
pagination: {
|
||||
productList: { page: 1, size: 20, total: 0 },
|
||||
orderList: { page: 1, size: 20, total: 0 }
|
||||
},
|
||||
|
||||
// 筛选条件
|
||||
filters: {
|
||||
categoryId: '',
|
||||
priceRange: [],
|
||||
brand: '',
|
||||
origin: '',
|
||||
sortBy: 'createTime',
|
||||
sortOrder: 'desc'
|
||||
},
|
||||
|
||||
// 搜索关键词
|
||||
searchKeyword: ''
|
||||
}),
|
||||
|
||||
getters: {
|
||||
// 获取购物车商品数量
|
||||
cartItemCount: (state) => {
|
||||
return state.cartItems.reduce((total, item) => total + item.quantity, 0)
|
||||
},
|
||||
|
||||
// 获取购物车总价
|
||||
cartTotalPrice: (state) => {
|
||||
return state.cartItems.reduce((total, item) => {
|
||||
return total + (item.price * item.quantity)
|
||||
}, 0)
|
||||
},
|
||||
|
||||
// 获取选中的购物车商品
|
||||
selectedCartItems: (state) => {
|
||||
return state.cartItems.filter(item => item.selected)
|
||||
},
|
||||
|
||||
// 获取选中商品总价
|
||||
selectedCartTotalPrice: (state) => {
|
||||
return state.cartItems
|
||||
.filter(item => item.selected)
|
||||
.reduce((total, item) => total + (item.price * item.quantity), 0)
|
||||
},
|
||||
|
||||
// 获取待付款订单
|
||||
pendingPaymentOrders: (state) => {
|
||||
return state.orderList.filter(order => order.status === 'pending_payment')
|
||||
},
|
||||
|
||||
// 获取待发货订单
|
||||
pendingShipmentOrders: (state) => {
|
||||
return state.orderList.filter(order => order.status === 'pending_shipment')
|
||||
},
|
||||
|
||||
// 获取待收货订单
|
||||
pendingReceiveOrders: (state) => {
|
||||
return state.orderList.filter(order => order.status === 'pending_receive')
|
||||
},
|
||||
|
||||
// 获取已完成订单
|
||||
completedOrders: (state) => {
|
||||
return state.orderList.filter(order => order.status === 'completed')
|
||||
},
|
||||
|
||||
// 获取可用优惠券数量
|
||||
availableCouponCount: (state) => {
|
||||
return state.availableCoupons.length
|
||||
},
|
||||
|
||||
// 获取商品分类树
|
||||
categoryTree: (state) => {
|
||||
const buildTree = (categories, parentId = null) => {
|
||||
return categories
|
||||
.filter(cat => cat.parentId === parentId)
|
||||
.map(cat => ({
|
||||
...cat,
|
||||
children: buildTree(categories, cat.id)
|
||||
}))
|
||||
}
|
||||
|
||||
return buildTree(state.categories)
|
||||
},
|
||||
|
||||
// 获取热门分类
|
||||
popularCategories: (state) => {
|
||||
return state.categories
|
||||
.filter(cat => cat.productCount > 0)
|
||||
.sort((a, b) => b.productCount - a.productCount)
|
||||
.slice(0, 8)
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
/**
|
||||
* 获取商品列表
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
async fetchProductList(params = {}) {
|
||||
this.loading.productList = true
|
||||
|
||||
try {
|
||||
const response = await mallApi.getProductList({
|
||||
page: this.pagination.productList.page,
|
||||
size: this.pagination.productList.size,
|
||||
keyword: this.searchKeyword,
|
||||
...this.filters,
|
||||
...params
|
||||
})
|
||||
|
||||
const { data, total } = response.data
|
||||
|
||||
if (params.page === 1) {
|
||||
this.productList = data
|
||||
} else {
|
||||
this.productList.push(...data)
|
||||
}
|
||||
|
||||
this.pagination.productList.total = total
|
||||
this.mallStats.totalProducts = total
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取商品列表失败:', error)
|
||||
throw error
|
||||
} finally {
|
||||
this.loading.productList = false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取商品详情
|
||||
* @param {number} id 商品ID
|
||||
*/
|
||||
async fetchProductDetail(id) {
|
||||
try {
|
||||
const response = await mallApi.getProductDetail(id)
|
||||
this.currentProduct = response.data
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取商品详情失败:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取商品分类
|
||||
*/
|
||||
async fetchCategories() {
|
||||
try {
|
||||
const response = await mallApi.getCategories()
|
||||
this.categories = response.data
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取商品分类失败:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取推荐商品
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
async fetchRecommendProducts(params = {}) {
|
||||
try {
|
||||
const response = await mallApi.getRecommendProducts(params)
|
||||
this.recommendProducts = response.data
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取推荐商品失败:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取热销商品
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
async fetchHotProducts(params = {}) {
|
||||
try {
|
||||
const response = await mallApi.getHotProducts(params)
|
||||
this.hotProducts = response.data
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取热销商品失败:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取新品推荐
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
async fetchNewProducts(params = {}) {
|
||||
try {
|
||||
const response = await mallApi.getNewProducts(params)
|
||||
this.newProducts = response.data
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取新品推荐失败:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取购物车商品
|
||||
*/
|
||||
async fetchCartItems() {
|
||||
this.loading.cartItems = true
|
||||
|
||||
try {
|
||||
const response = await mallApi.getCartItems()
|
||||
this.cartItems = response.data.map(item => ({
|
||||
...item,
|
||||
selected: false
|
||||
}))
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取购物车失败:', error)
|
||||
throw error
|
||||
} finally {
|
||||
this.loading.cartItems = false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 添加商品到购物车
|
||||
* @param {Object} productData 商品数据
|
||||
*/
|
||||
async addToCart(productData) {
|
||||
try {
|
||||
const response = await mallApi.addToCart(productData)
|
||||
|
||||
// 检查购物车中是否已存在该商品
|
||||
const existingIndex = this.cartItems.findIndex(
|
||||
item => item.productId === productData.productId &&
|
||||
item.skuId === productData.skuId
|
||||
)
|
||||
|
||||
if (existingIndex > -1) {
|
||||
// 更新数量
|
||||
this.cartItems[existingIndex].quantity += productData.quantity
|
||||
} else {
|
||||
// 添加新商品
|
||||
this.cartItems.push({
|
||||
...response.data,
|
||||
selected: false
|
||||
})
|
||||
}
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('添加购物车失败:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新购物车商品数量
|
||||
* @param {number} cartItemId 购物车商品ID
|
||||
* @param {number} quantity 数量
|
||||
*/
|
||||
async updateCartItemQuantity(cartItemId, quantity) {
|
||||
try {
|
||||
const response = await mallApi.updateCartItem(cartItemId, { quantity })
|
||||
|
||||
const index = this.cartItems.findIndex(item => item.id === cartItemId)
|
||||
if (index > -1) {
|
||||
if (quantity <= 0) {
|
||||
this.cartItems.splice(index, 1)
|
||||
} else {
|
||||
this.cartItems[index].quantity = quantity
|
||||
}
|
||||
}
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('更新购物车失败:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 删除购物车商品
|
||||
* @param {number} cartItemId 购物车商品ID
|
||||
*/
|
||||
async removeCartItem(cartItemId) {
|
||||
try {
|
||||
const response = await mallApi.removeCartItem(cartItemId)
|
||||
|
||||
const index = this.cartItems.findIndex(item => item.id === cartItemId)
|
||||
if (index > -1) {
|
||||
this.cartItems.splice(index, 1)
|
||||
}
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('删除购物车商品失败:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 清空购物车
|
||||
*/
|
||||
async clearCart() {
|
||||
try {
|
||||
const response = await mallApi.clearCart()
|
||||
this.cartItems = []
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('清空购物车失败:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 选择/取消选择购物车商品
|
||||
* @param {number} cartItemId 购物车商品ID
|
||||
* @param {boolean} selected 是否选中
|
||||
*/
|
||||
selectCartItem(cartItemId, selected) {
|
||||
const index = this.cartItems.findIndex(item => item.id === cartItemId)
|
||||
if (index > -1) {
|
||||
this.cartItems[index].selected = selected
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 全选/取消全选购物车商品
|
||||
* @param {boolean} selected 是否选中
|
||||
*/
|
||||
selectAllCartItems(selected) {
|
||||
this.cartItems.forEach(item => {
|
||||
item.selected = selected
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取订单列表
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
async fetchOrderList(params = {}) {
|
||||
this.loading.orderList = true
|
||||
|
||||
try {
|
||||
const response = await mallApi.getOrderList({
|
||||
page: this.pagination.orderList.page,
|
||||
size: this.pagination.orderList.size,
|
||||
...params
|
||||
})
|
||||
|
||||
const { data, total } = response.data
|
||||
|
||||
if (params.page === 1) {
|
||||
this.orderList = data
|
||||
} else {
|
||||
this.orderList.push(...data)
|
||||
}
|
||||
|
||||
this.pagination.orderList.total = total
|
||||
this.mallStats.totalOrders = total
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取订单列表失败:', error)
|
||||
throw error
|
||||
} finally {
|
||||
this.loading.orderList = false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取订单详情
|
||||
* @param {number} id 订单ID
|
||||
*/
|
||||
async fetchOrderDetail(id) {
|
||||
try {
|
||||
const response = await mallApi.getOrderDetail(id)
|
||||
this.currentOrder = response.data
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取订单详情失败:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 创建订单
|
||||
* @param {Object} orderData 订单数据
|
||||
*/
|
||||
async createOrder(orderData) {
|
||||
try {
|
||||
const response = await mallApi.createOrder(orderData)
|
||||
const newOrder = response.data
|
||||
|
||||
this.orderList.unshift(newOrder)
|
||||
|
||||
// 清除已购买的购物车商品
|
||||
if (orderData.cartItemIds) {
|
||||
this.cartItems = this.cartItems.filter(
|
||||
item => !orderData.cartItemIds.includes(item.id)
|
||||
)
|
||||
}
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('创建订单失败:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 支付订单
|
||||
* @param {number} orderId 订单ID
|
||||
* @param {Object} paymentData 支付数据
|
||||
*/
|
||||
async payOrder(orderId, paymentData) {
|
||||
try {
|
||||
const response = await mallApi.payOrder(orderId, paymentData)
|
||||
|
||||
// 更新订单状态
|
||||
const orderIndex = this.orderList.findIndex(order => order.id === orderId)
|
||||
if (orderIndex > -1) {
|
||||
this.orderList[orderIndex].status = 'paid'
|
||||
this.orderList[orderIndex].payTime = new Date().toISOString()
|
||||
}
|
||||
|
||||
if (this.currentOrder?.id === orderId) {
|
||||
this.currentOrder.status = 'paid'
|
||||
this.currentOrder.payTime = new Date().toISOString()
|
||||
}
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('支付订单失败:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 取消订单
|
||||
* @param {number} orderId 订单ID
|
||||
* @param {string} reason 取消原因
|
||||
*/
|
||||
async cancelOrder(orderId, reason) {
|
||||
try {
|
||||
const response = await mallApi.cancelOrder(orderId, { reason })
|
||||
|
||||
// 更新订单状态
|
||||
const orderIndex = this.orderList.findIndex(order => order.id === orderId)
|
||||
if (orderIndex > -1) {
|
||||
this.orderList[orderIndex].status = 'cancelled'
|
||||
this.orderList[orderIndex].cancelReason = reason
|
||||
}
|
||||
|
||||
if (this.currentOrder?.id === orderId) {
|
||||
this.currentOrder.status = 'cancelled'
|
||||
this.currentOrder.cancelReason = reason
|
||||
}
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('取消订单失败:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 确认收货
|
||||
* @param {number} orderId 订单ID
|
||||
*/
|
||||
async confirmReceive(orderId) {
|
||||
try {
|
||||
const response = await mallApi.confirmReceive(orderId)
|
||||
|
||||
// 更新订单状态
|
||||
const orderIndex = this.orderList.findIndex(order => order.id === orderId)
|
||||
if (orderIndex > -1) {
|
||||
this.orderList[orderIndex].status = 'completed'
|
||||
this.orderList[orderIndex].receiveTime = new Date().toISOString()
|
||||
}
|
||||
|
||||
if (this.currentOrder?.id === orderId) {
|
||||
this.currentOrder.status = 'completed'
|
||||
this.currentOrder.receiveTime = new Date().toISOString()
|
||||
}
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('确认收货失败:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取收货地址列表
|
||||
*/
|
||||
async fetchAddressList() {
|
||||
this.loading.addressList = true
|
||||
|
||||
try {
|
||||
const response = await mallApi.getAddressList()
|
||||
this.addressList = response.data
|
||||
|
||||
// 设置默认地址
|
||||
const defaultAddr = this.addressList.find(addr => addr.isDefault)
|
||||
if (defaultAddr) {
|
||||
this.defaultAddress = defaultAddr
|
||||
}
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取收货地址失败:', error)
|
||||
throw error
|
||||
} finally {
|
||||
this.loading.addressList = false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 添加收货地址
|
||||
* @param {Object} addressData 地址数据
|
||||
*/
|
||||
async addAddress(addressData) {
|
||||
try {
|
||||
const response = await mallApi.addAddress(addressData)
|
||||
const newAddress = response.data
|
||||
|
||||
this.addressList.push(newAddress)
|
||||
|
||||
// 如果是默认地址,更新默认地址
|
||||
if (newAddress.isDefault) {
|
||||
this.defaultAddress = newAddress
|
||||
// 取消其他地址的默认状态
|
||||
this.addressList.forEach(addr => {
|
||||
if (addr.id !== newAddress.id) {
|
||||
addr.isDefault = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('添加收货地址失败:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新收货地址
|
||||
* @param {number} id 地址ID
|
||||
* @param {Object} addressData 地址数据
|
||||
*/
|
||||
async updateAddress(id, addressData) {
|
||||
try {
|
||||
const response = await mallApi.updateAddress(id, addressData)
|
||||
const updatedAddress = response.data
|
||||
|
||||
const index = this.addressList.findIndex(addr => addr.id === id)
|
||||
if (index > -1) {
|
||||
this.addressList[index] = updatedAddress
|
||||
}
|
||||
|
||||
// 如果是默认地址,更新默认地址
|
||||
if (updatedAddress.isDefault) {
|
||||
this.defaultAddress = updatedAddress
|
||||
// 取消其他地址的默认状态
|
||||
this.addressList.forEach(addr => {
|
||||
if (addr.id !== updatedAddress.id) {
|
||||
addr.isDefault = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('更新收货地址失败:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 删除收货地址
|
||||
* @param {number} id 地址ID
|
||||
*/
|
||||
async deleteAddress(id) {
|
||||
try {
|
||||
const response = await mallApi.deleteAddress(id)
|
||||
|
||||
const index = this.addressList.findIndex(addr => addr.id === id)
|
||||
if (index > -1) {
|
||||
this.addressList.splice(index, 1)
|
||||
}
|
||||
|
||||
// 如果删除的是默认地址,清除默认地址
|
||||
if (this.defaultAddress?.id === id) {
|
||||
this.defaultAddress = null
|
||||
}
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('删除收货地址失败:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取优惠券列表
|
||||
*/
|
||||
async fetchCouponList() {
|
||||
this.loading.couponList = true
|
||||
|
||||
try {
|
||||
const response = await mallApi.getCouponList()
|
||||
this.couponList = response.data
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取优惠券失败:', error)
|
||||
throw error
|
||||
} finally {
|
||||
this.loading.couponList = false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取可用优惠券
|
||||
* @param {number} totalAmount 订单总金额
|
||||
*/
|
||||
async fetchAvailableCoupons(totalAmount) {
|
||||
try {
|
||||
const response = await mallApi.getAvailableCoupons({ totalAmount })
|
||||
this.availableCoupons = response.data
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取可用优惠券失败:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 领取优惠券
|
||||
* @param {number} couponId 优惠券ID
|
||||
*/
|
||||
async receiveCoupon(couponId) {
|
||||
try {
|
||||
const response = await mallApi.receiveCoupon(couponId)
|
||||
|
||||
// 更新优惠券状态
|
||||
const couponIndex = this.couponList.findIndex(coupon => coupon.id === couponId)
|
||||
if (couponIndex > -1) {
|
||||
this.couponList[couponIndex].received = true
|
||||
this.couponList[couponIndex].receiveCount++
|
||||
}
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('领取优惠券失败:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 设置搜索关键词
|
||||
* @param {string} keyword 搜索关键词
|
||||
*/
|
||||
setSearchKeyword(keyword) {
|
||||
this.searchKeyword = keyword
|
||||
},
|
||||
|
||||
/**
|
||||
* 设置筛选条件
|
||||
* @param {Object} filters 筛选条件
|
||||
*/
|
||||
setFilters(filters) {
|
||||
this.filters = { ...this.filters, ...filters }
|
||||
},
|
||||
|
||||
/**
|
||||
* 重置筛选条件
|
||||
*/
|
||||
resetFilters() {
|
||||
this.filters = {
|
||||
categoryId: '',
|
||||
priceRange: [],
|
||||
brand: '',
|
||||
origin: '',
|
||||
sortBy: 'createTime',
|
||||
sortOrder: 'desc'
|
||||
}
|
||||
this.searchKeyword = ''
|
||||
},
|
||||
|
||||
/**
|
||||
* 刷新数据
|
||||
*/
|
||||
async refreshData() {
|
||||
try {
|
||||
await Promise.all([
|
||||
this.fetchProductList({ page: 1 }),
|
||||
this.fetchCategories(),
|
||||
this.fetchRecommendProducts(),
|
||||
this.fetchHotProducts(),
|
||||
this.fetchNewProducts()
|
||||
])
|
||||
} catch (error) {
|
||||
console.error('刷新数据失败:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 清除数据
|
||||
*/
|
||||
clearData() {
|
||||
this.productList = []
|
||||
this.currentProduct = null
|
||||
this.cartItems = []
|
||||
this.orderList = []
|
||||
this.currentOrder = null
|
||||
this.addressList = []
|
||||
this.defaultAddress = null
|
||||
this.couponList = []
|
||||
this.availableCoupons = []
|
||||
this.recommendProducts = []
|
||||
this.hotProducts = []
|
||||
this.newProducts = []
|
||||
|
||||
// 重置分页
|
||||
Object.keys(this.pagination).forEach(key => {
|
||||
this.pagination[key].page = 1
|
||||
this.pagination[key].total = 0
|
||||
})
|
||||
|
||||
// 重置筛选条件
|
||||
this.resetFilters()
|
||||
|
||||
// 重置统计数据
|
||||
this.mallStats = {
|
||||
totalProducts: 0,
|
||||
totalOrders: 0,
|
||||
totalSales: 0,
|
||||
todayOrders: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
737
mini_program/common/store/modules/trading.js
Normal file
737
mini_program/common/store/modules/trading.js
Normal file
@@ -0,0 +1,737 @@
|
||||
/**
|
||||
* 牛只交易状态管理模块
|
||||
* 管理交易相关的数据和状态
|
||||
*/
|
||||
|
||||
import { defineStore } from 'pinia'
|
||||
import { tradingApi } from '@/common/api/trading'
|
||||
|
||||
export const useTradingStore = defineStore('trading', {
|
||||
state: () => ({
|
||||
// 交易列表
|
||||
tradeList: [],
|
||||
|
||||
// 当前交易详情
|
||||
currentTrade: null,
|
||||
|
||||
// 我的发布列表
|
||||
myPublishList: [],
|
||||
|
||||
// 我的购买列表
|
||||
myPurchaseList: [],
|
||||
|
||||
// 交易订单列表
|
||||
orderList: [],
|
||||
|
||||
// 当前订单详情
|
||||
currentOrder: null,
|
||||
|
||||
// 价格行情数据
|
||||
priceData: [],
|
||||
|
||||
// 市场统计数据
|
||||
marketStats: {
|
||||
totalTrades: 0,
|
||||
todayTrades: 0,
|
||||
averagePrice: 0,
|
||||
priceChange: 0,
|
||||
activeTraders: 0
|
||||
},
|
||||
|
||||
// 交易分类
|
||||
categories: [],
|
||||
|
||||
// 地区列表
|
||||
regions: [],
|
||||
|
||||
// 加载状态
|
||||
loading: {
|
||||
tradeList: false,
|
||||
myPublishList: false,
|
||||
myPurchaseList: false,
|
||||
orderList: false,
|
||||
priceData: false,
|
||||
marketStats: false
|
||||
},
|
||||
|
||||
// 分页信息
|
||||
pagination: {
|
||||
tradeList: { page: 1, size: 20, total: 0 },
|
||||
myPublishList: { page: 1, size: 20, total: 0 },
|
||||
myPurchaseList: { page: 1, size: 20, total: 0 },
|
||||
orderList: { page: 1, size: 20, total: 0 }
|
||||
},
|
||||
|
||||
// 筛选条件
|
||||
filters: {
|
||||
category: '',
|
||||
region: '',
|
||||
priceRange: [],
|
||||
weightRange: [],
|
||||
ageRange: [],
|
||||
gender: '',
|
||||
breed: '',
|
||||
sortBy: 'createTime',
|
||||
sortOrder: 'desc'
|
||||
},
|
||||
|
||||
// 搜索关键词
|
||||
searchKeyword: ''
|
||||
}),
|
||||
|
||||
getters: {
|
||||
// 获取在售交易列表
|
||||
availableTrades: (state) => {
|
||||
return state.tradeList.filter(trade => trade.status === 'available')
|
||||
},
|
||||
|
||||
// 获取已售出交易列表
|
||||
soldTrades: (state) => {
|
||||
return state.tradeList.filter(trade => trade.status === 'sold')
|
||||
},
|
||||
|
||||
// 获取我的在售发布
|
||||
myAvailablePublish: (state) => {
|
||||
return state.myPublishList.filter(trade => trade.status === 'available')
|
||||
},
|
||||
|
||||
// 获取我的已售发布
|
||||
mySoldPublish: (state) => {
|
||||
return state.myPublishList.filter(trade => trade.status === 'sold')
|
||||
},
|
||||
|
||||
// 获取待付款订单
|
||||
pendingPaymentOrders: (state) => {
|
||||
return state.orderList.filter(order => order.status === 'pending_payment')
|
||||
},
|
||||
|
||||
// 获取待发货订单
|
||||
pendingShipmentOrders: (state) => {
|
||||
return state.orderList.filter(order => order.status === 'pending_shipment')
|
||||
},
|
||||
|
||||
// 获取已完成订单
|
||||
completedOrders: (state) => {
|
||||
return state.orderList.filter(order => order.status === 'completed')
|
||||
},
|
||||
|
||||
// 获取今日价格变化
|
||||
todayPriceChange: (state) => {
|
||||
if (state.priceData.length < 2) return 0
|
||||
|
||||
const today = state.priceData[state.priceData.length - 1]
|
||||
const yesterday = state.priceData[state.priceData.length - 2]
|
||||
|
||||
return ((today.price - yesterday.price) / yesterday.price * 100).toFixed(2)
|
||||
},
|
||||
|
||||
// 获取价格趋势
|
||||
priceTrend: (state) => {
|
||||
if (state.priceData.length < 2) return 'stable'
|
||||
|
||||
const recent = state.priceData.slice(-5)
|
||||
const trend = recent.reduce((acc, curr, index) => {
|
||||
if (index === 0) return acc
|
||||
return acc + (curr.price > recent[index - 1].price ? 1 : -1)
|
||||
}, 0)
|
||||
|
||||
if (trend > 2) return 'rising'
|
||||
if (trend < -2) return 'falling'
|
||||
return 'stable'
|
||||
},
|
||||
|
||||
// 获取热门品种
|
||||
popularBreeds: (state) => {
|
||||
const breedCount = {}
|
||||
state.tradeList.forEach(trade => {
|
||||
breedCount[trade.breed] = (breedCount[trade.breed] || 0) + 1
|
||||
})
|
||||
|
||||
return Object.entries(breedCount)
|
||||
.sort(([,a], [,b]) => b - a)
|
||||
.slice(0, 5)
|
||||
.map(([breed, count]) => ({ breed, count }))
|
||||
},
|
||||
|
||||
// 获取活跃地区
|
||||
activeRegions: (state) => {
|
||||
const regionCount = {}
|
||||
state.tradeList.forEach(trade => {
|
||||
regionCount[trade.region] = (regionCount[trade.region] || 0) + 1
|
||||
})
|
||||
|
||||
return Object.entries(regionCount)
|
||||
.sort(([,a], [,b]) => b - a)
|
||||
.slice(0, 10)
|
||||
.map(([region, count]) => ({ region, count }))
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
/**
|
||||
* 获取交易列表
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
async fetchTradeList(params = {}) {
|
||||
this.loading.tradeList = true
|
||||
|
||||
try {
|
||||
const response = await tradingApi.getTradeList({
|
||||
page: this.pagination.tradeList.page,
|
||||
size: this.pagination.tradeList.size,
|
||||
keyword: this.searchKeyword,
|
||||
...this.filters,
|
||||
...params
|
||||
})
|
||||
|
||||
const { data, total } = response.data
|
||||
|
||||
if (params.page === 1) {
|
||||
this.tradeList = data
|
||||
} else {
|
||||
this.tradeList.push(...data)
|
||||
}
|
||||
|
||||
this.pagination.tradeList.total = total
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取交易列表失败:', error)
|
||||
throw error
|
||||
} finally {
|
||||
this.loading.tradeList = false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取交易详情
|
||||
* @param {number} id 交易ID
|
||||
*/
|
||||
async fetchTradeDetail(id) {
|
||||
try {
|
||||
const response = await tradingApi.getTradeDetail(id)
|
||||
this.currentTrade = response.data
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取交易详情失败:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取我的发布列表
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
async fetchMyPublishList(params = {}) {
|
||||
this.loading.myPublishList = true
|
||||
|
||||
try {
|
||||
const response = await tradingApi.getMyPublishList({
|
||||
page: this.pagination.myPublishList.page,
|
||||
size: this.pagination.myPublishList.size,
|
||||
...params
|
||||
})
|
||||
|
||||
const { data, total } = response.data
|
||||
|
||||
if (params.page === 1) {
|
||||
this.myPublishList = data
|
||||
} else {
|
||||
this.myPublishList.push(...data)
|
||||
}
|
||||
|
||||
this.pagination.myPublishList.total = total
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取我的发布列表失败:', error)
|
||||
throw error
|
||||
} finally {
|
||||
this.loading.myPublishList = false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取我的购买列表
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
async fetchMyPurchaseList(params = {}) {
|
||||
this.loading.myPurchaseList = true
|
||||
|
||||
try {
|
||||
const response = await tradingApi.getMyPurchaseList({
|
||||
page: this.pagination.myPurchaseList.page,
|
||||
size: this.pagination.myPurchaseList.size,
|
||||
...params
|
||||
})
|
||||
|
||||
const { data, total } = response.data
|
||||
|
||||
if (params.page === 1) {
|
||||
this.myPurchaseList = data
|
||||
} else {
|
||||
this.myPurchaseList.push(...data)
|
||||
}
|
||||
|
||||
this.pagination.myPurchaseList.total = total
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取我的购买列表失败:', error)
|
||||
throw error
|
||||
} finally {
|
||||
this.loading.myPurchaseList = false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取订单列表
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
async fetchOrderList(params = {}) {
|
||||
this.loading.orderList = true
|
||||
|
||||
try {
|
||||
const response = await tradingApi.getOrderList({
|
||||
page: this.pagination.orderList.page,
|
||||
size: this.pagination.orderList.size,
|
||||
...params
|
||||
})
|
||||
|
||||
const { data, total } = response.data
|
||||
|
||||
if (params.page === 1) {
|
||||
this.orderList = data
|
||||
} else {
|
||||
this.orderList.push(...data)
|
||||
}
|
||||
|
||||
this.pagination.orderList.total = total
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取订单列表失败:', error)
|
||||
throw error
|
||||
} finally {
|
||||
this.loading.orderList = false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取订单详情
|
||||
* @param {number} id 订单ID
|
||||
*/
|
||||
async fetchOrderDetail(id) {
|
||||
try {
|
||||
const response = await tradingApi.getOrderDetail(id)
|
||||
this.currentOrder = response.data
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取订单详情失败:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取价格行情数据
|
||||
* @param {Object} params 查询参数
|
||||
*/
|
||||
async fetchPriceData(params = {}) {
|
||||
this.loading.priceData = true
|
||||
|
||||
try {
|
||||
const response = await tradingApi.getPriceData(params)
|
||||
this.priceData = response.data
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取价格行情失败:', error)
|
||||
throw error
|
||||
} finally {
|
||||
this.loading.priceData = false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取市场统计数据
|
||||
*/
|
||||
async fetchMarketStats() {
|
||||
this.loading.marketStats = true
|
||||
|
||||
try {
|
||||
const response = await tradingApi.getMarketStats()
|
||||
this.marketStats = response.data
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取市场统计失败:', error)
|
||||
throw error
|
||||
} finally {
|
||||
this.loading.marketStats = false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 发布交易信息
|
||||
* @param {Object} tradeData 交易数据
|
||||
*/
|
||||
async publishTrade(tradeData) {
|
||||
try {
|
||||
const response = await tradingApi.publishTrade(tradeData)
|
||||
const newTrade = response.data
|
||||
|
||||
this.myPublishList.unshift(newTrade)
|
||||
this.tradeList.unshift(newTrade)
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('发布交易失败:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新交易信息
|
||||
* @param {number} id 交易ID
|
||||
* @param {Object} tradeData 交易数据
|
||||
*/
|
||||
async updateTrade(id, tradeData) {
|
||||
try {
|
||||
const response = await tradingApi.updateTrade(id, tradeData)
|
||||
const updatedTrade = response.data
|
||||
|
||||
// 更新我的发布列表
|
||||
const publishIndex = this.myPublishList.findIndex(trade => trade.id === id)
|
||||
if (publishIndex > -1) {
|
||||
this.myPublishList[publishIndex] = updatedTrade
|
||||
}
|
||||
|
||||
// 更新交易列表
|
||||
const tradeIndex = this.tradeList.findIndex(trade => trade.id === id)
|
||||
if (tradeIndex > -1) {
|
||||
this.tradeList[tradeIndex] = updatedTrade
|
||||
}
|
||||
|
||||
// 更新当前交易
|
||||
if (this.currentTrade?.id === id) {
|
||||
this.currentTrade = updatedTrade
|
||||
}
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('更新交易失败:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 删除交易信息
|
||||
* @param {number} id 交易ID
|
||||
*/
|
||||
async deleteTrade(id) {
|
||||
try {
|
||||
const response = await tradingApi.deleteTrade(id)
|
||||
|
||||
// 从我的发布列表中移除
|
||||
const publishIndex = this.myPublishList.findIndex(trade => trade.id === id)
|
||||
if (publishIndex > -1) {
|
||||
this.myPublishList.splice(publishIndex, 1)
|
||||
}
|
||||
|
||||
// 从交易列表中移除
|
||||
const tradeIndex = this.tradeList.findIndex(trade => trade.id === id)
|
||||
if (tradeIndex > -1) {
|
||||
this.tradeList.splice(tradeIndex, 1)
|
||||
}
|
||||
|
||||
// 清除当前交易
|
||||
if (this.currentTrade?.id === id) {
|
||||
this.currentTrade = null
|
||||
}
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('删除交易失败:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 创建订单
|
||||
* @param {Object} orderData 订单数据
|
||||
*/
|
||||
async createOrder(orderData) {
|
||||
try {
|
||||
const response = await tradingApi.createOrder(orderData)
|
||||
const newOrder = response.data
|
||||
|
||||
this.orderList.unshift(newOrder)
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('创建订单失败:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 支付订单
|
||||
* @param {number} orderId 订单ID
|
||||
* @param {Object} paymentData 支付数据
|
||||
*/
|
||||
async payOrder(orderId, paymentData) {
|
||||
try {
|
||||
const response = await tradingApi.payOrder(orderId, paymentData)
|
||||
|
||||
// 更新订单状态
|
||||
const orderIndex = this.orderList.findIndex(order => order.id === orderId)
|
||||
if (orderIndex > -1) {
|
||||
this.orderList[orderIndex].status = 'paid'
|
||||
this.orderList[orderIndex].payTime = new Date().toISOString()
|
||||
}
|
||||
|
||||
if (this.currentOrder?.id === orderId) {
|
||||
this.currentOrder.status = 'paid'
|
||||
this.currentOrder.payTime = new Date().toISOString()
|
||||
}
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('支付订单失败:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 取消订单
|
||||
* @param {number} orderId 订单ID
|
||||
* @param {string} reason 取消原因
|
||||
*/
|
||||
async cancelOrder(orderId, reason) {
|
||||
try {
|
||||
const response = await tradingApi.cancelOrder(orderId, { reason })
|
||||
|
||||
// 更新订单状态
|
||||
const orderIndex = this.orderList.findIndex(order => order.id === orderId)
|
||||
if (orderIndex > -1) {
|
||||
this.orderList[orderIndex].status = 'cancelled'
|
||||
this.orderList[orderIndex].cancelReason = reason
|
||||
}
|
||||
|
||||
if (this.currentOrder?.id === orderId) {
|
||||
this.currentOrder.status = 'cancelled'
|
||||
this.currentOrder.cancelReason = reason
|
||||
}
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('取消订单失败:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 确认收货
|
||||
* @param {number} orderId 订单ID
|
||||
*/
|
||||
async confirmReceive(orderId) {
|
||||
try {
|
||||
const response = await tradingApi.confirmReceive(orderId)
|
||||
|
||||
// 更新订单状态
|
||||
const orderIndex = this.orderList.findIndex(order => order.id === orderId)
|
||||
if (orderIndex > -1) {
|
||||
this.orderList[orderIndex].status = 'completed'
|
||||
this.orderList[orderIndex].receiveTime = new Date().toISOString()
|
||||
}
|
||||
|
||||
if (this.currentOrder?.id === orderId) {
|
||||
this.currentOrder.status = 'completed'
|
||||
this.currentOrder.receiveTime = new Date().toISOString()
|
||||
}
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('确认收货失败:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取交易分类
|
||||
*/
|
||||
async fetchCategories() {
|
||||
try {
|
||||
const response = await tradingApi.getCategories()
|
||||
this.categories = response.data
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取交易分类失败:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取地区列表
|
||||
*/
|
||||
async fetchRegions() {
|
||||
try {
|
||||
const response = await tradingApi.getRegions()
|
||||
this.regions = response.data
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取地区列表失败:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 设置搜索关键词
|
||||
* @param {string} keyword 搜索关键词
|
||||
*/
|
||||
setSearchKeyword(keyword) {
|
||||
this.searchKeyword = keyword
|
||||
},
|
||||
|
||||
/**
|
||||
* 设置筛选条件
|
||||
* @param {Object} filters 筛选条件
|
||||
*/
|
||||
setFilters(filters) {
|
||||
this.filters = { ...this.filters, ...filters }
|
||||
},
|
||||
|
||||
/**
|
||||
* 重置筛选条件
|
||||
*/
|
||||
resetFilters() {
|
||||
this.filters = {
|
||||
category: '',
|
||||
region: '',
|
||||
priceRange: [],
|
||||
weightRange: [],
|
||||
ageRange: [],
|
||||
gender: '',
|
||||
breed: '',
|
||||
sortBy: 'createTime',
|
||||
sortOrder: 'desc'
|
||||
}
|
||||
this.searchKeyword = ''
|
||||
},
|
||||
|
||||
/**
|
||||
* 收藏交易
|
||||
* @param {number} tradeId 交易ID
|
||||
*/
|
||||
async favoriteTrade(tradeId) {
|
||||
try {
|
||||
const response = await tradingApi.favoriteTrade(tradeId)
|
||||
|
||||
// 更新交易的收藏状态
|
||||
const updateTradeFavorite = (list) => {
|
||||
const index = list.findIndex(trade => trade.id === tradeId)
|
||||
if (index > -1) {
|
||||
list[index].isFavorited = true
|
||||
list[index].favoriteCount = (list[index].favoriteCount || 0) + 1
|
||||
}
|
||||
}
|
||||
|
||||
updateTradeFavorite(this.tradeList)
|
||||
updateTradeFavorite(this.myPublishList)
|
||||
|
||||
if (this.currentTrade?.id === tradeId) {
|
||||
this.currentTrade.isFavorited = true
|
||||
this.currentTrade.favoriteCount = (this.currentTrade.favoriteCount || 0) + 1
|
||||
}
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('收藏交易失败:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 取消收藏交易
|
||||
* @param {number} tradeId 交易ID
|
||||
*/
|
||||
async unfavoriteTrade(tradeId) {
|
||||
try {
|
||||
const response = await tradingApi.unfavoriteTrade(tradeId)
|
||||
|
||||
// 更新交易的收藏状态
|
||||
const updateTradeFavorite = (list) => {
|
||||
const index = list.findIndex(trade => trade.id === tradeId)
|
||||
if (index > -1) {
|
||||
list[index].isFavorited = false
|
||||
list[index].favoriteCount = Math.max((list[index].favoriteCount || 1) - 1, 0)
|
||||
}
|
||||
}
|
||||
|
||||
updateTradeFavorite(this.tradeList)
|
||||
updateTradeFavorite(this.myPublishList)
|
||||
|
||||
if (this.currentTrade?.id === tradeId) {
|
||||
this.currentTrade.isFavorited = false
|
||||
this.currentTrade.favoriteCount = Math.max((this.currentTrade.favoriteCount || 1) - 1, 0)
|
||||
}
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('取消收藏失败:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 刷新数据
|
||||
*/
|
||||
async refreshData() {
|
||||
try {
|
||||
await Promise.all([
|
||||
this.fetchTradeList({ page: 1 }),
|
||||
this.fetchMarketStats(),
|
||||
this.fetchPriceData()
|
||||
])
|
||||
} catch (error) {
|
||||
console.error('刷新数据失败:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 清除数据
|
||||
*/
|
||||
clearData() {
|
||||
this.tradeList = []
|
||||
this.currentTrade = null
|
||||
this.myPublishList = []
|
||||
this.myPurchaseList = []
|
||||
this.orderList = []
|
||||
this.currentOrder = null
|
||||
this.priceData = []
|
||||
|
||||
// 重置分页
|
||||
Object.keys(this.pagination).forEach(key => {
|
||||
this.pagination[key].page = 1
|
||||
this.pagination[key].total = 0
|
||||
})
|
||||
|
||||
// 重置筛选条件
|
||||
this.resetFilters()
|
||||
|
||||
// 重置统计数据
|
||||
this.marketStats = {
|
||||
totalTrades: 0,
|
||||
todayTrades: 0,
|
||||
averagePrice: 0,
|
||||
priceChange: 0,
|
||||
activeTraders: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
375
mini_program/common/store/modules/user.js
Normal file
375
mini_program/common/store/modules/user.js
Normal file
@@ -0,0 +1,375 @@
|
||||
/**
|
||||
* 用户状态管理模块
|
||||
* 管理用户信息、登录状态、权限等
|
||||
*/
|
||||
|
||||
import { defineStore } from 'pinia'
|
||||
import { userApi } from '@/common/api/user'
|
||||
import { storage } from '@/common/utils/storage'
|
||||
import { APP_CONFIG } from '@/common/config'
|
||||
|
||||
export const useUserStore = defineStore('user', {
|
||||
state: () => ({
|
||||
// 用户基本信息
|
||||
userInfo: null,
|
||||
|
||||
// 登录状态
|
||||
isLoggedIn: false,
|
||||
|
||||
// 访问令牌
|
||||
accessToken: null,
|
||||
|
||||
// 刷新令牌
|
||||
refreshToken: null,
|
||||
|
||||
// 用户权限列表
|
||||
permissions: [],
|
||||
|
||||
// 用户角色
|
||||
roles: [],
|
||||
|
||||
// 登录时间
|
||||
loginTime: null,
|
||||
|
||||
// 最后活跃时间
|
||||
lastActiveTime: null,
|
||||
|
||||
// 用户设置
|
||||
settings: {
|
||||
theme: 'light',
|
||||
language: 'zh-CN',
|
||||
notifications: true,
|
||||
autoLogin: true
|
||||
}
|
||||
}),
|
||||
|
||||
getters: {
|
||||
// 获取用户ID
|
||||
userId: (state) => state.userInfo?.id,
|
||||
|
||||
// 获取用户名
|
||||
username: (state) => state.userInfo?.username,
|
||||
|
||||
// 获取用户昵称
|
||||
nickname: (state) => state.userInfo?.nickname || state.userInfo?.username,
|
||||
|
||||
// 获取用户头像
|
||||
avatar: (state) => state.userInfo?.avatar || '/static/images/default-avatar.png',
|
||||
|
||||
// 获取用户手机号
|
||||
phone: (state) => state.userInfo?.phone,
|
||||
|
||||
// 获取用户邮箱
|
||||
email: (state) => state.userInfo?.email,
|
||||
|
||||
// 检查是否有特定权限
|
||||
hasPermission: (state) => (permission) => {
|
||||
return state.permissions.includes(permission)
|
||||
},
|
||||
|
||||
// 检查是否有特定角色
|
||||
hasRole: (state) => (role) => {
|
||||
return state.roles.includes(role)
|
||||
},
|
||||
|
||||
// 检查是否为管理员
|
||||
isAdmin: (state) => {
|
||||
return state.roles.includes('admin') || state.roles.includes('super_admin')
|
||||
},
|
||||
|
||||
// 检查登录是否过期
|
||||
isTokenExpired: (state) => {
|
||||
if (!state.loginTime) return true
|
||||
const now = Date.now()
|
||||
const expireTime = state.loginTime + (24 * 60 * 60 * 1000) // 24小时
|
||||
return now > expireTime
|
||||
},
|
||||
|
||||
// 获取用户完整信息
|
||||
fullUserInfo: (state) => {
|
||||
return {
|
||||
...state.userInfo,
|
||||
permissions: state.permissions,
|
||||
roles: state.roles,
|
||||
settings: state.settings
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
/**
|
||||
* 用户登录
|
||||
* @param {Object} credentials 登录凭证
|
||||
* @param {string} credentials.username 用户名
|
||||
* @param {string} credentials.password 密码
|
||||
* @param {string} credentials.code 验证码(可选)
|
||||
*/
|
||||
async login(credentials) {
|
||||
try {
|
||||
const response = await userApi.login(credentials)
|
||||
const { user, token, refreshToken, permissions, roles } = response.data
|
||||
|
||||
// 保存用户信息
|
||||
this.userInfo = user
|
||||
this.accessToken = token
|
||||
this.refreshToken = refreshToken
|
||||
this.permissions = permissions || []
|
||||
this.roles = roles || []
|
||||
this.isLoggedIn = true
|
||||
this.loginTime = Date.now()
|
||||
this.lastActiveTime = Date.now()
|
||||
|
||||
// 保存到本地存储
|
||||
storage.set(APP_CONFIG.storage.tokenKey, token)
|
||||
storage.set(APP_CONFIG.storage.userKey, user)
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('登录失败:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 微信登录
|
||||
* @param {Object} wxLoginData 微信登录数据
|
||||
*/
|
||||
async wxLogin(wxLoginData) {
|
||||
try {
|
||||
const response = await userApi.wxLogin(wxLoginData)
|
||||
const { user, token, refreshToken, permissions, roles } = response.data
|
||||
|
||||
// 保存用户信息
|
||||
this.userInfo = user
|
||||
this.accessToken = token
|
||||
this.refreshToken = refreshToken
|
||||
this.permissions = permissions || []
|
||||
this.roles = roles || []
|
||||
this.isLoggedIn = true
|
||||
this.loginTime = Date.now()
|
||||
this.lastActiveTime = Date.now()
|
||||
|
||||
// 保存到本地存储
|
||||
storage.set(APP_CONFIG.storage.tokenKey, token)
|
||||
storage.set(APP_CONFIG.storage.userKey, user)
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('微信登录失败:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 用户注册
|
||||
* @param {Object} userData 注册数据
|
||||
*/
|
||||
async register(userData) {
|
||||
try {
|
||||
const response = await userApi.register(userData)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('注册失败:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 用户登出
|
||||
*/
|
||||
async logout() {
|
||||
try {
|
||||
if (this.accessToken) {
|
||||
await userApi.logout()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('登出请求失败:', error)
|
||||
} finally {
|
||||
// 清除用户信息
|
||||
this.userInfo = null
|
||||
this.accessToken = null
|
||||
this.refreshToken = null
|
||||
this.permissions = []
|
||||
this.roles = []
|
||||
this.isLoggedIn = false
|
||||
this.loginTime = null
|
||||
this.lastActiveTime = null
|
||||
|
||||
// 清除本地存储
|
||||
storage.remove(APP_CONFIG.storage.tokenKey)
|
||||
storage.remove(APP_CONFIG.storage.userKey)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 刷新访问令牌
|
||||
*/
|
||||
async refreshAccessToken() {
|
||||
try {
|
||||
if (!this.refreshToken) {
|
||||
throw new Error('没有刷新令牌')
|
||||
}
|
||||
|
||||
const response = await userApi.refreshToken(this.refreshToken)
|
||||
const { token, refreshToken } = response.data
|
||||
|
||||
this.accessToken = token
|
||||
if (refreshToken) {
|
||||
this.refreshToken = refreshToken
|
||||
}
|
||||
|
||||
// 更新本地存储
|
||||
storage.set(APP_CONFIG.storage.tokenKey, token)
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('刷新令牌失败:', error)
|
||||
// 刷新失败,清除登录状态
|
||||
await this.logout()
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取用户信息
|
||||
*/
|
||||
async fetchUserInfo() {
|
||||
try {
|
||||
const response = await userApi.getUserInfo()
|
||||
const { user, permissions, roles } = response.data
|
||||
|
||||
this.userInfo = user
|
||||
this.permissions = permissions || []
|
||||
this.roles = roles || []
|
||||
this.lastActiveTime = Date.now()
|
||||
|
||||
// 更新本地存储
|
||||
storage.set(APP_CONFIG.storage.userKey, user)
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('获取用户信息失败:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新用户信息
|
||||
* @param {Object} userData 用户数据
|
||||
*/
|
||||
async updateUserInfo(userData) {
|
||||
try {
|
||||
const response = await userApi.updateUserInfo(userData)
|
||||
const { user } = response.data
|
||||
|
||||
this.userInfo = { ...this.userInfo, ...user }
|
||||
this.lastActiveTime = Date.now()
|
||||
|
||||
// 更新本地存储
|
||||
storage.set(APP_CONFIG.storage.userKey, this.userInfo)
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('更新用户信息失败:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 修改密码
|
||||
* @param {Object} passwordData 密码数据
|
||||
* @param {string} passwordData.oldPassword 旧密码
|
||||
* @param {string} passwordData.newPassword 新密码
|
||||
*/
|
||||
async changePassword(passwordData) {
|
||||
try {
|
||||
const response = await userApi.changePassword(passwordData)
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('修改密码失败:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 绑定手机号
|
||||
* @param {Object} phoneData 手机号数据
|
||||
* @param {string} phoneData.phone 手机号
|
||||
* @param {string} phoneData.code 验证码
|
||||
*/
|
||||
async bindPhone(phoneData) {
|
||||
try {
|
||||
const response = await userApi.bindPhone(phoneData)
|
||||
const { user } = response.data
|
||||
|
||||
this.userInfo = { ...this.userInfo, ...user }
|
||||
|
||||
// 更新本地存储
|
||||
storage.set(APP_CONFIG.storage.userKey, this.userInfo)
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('绑定手机号失败:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新用户设置
|
||||
* @param {Object} settings 设置数据
|
||||
*/
|
||||
updateSettings(settings) {
|
||||
this.settings = { ...this.settings, ...settings }
|
||||
|
||||
// 保存到本地存储
|
||||
storage.set(APP_CONFIG.storage.settingsKey, this.settings)
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新最后活跃时间
|
||||
*/
|
||||
updateLastActiveTime() {
|
||||
this.lastActiveTime = Date.now()
|
||||
},
|
||||
|
||||
/**
|
||||
* 检查登录状态
|
||||
*/
|
||||
checkLoginStatus() {
|
||||
const token = storage.get(APP_CONFIG.storage.tokenKey)
|
||||
const user = storage.get(APP_CONFIG.storage.userKey)
|
||||
|
||||
if (token && user && !this.isTokenExpired) {
|
||||
this.accessToken = token
|
||||
this.userInfo = user
|
||||
this.isLoggedIn = true
|
||||
this.lastActiveTime = Date.now()
|
||||
return true
|
||||
} else {
|
||||
this.logout()
|
||||
return false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 清除用户数据
|
||||
*/
|
||||
clearUserData() {
|
||||
this.$reset()
|
||||
storage.remove(APP_CONFIG.storage.tokenKey)
|
||||
storage.remove(APP_CONFIG.storage.userKey)
|
||||
storage.remove(APP_CONFIG.storage.settingsKey)
|
||||
}
|
||||
},
|
||||
|
||||
// 持久化配置
|
||||
persist: {
|
||||
key: 'user-store',
|
||||
storage: {
|
||||
getItem: (key) => uni.getStorageSync(key),
|
||||
setItem: (key, value) => uni.setStorageSync(key, value),
|
||||
removeItem: (key) => uni.removeStorageSync(key)
|
||||
},
|
||||
paths: ['userInfo', 'isLoggedIn', 'accessToken', 'refreshToken', 'permissions', 'roles', 'loginTime', 'settings']
|
||||
}
|
||||
})
|
||||
283
mini_program/common/styles/base.scss
Normal file
283
mini_program/common/styles/base.scss
Normal file
@@ -0,0 +1,283 @@
|
||||
// 基础样式重置
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
|
||||
font-size: $font-size-base;
|
||||
line-height: $line-height-normal;
|
||||
color: $text-primary;
|
||||
background-color: $bg-secondary;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
// 链接样式
|
||||
a {
|
||||
color: $primary-color;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
color: darken($primary-color, 10%);
|
||||
}
|
||||
|
||||
&:active {
|
||||
color: darken($primary-color, 20%);
|
||||
}
|
||||
}
|
||||
|
||||
// 图片样式
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
// 按钮基础样式
|
||||
button {
|
||||
border: none;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
background: transparent;
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
// 输入框基础样式
|
||||
input,
|
||||
textarea {
|
||||
border: none;
|
||||
outline: none;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
background: transparent;
|
||||
|
||||
&::placeholder {
|
||||
color: $text-muted;
|
||||
}
|
||||
}
|
||||
|
||||
// 列表样式
|
||||
ul,
|
||||
ol {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
// 表格样式
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
// 通用工具类
|
||||
.text-left { text-align: left; }
|
||||
.text-center { text-align: center; }
|
||||
.text-right { text-align: right; }
|
||||
|
||||
.text-primary { color: $text-primary; }
|
||||
.text-secondary { color: $text-secondary; }
|
||||
.text-muted { color: $text-muted; }
|
||||
.text-light { color: $text-light; }
|
||||
.text-white { color: $text-white; }
|
||||
.text-success { color: $success-color; }
|
||||
.text-warning { color: $warning-color; }
|
||||
.text-error { color: $error-color; }
|
||||
.text-info { color: $info-color; }
|
||||
|
||||
.bg-primary { background-color: $bg-primary; }
|
||||
.bg-secondary { background-color: $bg-secondary; }
|
||||
.bg-light { background-color: $bg-light; }
|
||||
.bg-dark { background-color: $bg-dark; }
|
||||
.bg-success { background-color: $success-color; }
|
||||
.bg-warning { background-color: $warning-color; }
|
||||
.bg-error { background-color: $error-color; }
|
||||
.bg-info { background-color: $info-color; }
|
||||
|
||||
.font-xs { font-size: $font-size-xs; }
|
||||
.font-sm { font-size: $font-size-sm; }
|
||||
.font-base { font-size: $font-size-base; }
|
||||
.font-lg { font-size: $font-size-lg; }
|
||||
.font-xl { font-size: $font-size-xl; }
|
||||
.font-2xl { font-size: $font-size-2xl; }
|
||||
.font-3xl { font-size: $font-size-3xl; }
|
||||
|
||||
.font-light { font-weight: $font-weight-light; }
|
||||
.font-normal { font-weight: $font-weight-normal; }
|
||||
.font-medium { font-weight: $font-weight-medium; }
|
||||
.font-semibold { font-weight: $font-weight-semibold; }
|
||||
.font-bold { font-weight: $font-weight-bold; }
|
||||
|
||||
.m-0 { margin: 0; }
|
||||
.m-xs { margin: $spacing-xs; }
|
||||
.m-sm { margin: $spacing-sm; }
|
||||
.m-base { margin: $spacing-base; }
|
||||
.m-md { margin: $spacing-md; }
|
||||
.m-lg { margin: $spacing-lg; }
|
||||
.m-xl { margin: $spacing-xl; }
|
||||
|
||||
.mt-0 { margin-top: 0; }
|
||||
.mt-xs { margin-top: $spacing-xs; }
|
||||
.mt-sm { margin-top: $spacing-sm; }
|
||||
.mt-base { margin-top: $spacing-base; }
|
||||
.mt-md { margin-top: $spacing-md; }
|
||||
.mt-lg { margin-top: $spacing-lg; }
|
||||
.mt-xl { margin-top: $spacing-xl; }
|
||||
|
||||
.mb-0 { margin-bottom: 0; }
|
||||
.mb-xs { margin-bottom: $spacing-xs; }
|
||||
.mb-sm { margin-bottom: $spacing-sm; }
|
||||
.mb-base { margin-bottom: $spacing-base; }
|
||||
.mb-md { margin-bottom: $spacing-md; }
|
||||
.mb-lg { margin-bottom: $spacing-lg; }
|
||||
.mb-xl { margin-bottom: $spacing-xl; }
|
||||
|
||||
.ml-0 { margin-left: 0; }
|
||||
.ml-xs { margin-left: $spacing-xs; }
|
||||
.ml-sm { margin-left: $spacing-sm; }
|
||||
.ml-base { margin-left: $spacing-base; }
|
||||
.ml-md { margin-left: $spacing-md; }
|
||||
.ml-lg { margin-left: $spacing-lg; }
|
||||
.ml-xl { margin-left: $spacing-xl; }
|
||||
|
||||
.mr-0 { margin-right: 0; }
|
||||
.mr-xs { margin-right: $spacing-xs; }
|
||||
.mr-sm { margin-right: $spacing-sm; }
|
||||
.mr-base { margin-right: $spacing-base; }
|
||||
.mr-md { margin-right: $spacing-md; }
|
||||
.mr-lg { margin-right: $spacing-lg; }
|
||||
.mr-xl { margin-right: $spacing-xl; }
|
||||
|
||||
.p-0 { padding: 0; }
|
||||
.p-xs { padding: $spacing-xs; }
|
||||
.p-sm { padding: $spacing-sm; }
|
||||
.p-base { padding: $spacing-base; }
|
||||
.p-md { padding: $spacing-md; }
|
||||
.p-lg { padding: $spacing-lg; }
|
||||
.p-xl { padding: $spacing-xl; }
|
||||
|
||||
.pt-0 { padding-top: 0; }
|
||||
.pt-xs { padding-top: $spacing-xs; }
|
||||
.pt-sm { padding-top: $spacing-sm; }
|
||||
.pt-base { padding-top: $spacing-base; }
|
||||
.pt-md { padding-top: $spacing-md; }
|
||||
.pt-lg { padding-top: $spacing-lg; }
|
||||
.pt-xl { padding-top: $spacing-xl; }
|
||||
|
||||
.pb-0 { padding-bottom: 0; }
|
||||
.pb-xs { padding-bottom: $spacing-xs; }
|
||||
.pb-sm { padding-bottom: $spacing-sm; }
|
||||
.pb-base { padding-bottom: $spacing-base; }
|
||||
.pb-md { padding-bottom: $spacing-md; }
|
||||
.pb-lg { padding-bottom: $spacing-lg; }
|
||||
.pb-xl { padding-bottom: $spacing-xl; }
|
||||
|
||||
.pl-0 { padding-left: 0; }
|
||||
.pl-xs { padding-left: $spacing-xs; }
|
||||
.pl-sm { padding-left: $spacing-sm; }
|
||||
.pl-base { padding-left: $spacing-base; }
|
||||
.pl-md { padding-left: $spacing-md; }
|
||||
.pl-lg { padding-left: $spacing-lg; }
|
||||
.pl-xl { padding-left: $spacing-xl; }
|
||||
|
||||
.pr-0 { padding-right: 0; }
|
||||
.pr-xs { padding-right: $spacing-xs; }
|
||||
.pr-sm { padding-right: $spacing-sm; }
|
||||
.pr-base { padding-right: $spacing-base; }
|
||||
.pr-md { padding-right: $spacing-md; }
|
||||
.pr-lg { padding-right: $spacing-lg; }
|
||||
.pr-xl { padding-right: $spacing-xl; }
|
||||
|
||||
.flex { @include flex; }
|
||||
.flex-center { @include flex-center; }
|
||||
.flex-column { @include flex(column); }
|
||||
.flex-wrap { flex-wrap: wrap; }
|
||||
.flex-nowrap { flex-wrap: nowrap; }
|
||||
.flex-1 { flex: 1; }
|
||||
|
||||
.justify-start { justify-content: flex-start; }
|
||||
.justify-center { justify-content: center; }
|
||||
.justify-end { justify-content: flex-end; }
|
||||
.justify-between { justify-content: space-between; }
|
||||
.justify-around { justify-content: space-around; }
|
||||
|
||||
.items-start { align-items: flex-start; }
|
||||
.items-center { align-items: center; }
|
||||
.items-end { align-items: flex-end; }
|
||||
.items-stretch { align-items: stretch; }
|
||||
|
||||
.rounded-none { border-radius: $border-radius-none; }
|
||||
.rounded-sm { border-radius: $border-radius-sm; }
|
||||
.rounded { border-radius: $border-radius-base; }
|
||||
.rounded-md { border-radius: $border-radius-md; }
|
||||
.rounded-lg { border-radius: $border-radius-lg; }
|
||||
.rounded-xl { border-radius: $border-radius-xl; }
|
||||
.rounded-2xl { border-radius: $border-radius-2xl; }
|
||||
.rounded-full { border-radius: $border-radius-full; }
|
||||
|
||||
.shadow-sm { box-shadow: $shadow-sm; }
|
||||
.shadow { box-shadow: $shadow-base; }
|
||||
.shadow-md { box-shadow: $shadow-md; }
|
||||
.shadow-lg { box-shadow: $shadow-lg; }
|
||||
.shadow-xl { box-shadow: $shadow-xl; }
|
||||
.shadow-2xl { box-shadow: $shadow-2xl; }
|
||||
|
||||
.overflow-hidden { overflow: hidden; }
|
||||
.overflow-auto { overflow: auto; }
|
||||
.overflow-scroll { overflow: scroll; }
|
||||
|
||||
.relative { position: relative; }
|
||||
.absolute { position: absolute; }
|
||||
.fixed { position: fixed; }
|
||||
.sticky { position: sticky; }
|
||||
|
||||
.top-0 { top: 0; }
|
||||
.right-0 { right: 0; }
|
||||
.bottom-0 { bottom: 0; }
|
||||
.left-0 { left: 0; }
|
||||
|
||||
.w-full { width: 100%; }
|
||||
.h-full { height: 100%; }
|
||||
.w-screen { width: 100vw; }
|
||||
.h-screen { height: 100vh; }
|
||||
|
||||
.block { display: block; }
|
||||
.inline { display: inline; }
|
||||
.inline-block { display: inline-block; }
|
||||
.hidden { display: none; }
|
||||
|
||||
.opacity-0 { opacity: 0; }
|
||||
.opacity-25 { opacity: 0.25; }
|
||||
.opacity-50 { opacity: 0.5; }
|
||||
.opacity-75 { opacity: 0.75; }
|
||||
.opacity-100 { opacity: 1; }
|
||||
|
||||
.cursor-pointer { cursor: pointer; }
|
||||
.cursor-not-allowed { cursor: not-allowed; }
|
||||
|
||||
.select-none { user-select: none; }
|
||||
.select-text { user-select: text; }
|
||||
.select-all { user-select: all; }
|
||||
|
||||
.pointer-events-none { pointer-events: none; }
|
||||
.pointer-events-auto { pointer-events: auto; }
|
||||
|
||||
// 文本省略
|
||||
.ellipsis { @include text-ellipsis(1); }
|
||||
.ellipsis-2 { @include text-ellipsis(2); }
|
||||
.ellipsis-3 { @include text-ellipsis(3); }
|
||||
|
||||
// 安全区域
|
||||
.safe-area-top { @include safe-area-inset(padding-top, top); }
|
||||
.safe-area-bottom { @include safe-area-inset(padding-bottom, bottom); }
|
||||
.safe-area-left { @include safe-area-inset(padding-left, left); }
|
||||
.safe-area-right { @include safe-area-inset(padding-right, right); }
|
||||
267
mini_program/common/styles/mixins.scss
Normal file
267
mini_program/common/styles/mixins.scss
Normal file
@@ -0,0 +1,267 @@
|
||||
// 清除浮动
|
||||
@mixin clearfix {
|
||||
&::after {
|
||||
content: '';
|
||||
display: table;
|
||||
clear: both;
|
||||
}
|
||||
}
|
||||
|
||||
// 文本省略
|
||||
@mixin text-ellipsis($lines: 1) {
|
||||
@if $lines == 1 {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
} @else {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: $lines;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
// 居中对齐
|
||||
@mixin center($type: both) {
|
||||
position: absolute;
|
||||
@if $type == both {
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
} @else if $type == horizontal {
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
} @else if $type == vertical {
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
// Flex 布局
|
||||
@mixin flex($direction: row, $justify: flex-start, $align: stretch, $wrap: nowrap) {
|
||||
display: flex;
|
||||
flex-direction: $direction;
|
||||
justify-content: $justify;
|
||||
align-items: $align;
|
||||
flex-wrap: $wrap;
|
||||
}
|
||||
|
||||
// Flex 居中
|
||||
@mixin flex-center {
|
||||
@include flex(row, center, center);
|
||||
}
|
||||
|
||||
// 响应式断点
|
||||
@mixin respond-to($breakpoint) {
|
||||
@if $breakpoint == xs {
|
||||
@media (max-width: #{$breakpoint-sm - 1px}) {
|
||||
@content;
|
||||
}
|
||||
} @else if $breakpoint == sm {
|
||||
@media (min-width: #{$breakpoint-sm}) {
|
||||
@content;
|
||||
}
|
||||
} @else if $breakpoint == md {
|
||||
@media (min-width: #{$breakpoint-md}) {
|
||||
@content;
|
||||
}
|
||||
} @else if $breakpoint == lg {
|
||||
@media (min-width: #{$breakpoint-lg}) {
|
||||
@content;
|
||||
}
|
||||
} @else if $breakpoint == xl {
|
||||
@media (min-width: #{$breakpoint-xl}) {
|
||||
@content;
|
||||
}
|
||||
} @else if $breakpoint == 2xl {
|
||||
@media (min-width: #{$breakpoint-2xl}) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 按钮样式
|
||||
@mixin button-variant($bg-color, $text-color: $white, $border-color: $bg-color) {
|
||||
background-color: $bg-color;
|
||||
color: $text-color;
|
||||
border: 1px solid $border-color;
|
||||
|
||||
&:hover {
|
||||
background-color: darken($bg-color, 5%);
|
||||
border-color: darken($border-color, 5%);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: darken($bg-color, 10%);
|
||||
border-color: darken($border-color, 10%);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background-color: lighten($bg-color, 20%);
|
||||
border-color: lighten($border-color, 20%);
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
// 输入框样式
|
||||
@mixin input-variant($border-color: $input-border-color, $focus-color: $input-focus-border-color) {
|
||||
border: $input-border-width solid $border-color;
|
||||
border-radius: $input-border-radius;
|
||||
padding: $input-padding-y $input-padding-x;
|
||||
font-size: $font-size-base;
|
||||
line-height: $line-height-normal;
|
||||
transition: border-color $transition-base ease-in-out, box-shadow $transition-base ease-in-out;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: $focus-color;
|
||||
box-shadow: $input-focus-box-shadow;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background-color: $gray-100;
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
// 卡片样式
|
||||
@mixin card($padding: $card-padding, $radius: $card-border-radius, $shadow: $card-shadow) {
|
||||
background-color: $white;
|
||||
border-radius: $radius;
|
||||
padding: $padding;
|
||||
box-shadow: $shadow;
|
||||
border: 1px solid $card-border-color;
|
||||
}
|
||||
|
||||
// 头像样式
|
||||
@mixin avatar($size: $avatar-size-base) {
|
||||
width: $size;
|
||||
height: $size;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
// 徽章样式
|
||||
@mixin badge($bg-color: $primary-color, $text-color: $white) {
|
||||
display: inline-block;
|
||||
padding: $badge-padding-y $badge-padding-x;
|
||||
font-size: $badge-font-size;
|
||||
font-weight: $font-weight-medium;
|
||||
line-height: 1;
|
||||
color: $text-color;
|
||||
background-color: $bg-color;
|
||||
border-radius: $badge-border-radius;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
// 加载动画
|
||||
@mixin loading-spinner($size: $loading-size, $color: $loading-color) {
|
||||
width: $size;
|
||||
height: $size;
|
||||
border: 2px solid rgba($color, 0.2);
|
||||
border-top: 2px solid $color;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
// 渐变背景
|
||||
@mixin gradient-bg($start-color, $end-color, $direction: to right) {
|
||||
background: linear-gradient($direction, $start-color, $end-color);
|
||||
}
|
||||
|
||||
// 阴影效果
|
||||
@mixin box-shadow($shadow: $shadow-base) {
|
||||
box-shadow: $shadow;
|
||||
}
|
||||
|
||||
// 过渡动画
|
||||
@mixin transition($property: all, $duration: $transition-base, $timing: $ease-in-out) {
|
||||
transition: $property $duration $timing;
|
||||
}
|
||||
|
||||
// 隐藏滚动条
|
||||
@mixin hide-scrollbar {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// 自定义滚动条
|
||||
@mixin custom-scrollbar($width: 6px, $track-color: $gray-100, $thumb-color: $gray-300) {
|
||||
&::-webkit-scrollbar {
|
||||
width: $width;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: $track-color;
|
||||
border-radius: $width / 2;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: $thumb-color;
|
||||
border-radius: $width / 2;
|
||||
|
||||
&:hover {
|
||||
background: darken($thumb-color, 10%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 安全区域适配
|
||||
@mixin safe-area-inset($property, $direction: bottom) {
|
||||
#{$property}: constant(safe-area-inset-#{$direction});
|
||||
#{$property}: env(safe-area-inset-#{$direction});
|
||||
}
|
||||
|
||||
// 1px 边框解决方案
|
||||
@mixin border-1px($color: $border-color, $direction: bottom) {
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
#{$direction}: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1px;
|
||||
background-color: $color;
|
||||
transform: scaleY(0.5);
|
||||
transform-origin: 0 #{$direction};
|
||||
}
|
||||
}
|
||||
|
||||
// 毛玻璃效果
|
||||
@mixin backdrop-blur($blur: 10px, $bg-color: rgba(255, 255, 255, 0.8)) {
|
||||
background-color: $bg-color;
|
||||
backdrop-filter: blur($blur);
|
||||
-webkit-backdrop-filter: blur($blur);
|
||||
}
|
||||
|
||||
// 文字渐变
|
||||
@mixin text-gradient($start-color, $end-color, $direction: to right) {
|
||||
background: linear-gradient($direction, $start-color, $end-color);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
192
mini_program/common/styles/variables.scss
Normal file
192
mini_program/common/styles/variables.scss
Normal file
@@ -0,0 +1,192 @@
|
||||
// 颜色变量
|
||||
$primary-color: #2E8B57; // 海绿色 - 主色调
|
||||
$secondary-color: #32CD32; // 酸橙绿 - 辅助色
|
||||
$accent-color: #FFD700; // 金色 - 强调色
|
||||
$success-color: #28a745; // 成功色
|
||||
$warning-color: #ffc107; // 警告色
|
||||
$error-color: #dc3545; // 错误色
|
||||
$info-color: #17a2b8; // 信息色
|
||||
|
||||
// 中性色
|
||||
$white: #ffffff;
|
||||
$black: #000000;
|
||||
$gray-50: #f8f9fa;
|
||||
$gray-100: #e9ecef;
|
||||
$gray-200: #dee2e6;
|
||||
$gray-300: #ced4da;
|
||||
$gray-400: #adb5bd;
|
||||
$gray-500: #6c757d;
|
||||
$gray-600: #495057;
|
||||
$gray-700: #343a40;
|
||||
$gray-800: #212529;
|
||||
$gray-900: #0d1117;
|
||||
|
||||
// 文字颜色
|
||||
$text-primary: $gray-900;
|
||||
$text-secondary: $gray-600;
|
||||
$text-muted: $gray-500;
|
||||
$text-light: $gray-400;
|
||||
$text-white: $white;
|
||||
|
||||
// 背景色
|
||||
$bg-primary: $white;
|
||||
$bg-secondary: $gray-50;
|
||||
$bg-light: $gray-100;
|
||||
$bg-dark: $gray-800;
|
||||
|
||||
// 边框色
|
||||
$border-color: $gray-200;
|
||||
$border-light: $gray-100;
|
||||
$border-dark: $gray-300;
|
||||
|
||||
// 字体大小
|
||||
$font-size-xs: 10px;
|
||||
$font-size-sm: 12px;
|
||||
$font-size-base: 14px;
|
||||
$font-size-lg: 16px;
|
||||
$font-size-xl: 18px;
|
||||
$font-size-2xl: 20px;
|
||||
$font-size-3xl: 24px;
|
||||
$font-size-4xl: 28px;
|
||||
$font-size-5xl: 32px;
|
||||
|
||||
// 字体粗细
|
||||
$font-weight-light: 300;
|
||||
$font-weight-normal: 400;
|
||||
$font-weight-medium: 500;
|
||||
$font-weight-semibold: 600;
|
||||
$font-weight-bold: 700;
|
||||
|
||||
// 行高
|
||||
$line-height-tight: 1.2;
|
||||
$line-height-normal: 1.4;
|
||||
$line-height-relaxed: 1.6;
|
||||
$line-height-loose: 1.8;
|
||||
|
||||
// 间距
|
||||
$spacing-xs: 4px;
|
||||
$spacing-sm: 8px;
|
||||
$spacing-base: 12px;
|
||||
$spacing-md: 16px;
|
||||
$spacing-lg: 20px;
|
||||
$spacing-xl: 24px;
|
||||
$spacing-2xl: 32px;
|
||||
$spacing-3xl: 40px;
|
||||
$spacing-4xl: 48px;
|
||||
$spacing-5xl: 64px;
|
||||
|
||||
// 圆角
|
||||
$border-radius-none: 0;
|
||||
$border-radius-sm: 2px;
|
||||
$border-radius-base: 4px;
|
||||
$border-radius-md: 6px;
|
||||
$border-radius-lg: 8px;
|
||||
$border-radius-xl: 12px;
|
||||
$border-radius-2xl: 16px;
|
||||
$border-radius-full: 50%;
|
||||
|
||||
// 阴影
|
||||
$shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
$shadow-base: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
||||
$shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
$shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
$shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
$shadow-2xl: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
||||
|
||||
// Z-index
|
||||
$z-index-dropdown: 1000;
|
||||
$z-index-sticky: 1020;
|
||||
$z-index-fixed: 1030;
|
||||
$z-index-modal-backdrop: 1040;
|
||||
$z-index-modal: 1050;
|
||||
$z-index-popover: 1060;
|
||||
$z-index-tooltip: 1070;
|
||||
|
||||
// 断点
|
||||
$breakpoint-xs: 0;
|
||||
$breakpoint-sm: 576px;
|
||||
$breakpoint-md: 768px;
|
||||
$breakpoint-lg: 992px;
|
||||
$breakpoint-xl: 1200px;
|
||||
$breakpoint-2xl: 1400px;
|
||||
|
||||
// 容器最大宽度
|
||||
$container-max-width-sm: 540px;
|
||||
$container-max-width-md: 720px;
|
||||
$container-max-width-lg: 960px;
|
||||
$container-max-width-xl: 1140px;
|
||||
$container-max-width-2xl: 1320px;
|
||||
|
||||
// 动画时间
|
||||
$transition-fast: 0.15s;
|
||||
$transition-base: 0.3s;
|
||||
$transition-slow: 0.5s;
|
||||
|
||||
// 动画函数
|
||||
$ease-in: cubic-bezier(0.4, 0, 1, 1);
|
||||
$ease-out: cubic-bezier(0, 0, 0.2, 1);
|
||||
$ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
// 表单控件
|
||||
$input-height: 44px;
|
||||
$input-padding-x: $spacing-md;
|
||||
$input-padding-y: $spacing-sm;
|
||||
$input-border-width: 1px;
|
||||
$input-border-color: $border-color;
|
||||
$input-border-radius: $border-radius-md;
|
||||
$input-focus-border-color: $primary-color;
|
||||
$input-focus-box-shadow: 0 0 0 2px rgba(46, 139, 87, 0.2);
|
||||
|
||||
// 按钮
|
||||
$button-height: 44px;
|
||||
$button-padding-x: $spacing-lg;
|
||||
$button-padding-y: $spacing-sm;
|
||||
$button-border-radius: $border-radius-md;
|
||||
$button-font-weight: $font-weight-medium;
|
||||
|
||||
// 卡片
|
||||
$card-padding: $spacing-lg;
|
||||
$card-border-radius: $border-radius-lg;
|
||||
$card-border-color: $border-color;
|
||||
$card-shadow: $shadow-sm;
|
||||
|
||||
// 导航栏
|
||||
$navbar-height: 44px;
|
||||
$navbar-padding-x: $spacing-md;
|
||||
$navbar-background: $white;
|
||||
$navbar-border-color: $border-color;
|
||||
|
||||
// 标签栏
|
||||
$tabbar-height: 50px;
|
||||
$tabbar-background: $white;
|
||||
$tabbar-border-color: $border-color;
|
||||
$tabbar-active-color: $primary-color;
|
||||
$tabbar-inactive-color: $gray-500;
|
||||
|
||||
// 列表项
|
||||
$list-item-height: 44px;
|
||||
$list-item-padding-x: $spacing-md;
|
||||
$list-item-padding-y: $spacing-sm;
|
||||
$list-item-border-color: $border-light;
|
||||
|
||||
// 头像
|
||||
$avatar-size-xs: 24px;
|
||||
$avatar-size-sm: 32px;
|
||||
$avatar-size-base: 40px;
|
||||
$avatar-size-lg: 48px;
|
||||
$avatar-size-xl: 64px;
|
||||
$avatar-size-2xl: 80px;
|
||||
|
||||
// 徽章
|
||||
$badge-padding-x: 6px;
|
||||
$badge-padding-y: 2px;
|
||||
$badge-font-size: $font-size-xs;
|
||||
$badge-border-radius: $border-radius-full;
|
||||
|
||||
// 加载状态
|
||||
$loading-color: $primary-color;
|
||||
$loading-size: 20px;
|
||||
|
||||
// 空状态
|
||||
$empty-color: $gray-400;
|
||||
$empty-font-size: $font-size-sm;
|
||||
389
mini_program/common/utils/auth.js
Normal file
389
mini_program/common/utils/auth.js
Normal file
@@ -0,0 +1,389 @@
|
||||
// 认证工具类
|
||||
import { API_ENDPOINTS, buildURL } from '@/common/config/api'
|
||||
import request from './request'
|
||||
import storage from './storage'
|
||||
|
||||
class Auth {
|
||||
constructor() {
|
||||
this.token = storage.get('token') || ''
|
||||
this.userInfo = storage.get('userInfo') || null
|
||||
this.refreshPromise = null
|
||||
}
|
||||
|
||||
// 设置token
|
||||
setToken(token) {
|
||||
this.token = token
|
||||
storage.set('token', token)
|
||||
|
||||
// 设置全局数据
|
||||
const app = getApp()
|
||||
if (app) {
|
||||
app.globalData.token = token
|
||||
}
|
||||
}
|
||||
|
||||
// 获取token
|
||||
getToken() {
|
||||
return this.token
|
||||
}
|
||||
|
||||
// 设置用户信息
|
||||
setUserInfo(userInfo) {
|
||||
this.userInfo = userInfo
|
||||
storage.set('userInfo', userInfo)
|
||||
|
||||
// 设置全局数据
|
||||
const app = getApp()
|
||||
if (app) {
|
||||
app.globalData.userInfo = userInfo
|
||||
}
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
getUserInfo() {
|
||||
return this.userInfo
|
||||
}
|
||||
|
||||
// 检查是否已登录
|
||||
isLoggedIn() {
|
||||
return !!this.token && !!this.userInfo
|
||||
}
|
||||
|
||||
// 登录
|
||||
async login(credentials) {
|
||||
try {
|
||||
const res = await request({
|
||||
url: API_ENDPOINTS.AUTH.LOGIN,
|
||||
method: 'POST',
|
||||
data: credentials
|
||||
})
|
||||
|
||||
if (res.code === 200) {
|
||||
this.setToken(res.data.token)
|
||||
this.setUserInfo(res.data.userInfo)
|
||||
return res.data
|
||||
} else {
|
||||
throw new Error(res.message || '登录失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('登录失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 微信登录
|
||||
async wechatLogin(code) {
|
||||
try {
|
||||
const res = await request({
|
||||
url: API_ENDPOINTS.AUTH.WECHAT_LOGIN,
|
||||
method: 'POST',
|
||||
data: { code }
|
||||
})
|
||||
|
||||
if (res.code === 200) {
|
||||
this.setToken(res.data.token)
|
||||
this.setUserInfo(res.data.userInfo)
|
||||
return res.data
|
||||
} else {
|
||||
throw new Error(res.message || '微信登录失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('微信登录失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 退出登录
|
||||
async logout() {
|
||||
try {
|
||||
// 调用退出登录接口
|
||||
if (this.token) {
|
||||
await request({
|
||||
url: API_ENDPOINTS.AUTH.LOGOUT,
|
||||
method: 'POST'
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('退出登录接口调用失败:', error)
|
||||
} finally {
|
||||
// 清除本地数据
|
||||
this.clearAuth()
|
||||
}
|
||||
}
|
||||
|
||||
// 清除认证信息
|
||||
clearAuth() {
|
||||
this.token = ''
|
||||
this.userInfo = null
|
||||
storage.remove('token')
|
||||
storage.remove('userInfo')
|
||||
|
||||
// 清除全局数据
|
||||
const app = getApp()
|
||||
if (app) {
|
||||
app.globalData.token = ''
|
||||
app.globalData.userInfo = null
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新token
|
||||
async refreshToken() {
|
||||
// 防止重复刷新
|
||||
if (this.refreshPromise) {
|
||||
return this.refreshPromise
|
||||
}
|
||||
|
||||
this.refreshPromise = this._doRefreshToken()
|
||||
|
||||
try {
|
||||
const result = await this.refreshPromise
|
||||
return result
|
||||
} finally {
|
||||
this.refreshPromise = null
|
||||
}
|
||||
}
|
||||
|
||||
// 执行token刷新
|
||||
async _doRefreshToken() {
|
||||
try {
|
||||
const res = await request({
|
||||
url: API_ENDPOINTS.AUTH.REFRESH_TOKEN,
|
||||
method: 'POST',
|
||||
skipAuth: true // 跳过认证拦截
|
||||
})
|
||||
|
||||
if (res.code === 200) {
|
||||
this.setToken(res.data.token)
|
||||
if (res.data.userInfo) {
|
||||
this.setUserInfo(res.data.userInfo)
|
||||
}
|
||||
return res.data
|
||||
} else {
|
||||
throw new Error(res.message || 'Token刷新失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Token刷新失败:', error)
|
||||
// 刷新失败,清除认证信息
|
||||
this.clearAuth()
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 验证token有效性
|
||||
async validateToken() {
|
||||
if (!this.token) {
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await request({
|
||||
url: API_ENDPOINTS.AUTH.VALIDATE_TOKEN,
|
||||
method: 'POST'
|
||||
})
|
||||
|
||||
return res.code === 200
|
||||
} catch (error) {
|
||||
console.error('Token验证失败:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 检查登录状态并跳转
|
||||
checkLoginAndRedirect(redirectUrl = '/pages/auth/login') {
|
||||
if (!this.isLoggedIn()) {
|
||||
uni.navigateTo({
|
||||
url: redirectUrl
|
||||
})
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// 获取用户权限
|
||||
getUserPermissions() {
|
||||
if (!this.userInfo || !this.userInfo.permissions) {
|
||||
return []
|
||||
}
|
||||
return this.userInfo.permissions
|
||||
}
|
||||
|
||||
// 检查用户权限
|
||||
hasPermission(permission) {
|
||||
const permissions = this.getUserPermissions()
|
||||
return permissions.includes(permission)
|
||||
}
|
||||
|
||||
// 检查用户角色
|
||||
hasRole(role) {
|
||||
if (!this.userInfo || !this.userInfo.roles) {
|
||||
return false
|
||||
}
|
||||
return this.userInfo.roles.includes(role)
|
||||
}
|
||||
|
||||
// 获取用户头像
|
||||
getUserAvatar() {
|
||||
if (!this.userInfo) {
|
||||
return '/static/images/default-avatar.png'
|
||||
}
|
||||
return this.userInfo.avatar || '/static/images/default-avatar.png'
|
||||
}
|
||||
|
||||
// 获取用户昵称
|
||||
getUserNickname() {
|
||||
if (!this.userInfo) {
|
||||
return '未登录'
|
||||
}
|
||||
return this.userInfo.nickname || this.userInfo.phone || '用户'
|
||||
}
|
||||
|
||||
// 更新用户信息
|
||||
async updateUserInfo(userInfo) {
|
||||
try {
|
||||
const res = await request({
|
||||
url: API_ENDPOINTS.USER.UPDATE_PROFILE,
|
||||
method: 'PUT',
|
||||
data: userInfo
|
||||
})
|
||||
|
||||
if (res.code === 200) {
|
||||
this.setUserInfo(res.data)
|
||||
return res.data
|
||||
} else {
|
||||
throw new Error(res.message || '更新用户信息失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('更新用户信息失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 修改密码
|
||||
async changePassword(oldPassword, newPassword) {
|
||||
try {
|
||||
const res = await request({
|
||||
url: API_ENDPOINTS.AUTH.CHANGE_PASSWORD,
|
||||
method: 'POST',
|
||||
data: {
|
||||
oldPassword,
|
||||
newPassword
|
||||
}
|
||||
})
|
||||
|
||||
if (res.code === 200) {
|
||||
return true
|
||||
} else {
|
||||
throw new Error(res.message || '修改密码失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('修改密码失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 重置密码
|
||||
async resetPassword(phone, code, newPassword) {
|
||||
try {
|
||||
const res = await request({
|
||||
url: API_ENDPOINTS.AUTH.RESET_PASSWORD,
|
||||
method: 'POST',
|
||||
data: {
|
||||
phone,
|
||||
code,
|
||||
newPassword
|
||||
}
|
||||
})
|
||||
|
||||
if (res.code === 200) {
|
||||
return true
|
||||
} else {
|
||||
throw new Error(res.message || '重置密码失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('重置密码失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 发送验证码
|
||||
async sendVerificationCode(phone, type = 'login') {
|
||||
try {
|
||||
const res = await request({
|
||||
url: API_ENDPOINTS.AUTH.SEND_CODE,
|
||||
method: 'POST',
|
||||
data: {
|
||||
phone,
|
||||
type
|
||||
}
|
||||
})
|
||||
|
||||
if (res.code === 200) {
|
||||
return true
|
||||
} else {
|
||||
throw new Error(res.message || '发送验证码失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('发送验证码失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 绑定手机号
|
||||
async bindPhone(phone, code) {
|
||||
try {
|
||||
const res = await request({
|
||||
url: API_ENDPOINTS.USER.BIND_PHONE,
|
||||
method: 'POST',
|
||||
data: {
|
||||
phone,
|
||||
code
|
||||
}
|
||||
})
|
||||
|
||||
if (res.code === 200) {
|
||||
this.setUserInfo(res.data)
|
||||
return res.data
|
||||
} else {
|
||||
throw new Error(res.message || '绑定手机号失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('绑定手机号失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 上传头像
|
||||
async uploadAvatar(filePath) {
|
||||
try {
|
||||
const res = await uni.uploadFile({
|
||||
url: API_ENDPOINTS.USER.AVATAR_UPLOAD,
|
||||
filePath: filePath,
|
||||
name: 'avatar',
|
||||
header: {
|
||||
'Authorization': `Bearer ${this.token}`
|
||||
}
|
||||
})
|
||||
|
||||
const data = JSON.parse(res.data)
|
||||
if (data.code === 200) {
|
||||
// 更新用户信息中的头像
|
||||
const updatedUserInfo = {
|
||||
...this.userInfo,
|
||||
avatar: data.data.url
|
||||
}
|
||||
this.setUserInfo(updatedUserInfo)
|
||||
return data.data.url
|
||||
} else {
|
||||
throw new Error(data.message || '上传头像失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('上传头像失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建认证实例
|
||||
const auth = new Auth()
|
||||
|
||||
export default auth
|
||||
684
mini_program/common/utils/crypto.js
Normal file
684
mini_program/common/utils/crypto.js
Normal file
@@ -0,0 +1,684 @@
|
||||
/**
|
||||
* 加密解密工具类
|
||||
* 提供数据加密、解密、哈希等安全功能
|
||||
*/
|
||||
|
||||
import { createLogger } from './logger.js'
|
||||
|
||||
const logger = createLogger('Crypto')
|
||||
|
||||
/**
|
||||
* Base64 编码解码
|
||||
*/
|
||||
export const base64 = {
|
||||
/**
|
||||
* Base64 编码
|
||||
* @param {string} str - 要编码的字符串
|
||||
* @returns {string} 编码后的字符串
|
||||
*/
|
||||
encode(str) {
|
||||
try {
|
||||
// 在小程序环境中使用内置方法
|
||||
if (typeof wx !== 'undefined') {
|
||||
return wx.arrayBufferToBase64(
|
||||
new TextEncoder().encode(str).buffer
|
||||
)
|
||||
}
|
||||
|
||||
// 在其他环境中使用 btoa
|
||||
if (typeof btoa !== 'undefined') {
|
||||
return btoa(unescape(encodeURIComponent(str)))
|
||||
}
|
||||
|
||||
// 手动实现 Base64 编码
|
||||
return this._manualEncode(str)
|
||||
} catch (error) {
|
||||
logger.error('Base64 encode failed', { str }, error)
|
||||
return ''
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Base64 解码
|
||||
* @param {string} str - 要解码的字符串
|
||||
* @returns {string} 解码后的字符串
|
||||
*/
|
||||
decode(str) {
|
||||
try {
|
||||
// 在小程序环境中使用内置方法
|
||||
if (typeof wx !== 'undefined') {
|
||||
const buffer = wx.base64ToArrayBuffer(str)
|
||||
return new TextDecoder().decode(buffer)
|
||||
}
|
||||
|
||||
// 在其他环境中使用 atob
|
||||
if (typeof atob !== 'undefined') {
|
||||
return decodeURIComponent(escape(atob(str)))
|
||||
}
|
||||
|
||||
// 手动实现 Base64 解码
|
||||
return this._manualDecode(str)
|
||||
} catch (error) {
|
||||
logger.error('Base64 decode failed', { str }, error)
|
||||
return ''
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 手动 Base64 编码实现
|
||||
* @private
|
||||
*/
|
||||
_manualEncode(str) {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
|
||||
let result = ''
|
||||
let i = 0
|
||||
|
||||
while (i < str.length) {
|
||||
const a = str.charCodeAt(i++)
|
||||
const b = i < str.length ? str.charCodeAt(i++) : 0
|
||||
const c = i < str.length ? str.charCodeAt(i++) : 0
|
||||
|
||||
const bitmap = (a << 16) | (b << 8) | c
|
||||
|
||||
result += chars.charAt((bitmap >> 18) & 63)
|
||||
result += chars.charAt((bitmap >> 12) & 63)
|
||||
result += i - 2 < str.length ? chars.charAt((bitmap >> 6) & 63) : '='
|
||||
result += i - 1 < str.length ? chars.charAt(bitmap & 63) : '='
|
||||
}
|
||||
|
||||
return result
|
||||
},
|
||||
|
||||
/**
|
||||
* 手动 Base64 解码实现
|
||||
* @private
|
||||
*/
|
||||
_manualDecode(str) {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
|
||||
let result = ''
|
||||
let i = 0
|
||||
|
||||
str = str.replace(/[^A-Za-z0-9+/]/g, '')
|
||||
|
||||
while (i < str.length) {
|
||||
const encoded1 = chars.indexOf(str.charAt(i++))
|
||||
const encoded2 = chars.indexOf(str.charAt(i++))
|
||||
const encoded3 = chars.indexOf(str.charAt(i++))
|
||||
const encoded4 = chars.indexOf(str.charAt(i++))
|
||||
|
||||
const bitmap = (encoded1 << 18) | (encoded2 << 12) | (encoded3 << 6) | encoded4
|
||||
|
||||
result += String.fromCharCode((bitmap >> 16) & 255)
|
||||
if (encoded3 !== 64) result += String.fromCharCode((bitmap >> 8) & 255)
|
||||
if (encoded4 !== 64) result += String.fromCharCode(bitmap & 255)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* MD5 哈希算法
|
||||
*/
|
||||
export const md5 = {
|
||||
/**
|
||||
* 计算 MD5 哈希值
|
||||
* @param {string} str - 要计算哈希的字符串
|
||||
* @returns {string} MD5 哈希值
|
||||
*/
|
||||
hash(str) {
|
||||
try {
|
||||
return this._md5(str)
|
||||
} catch (error) {
|
||||
logger.error('MD5 hash failed', { str }, error)
|
||||
return ''
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* MD5 算法实现
|
||||
* @private
|
||||
*/
|
||||
_md5(str) {
|
||||
const rotateLeft = (value, amount) => {
|
||||
const lbits = value << amount
|
||||
const rbits = value >>> (32 - amount)
|
||||
return lbits | rbits
|
||||
}
|
||||
|
||||
const addUnsigned = (x, y) => {
|
||||
const x4 = x & 0x40000000
|
||||
const y4 = y & 0x40000000
|
||||
const x8 = x & 0x80000000
|
||||
const y8 = y & 0x80000000
|
||||
const result = (x & 0x3FFFFFFF) + (y & 0x3FFFFFFF)
|
||||
|
||||
if (x4 & y4) {
|
||||
return result ^ 0x80000000 ^ x8 ^ y8
|
||||
}
|
||||
if (x4 | y4) {
|
||||
if (result & 0x40000000) {
|
||||
return result ^ 0xC0000000 ^ x8 ^ y8
|
||||
} else {
|
||||
return result ^ 0x40000000 ^ x8 ^ y8
|
||||
}
|
||||
} else {
|
||||
return result ^ x8 ^ y8
|
||||
}
|
||||
}
|
||||
|
||||
const f = (x, y, z) => (x & y) | (~x & z)
|
||||
const g = (x, y, z) => (x & z) | (y & ~z)
|
||||
const h = (x, y, z) => x ^ y ^ z
|
||||
const i = (x, y, z) => y ^ (x | ~z)
|
||||
|
||||
const ff = (a, b, c, d, x, s, ac) => {
|
||||
a = addUnsigned(a, addUnsigned(addUnsigned(f(b, c, d), x), ac))
|
||||
return addUnsigned(rotateLeft(a, s), b)
|
||||
}
|
||||
|
||||
const gg = (a, b, c, d, x, s, ac) => {
|
||||
a = addUnsigned(a, addUnsigned(addUnsigned(g(b, c, d), x), ac))
|
||||
return addUnsigned(rotateLeft(a, s), b)
|
||||
}
|
||||
|
||||
const hh = (a, b, c, d, x, s, ac) => {
|
||||
a = addUnsigned(a, addUnsigned(addUnsigned(h(b, c, d), x), ac))
|
||||
return addUnsigned(rotateLeft(a, s), b)
|
||||
}
|
||||
|
||||
const ii = (a, b, c, d, x, s, ac) => {
|
||||
a = addUnsigned(a, addUnsigned(addUnsigned(i(b, c, d), x), ac))
|
||||
return addUnsigned(rotateLeft(a, s), b)
|
||||
}
|
||||
|
||||
const convertToWordArray = (str) => {
|
||||
let wordArray = []
|
||||
let messageLength = str.length
|
||||
let numberOfWords = (((messageLength + 8) - ((messageLength + 8) % 64)) / 64 + 1) * 16
|
||||
|
||||
for (let i = 0; i < numberOfWords; i++) {
|
||||
wordArray[i] = 0
|
||||
}
|
||||
|
||||
for (let i = 0; i < messageLength; i++) {
|
||||
wordArray[i >>> 2] |= (str.charCodeAt(i) & 0xFF) << (24 - (i % 4) * 8)
|
||||
}
|
||||
|
||||
wordArray[messageLength >>> 2] |= 0x80 << (24 - (messageLength % 4) * 8)
|
||||
wordArray[numberOfWords - 2] = messageLength << 3
|
||||
wordArray[numberOfWords - 1] = messageLength >>> 29
|
||||
|
||||
return wordArray
|
||||
}
|
||||
|
||||
const wordToHex = (value) => {
|
||||
let result = ''
|
||||
for (let i = 0; i <= 3; i++) {
|
||||
const byte = (value >>> (i * 8)) & 255
|
||||
result += ('0' + byte.toString(16)).slice(-2)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
const x = convertToWordArray(str)
|
||||
let a = 0x67452301
|
||||
let b = 0xEFCDAB89
|
||||
let c = 0x98BADCFE
|
||||
let d = 0x10325476
|
||||
|
||||
for (let k = 0; k < x.length; k += 16) {
|
||||
const AA = a
|
||||
const BB = b
|
||||
const CC = c
|
||||
const DD = d
|
||||
|
||||
a = ff(a, b, c, d, x[k], 7, 0xD76AA478)
|
||||
d = ff(d, a, b, c, x[k + 1], 12, 0xE8C7B756)
|
||||
c = ff(c, d, a, b, x[k + 2], 17, 0x242070DB)
|
||||
b = ff(b, c, d, a, x[k + 3], 22, 0xC1BDCEEE)
|
||||
a = ff(a, b, c, d, x[k + 4], 7, 0xF57C0FAF)
|
||||
d = ff(d, a, b, c, x[k + 5], 12, 0x4787C62A)
|
||||
c = ff(c, d, a, b, x[k + 6], 17, 0xA8304613)
|
||||
b = ff(b, c, d, a, x[k + 7], 22, 0xFD469501)
|
||||
a = ff(a, b, c, d, x[k + 8], 7, 0x698098D8)
|
||||
d = ff(d, a, b, c, x[k + 9], 12, 0x8B44F7AF)
|
||||
c = ff(c, d, a, b, x[k + 10], 17, 0xFFFF5BB1)
|
||||
b = ff(b, c, d, a, x[k + 11], 22, 0x895CD7BE)
|
||||
a = ff(a, b, c, d, x[k + 12], 7, 0x6B901122)
|
||||
d = ff(d, a, b, c, x[k + 13], 12, 0xFD987193)
|
||||
c = ff(c, d, a, b, x[k + 14], 17, 0xA679438E)
|
||||
b = ff(b, c, d, a, x[k + 15], 22, 0x49B40821)
|
||||
|
||||
a = gg(a, b, c, d, x[k + 1], 5, 0xF61E2562)
|
||||
d = gg(d, a, b, c, x[k + 6], 9, 0xC040B340)
|
||||
c = gg(c, d, a, b, x[k + 11], 14, 0x265E5A51)
|
||||
b = gg(b, c, d, a, x[k], 20, 0xE9B6C7AA)
|
||||
a = gg(a, b, c, d, x[k + 5], 5, 0xD62F105D)
|
||||
d = gg(d, a, b, c, x[k + 10], 9, 0x2441453)
|
||||
c = gg(c, d, a, b, x[k + 15], 14, 0xD8A1E681)
|
||||
b = gg(b, c, d, a, x[k + 4], 20, 0xE7D3FBC8)
|
||||
a = gg(a, b, c, d, x[k + 9], 5, 0x21E1CDE6)
|
||||
d = gg(d, a, b, c, x[k + 14], 9, 0xC33707D6)
|
||||
c = gg(c, d, a, b, x[k + 3], 14, 0xF4D50D87)
|
||||
b = gg(b, c, d, a, x[k + 8], 20, 0x455A14ED)
|
||||
a = gg(a, b, c, d, x[k + 13], 5, 0xA9E3E905)
|
||||
d = gg(d, a, b, c, x[k + 2], 9, 0xFCEFA3F8)
|
||||
c = gg(c, d, a, b, x[k + 7], 14, 0x676F02D9)
|
||||
b = gg(b, c, d, a, x[k + 12], 20, 0x8D2A4C8A)
|
||||
|
||||
a = hh(a, b, c, d, x[k + 5], 4, 0xFFFA3942)
|
||||
d = hh(d, a, b, c, x[k + 8], 11, 0x8771F681)
|
||||
c = hh(c, d, a, b, x[k + 11], 16, 0x6D9D6122)
|
||||
b = hh(b, c, d, a, x[k + 14], 23, 0xFDE5380C)
|
||||
a = hh(a, b, c, d, x[k + 1], 4, 0xA4BEEA44)
|
||||
d = hh(d, a, b, c, x[k + 4], 11, 0x4BDECFA9)
|
||||
c = hh(c, d, a, b, x[k + 7], 16, 0xF6BB4B60)
|
||||
b = hh(b, c, d, a, x[k + 10], 23, 0xBEBFBC70)
|
||||
a = hh(a, b, c, d, x[k + 13], 4, 0x289B7EC6)
|
||||
d = hh(d, a, b, c, x[k], 11, 0xEAA127FA)
|
||||
c = hh(c, d, a, b, x[k + 3], 16, 0xD4EF3085)
|
||||
b = hh(b, c, d, a, x[k + 6], 23, 0x4881D05)
|
||||
a = hh(a, b, c, d, x[k + 9], 4, 0xD9D4D039)
|
||||
d = hh(d, a, b, c, x[k + 12], 11, 0xE6DB99E5)
|
||||
c = hh(c, d, a, b, x[k + 15], 16, 0x1FA27CF8)
|
||||
b = hh(b, c, d, a, x[k + 2], 23, 0xC4AC5665)
|
||||
|
||||
a = ii(a, b, c, d, x[k], 6, 0xF4292244)
|
||||
d = ii(d, a, b, c, x[k + 7], 10, 0x432AFF97)
|
||||
c = ii(c, d, a, b, x[k + 14], 15, 0xAB9423A7)
|
||||
b = ii(b, c, d, a, x[k + 5], 21, 0xFC93A039)
|
||||
a = ii(a, b, c, d, x[k + 12], 6, 0x655B59C3)
|
||||
d = ii(d, a, b, c, x[k + 3], 10, 0x8F0CCC92)
|
||||
c = ii(c, d, a, b, x[k + 10], 15, 0xFFEFF47D)
|
||||
b = ii(b, c, d, a, x[k + 1], 21, 0x85845DD1)
|
||||
a = ii(a, b, c, d, x[k + 8], 6, 0x6FA87E4F)
|
||||
d = ii(d, a, b, c, x[k + 15], 10, 0xFE2CE6E0)
|
||||
c = ii(c, d, a, b, x[k + 6], 15, 0xA3014314)
|
||||
b = ii(b, c, d, a, x[k + 13], 21, 0x4E0811A1)
|
||||
a = ii(a, b, c, d, x[k + 4], 6, 0xF7537E82)
|
||||
d = ii(d, a, b, c, x[k + 11], 10, 0xBD3AF235)
|
||||
c = ii(c, d, a, b, x[k + 2], 15, 0x2AD7D2BB)
|
||||
b = ii(b, c, d, a, x[k + 9], 21, 0xEB86D391)
|
||||
|
||||
a = addUnsigned(a, AA)
|
||||
b = addUnsigned(b, BB)
|
||||
c = addUnsigned(c, CC)
|
||||
d = addUnsigned(d, DD)
|
||||
}
|
||||
|
||||
return (wordToHex(a) + wordToHex(b) + wordToHex(c) + wordToHex(d)).toLowerCase()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SHA256 哈希算法
|
||||
*/
|
||||
export const sha256 = {
|
||||
/**
|
||||
* 计算 SHA256 哈希值
|
||||
* @param {string} str - 要计算哈希的字符串
|
||||
* @returns {string} SHA256 哈希值
|
||||
*/
|
||||
hash(str) {
|
||||
try {
|
||||
return this._sha256(str)
|
||||
} catch (error) {
|
||||
logger.error('SHA256 hash failed', { str }, error)
|
||||
return ''
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* SHA256 算法实现
|
||||
* @private
|
||||
*/
|
||||
_sha256(str) {
|
||||
const K = [
|
||||
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
||||
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
||||
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
||||
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
||||
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
||||
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
||||
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
||||
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
|
||||
]
|
||||
|
||||
const rotr = (n, x) => (x >>> n) | (x << (32 - n))
|
||||
const ch = (x, y, z) => (x & y) ^ (~x & z)
|
||||
const maj = (x, y, z) => (x & y) ^ (x & z) ^ (y & z)
|
||||
const sigma0 = (x) => rotr(2, x) ^ rotr(13, x) ^ rotr(22, x)
|
||||
const sigma1 = (x) => rotr(6, x) ^ rotr(11, x) ^ rotr(25, x)
|
||||
const gamma0 = (x) => rotr(7, x) ^ rotr(18, x) ^ (x >>> 3)
|
||||
const gamma1 = (x) => rotr(17, x) ^ rotr(19, x) ^ (x >>> 10)
|
||||
|
||||
const utf8Encode = (str) => {
|
||||
return unescape(encodeURIComponent(str))
|
||||
}
|
||||
|
||||
const stringToBytes = (str) => {
|
||||
const bytes = []
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
bytes.push(str.charCodeAt(i))
|
||||
}
|
||||
return bytes
|
||||
}
|
||||
|
||||
const bytesToWords = (bytes) => {
|
||||
const words = []
|
||||
for (let i = 0, b = 0; i < bytes.length; i++, b += 8) {
|
||||
words[b >>> 5] |= bytes[i] << (24 - b % 32)
|
||||
}
|
||||
return words
|
||||
}
|
||||
|
||||
const wordsToHex = (words) => {
|
||||
const hex = []
|
||||
for (let i = 0; i < words.length * 32; i += 8) {
|
||||
hex.push(((words[i >>> 5] >>> (24 - i % 32)) & 0xFF).toString(16).padStart(2, '0'))
|
||||
}
|
||||
return hex.join('')
|
||||
}
|
||||
|
||||
// 预处理
|
||||
const message = utf8Encode(str)
|
||||
const messageBytes = stringToBytes(message)
|
||||
const messageBits = messageBytes.length * 8
|
||||
|
||||
// 添加填充
|
||||
messageBytes.push(0x80)
|
||||
while (messageBytes.length % 64 !== 56) {
|
||||
messageBytes.push(0x00)
|
||||
}
|
||||
|
||||
// 添加长度
|
||||
for (let i = 7; i >= 0; i--) {
|
||||
messageBytes.push((messageBits >>> (i * 8)) & 0xFF)
|
||||
}
|
||||
|
||||
const words = bytesToWords(messageBytes)
|
||||
|
||||
// 初始哈希值
|
||||
let h0 = 0x6a09e667
|
||||
let h1 = 0xbb67ae85
|
||||
let h2 = 0x3c6ef372
|
||||
let h3 = 0xa54ff53a
|
||||
let h4 = 0x510e527f
|
||||
let h5 = 0x9b05688c
|
||||
let h6 = 0x1f83d9ab
|
||||
let h7 = 0x5be0cd19
|
||||
|
||||
// 处理消息块
|
||||
for (let i = 0; i < words.length; i += 16) {
|
||||
const w = words.slice(i, i + 16)
|
||||
|
||||
// 扩展消息调度
|
||||
for (let j = 16; j < 64; j++) {
|
||||
w[j] = (gamma1(w[j - 2]) + w[j - 7] + gamma0(w[j - 15]) + w[j - 16]) >>> 0
|
||||
}
|
||||
|
||||
let a = h0, b = h1, c = h2, d = h3, e = h4, f = h5, g = h6, h = h7
|
||||
|
||||
// 主循环
|
||||
for (let j = 0; j < 64; j++) {
|
||||
const t1 = (h + sigma1(e) + ch(e, f, g) + K[j] + w[j]) >>> 0
|
||||
const t2 = (sigma0(a) + maj(a, b, c)) >>> 0
|
||||
|
||||
h = g
|
||||
g = f
|
||||
f = e
|
||||
e = (d + t1) >>> 0
|
||||
d = c
|
||||
c = b
|
||||
b = a
|
||||
a = (t1 + t2) >>> 0
|
||||
}
|
||||
|
||||
// 更新哈希值
|
||||
h0 = (h0 + a) >>> 0
|
||||
h1 = (h1 + b) >>> 0
|
||||
h2 = (h2 + c) >>> 0
|
||||
h3 = (h3 + d) >>> 0
|
||||
h4 = (h4 + e) >>> 0
|
||||
h5 = (h5 + f) >>> 0
|
||||
h6 = (h6 + g) >>> 0
|
||||
h7 = (h7 + h) >>> 0
|
||||
}
|
||||
|
||||
return wordsToHex([h0, h1, h2, h3, h4, h5, h6, h7])
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 简单的对称加密(仅用于演示,不建议用于生产环境)
|
||||
*/
|
||||
export const simpleEncrypt = {
|
||||
/**
|
||||
* 简单加密
|
||||
* @param {string} text - 要加密的文本
|
||||
* @param {string} key - 加密密钥
|
||||
* @returns {string} 加密后的文本
|
||||
*/
|
||||
encrypt(text, key) {
|
||||
try {
|
||||
let result = ''
|
||||
const keyLength = key.length
|
||||
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const textChar = text.charCodeAt(i)
|
||||
const keyChar = key.charCodeAt(i % keyLength)
|
||||
const encryptedChar = textChar ^ keyChar
|
||||
result += String.fromCharCode(encryptedChar)
|
||||
}
|
||||
|
||||
return base64.encode(result)
|
||||
} catch (error) {
|
||||
logger.error('Simple encrypt failed', { text, key }, error)
|
||||
return ''
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 简单解密
|
||||
* @param {string} encryptedText - 要解密的文本
|
||||
* @param {string} key - 解密密钥
|
||||
* @returns {string} 解密后的文本
|
||||
*/
|
||||
decrypt(encryptedText, key) {
|
||||
try {
|
||||
const decodedText = base64.decode(encryptedText)
|
||||
let result = ''
|
||||
const keyLength = key.length
|
||||
|
||||
for (let i = 0; i < decodedText.length; i++) {
|
||||
const encryptedChar = decodedText.charCodeAt(i)
|
||||
const keyChar = key.charCodeAt(i % keyLength)
|
||||
const decryptedChar = encryptedChar ^ keyChar
|
||||
result += String.fromCharCode(decryptedChar)
|
||||
}
|
||||
|
||||
return result
|
||||
} catch (error) {
|
||||
logger.error('Simple decrypt failed', { encryptedText, key }, error)
|
||||
return ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 随机数生成器
|
||||
*/
|
||||
export const random = {
|
||||
/**
|
||||
* 生成随机字符串
|
||||
* @param {number} length - 字符串长度
|
||||
* @param {string} chars - 字符集
|
||||
* @returns {string} 随机字符串
|
||||
*/
|
||||
string(length = 16, chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789') {
|
||||
let result = ''
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length))
|
||||
}
|
||||
return result
|
||||
},
|
||||
|
||||
/**
|
||||
* 生成随机数字
|
||||
* @param {number} min - 最小值
|
||||
* @param {number} max - 最大值
|
||||
* @returns {number} 随机数字
|
||||
*/
|
||||
number(min = 0, max = 100) {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min
|
||||
},
|
||||
|
||||
/**
|
||||
* 生成UUID
|
||||
* @returns {string} UUID
|
||||
*/
|
||||
uuid() {
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
||||
const r = Math.random() * 16 | 0
|
||||
const v = c === 'x' ? r : (r & 0x3 | 0x8)
|
||||
return v.toString(16)
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 生成随机盐值
|
||||
* @param {number} length - 盐值长度
|
||||
* @returns {string} 盐值
|
||||
*/
|
||||
salt(length = 32) {
|
||||
return this.string(length, 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 密码工具
|
||||
*/
|
||||
export const password = {
|
||||
/**
|
||||
* 生成密码哈希
|
||||
* @param {string} password - 原始密码
|
||||
* @param {string} salt - 盐值
|
||||
* @returns {string} 密码哈希
|
||||
*/
|
||||
hash(password, salt = null) {
|
||||
try {
|
||||
const usedSalt = salt || random.salt()
|
||||
const combined = password + usedSalt
|
||||
const hashed = sha256.hash(combined)
|
||||
return `${hashed}:${usedSalt}`
|
||||
} catch (error) {
|
||||
logger.error('Password hash failed', { password, salt }, error)
|
||||
return ''
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 验证密码
|
||||
* @param {string} password - 原始密码
|
||||
* @param {string} hashedPassword - 哈希密码
|
||||
* @returns {boolean} 是否匹配
|
||||
*/
|
||||
verify(password, hashedPassword) {
|
||||
try {
|
||||
const [hash, salt] = hashedPassword.split(':')
|
||||
if (!hash || !salt) {
|
||||
return false
|
||||
}
|
||||
|
||||
const newHash = this.hash(password, salt)
|
||||
return newHash === hashedPassword
|
||||
} catch (error) {
|
||||
logger.error('Password verify failed', { password, hashedPassword }, error)
|
||||
return false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 生成强密码
|
||||
* @param {number} length - 密码长度
|
||||
* @returns {string} 强密码
|
||||
*/
|
||||
generate(length = 12) {
|
||||
const lowercase = 'abcdefghijklmnopqrstuvwxyz'
|
||||
const uppercase = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
||||
const numbers = '0123456789'
|
||||
const symbols = '!@#$%^&*()_+-=[]{}|;:,.<>?'
|
||||
|
||||
let password = ''
|
||||
let chars = lowercase + uppercase + numbers + symbols
|
||||
|
||||
// 确保包含各种字符类型
|
||||
password += lowercase[Math.floor(Math.random() * lowercase.length)]
|
||||
password += uppercase[Math.floor(Math.random() * uppercase.length)]
|
||||
password += numbers[Math.floor(Math.random() * numbers.length)]
|
||||
password += symbols[Math.floor(Math.random() * symbols.length)]
|
||||
|
||||
// 填充剩余长度
|
||||
for (let i = 4; i < length; i++) {
|
||||
password += chars[Math.floor(Math.random() * chars.length)]
|
||||
}
|
||||
|
||||
// 打乱密码字符顺序
|
||||
return password.split('').sort(() => Math.random() - 0.5).join('')
|
||||
},
|
||||
|
||||
/**
|
||||
* 检查密码强度
|
||||
* @param {string} password - 密码
|
||||
* @returns {Object} 强度信息
|
||||
*/
|
||||
strength(password) {
|
||||
const checks = {
|
||||
length: password.length >= 8,
|
||||
lowercase: /[a-z]/.test(password),
|
||||
uppercase: /[A-Z]/.test(password),
|
||||
numbers: /\d/.test(password),
|
||||
symbols: /[!@#$%^&*()_+\-=\[\]{}|;:,.<>?]/.test(password)
|
||||
}
|
||||
|
||||
const score = Object.values(checks).filter(Boolean).length
|
||||
|
||||
let level = 'weak'
|
||||
if (score >= 4) level = 'strong'
|
||||
else if (score >= 3) level = 'medium'
|
||||
|
||||
return {
|
||||
score,
|
||||
level,
|
||||
checks,
|
||||
suggestions: this._getPasswordSuggestions(checks)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取密码建议
|
||||
* @private
|
||||
*/
|
||||
_getPasswordSuggestions(checks) {
|
||||
const suggestions = []
|
||||
|
||||
if (!checks.length) suggestions.push('密码长度至少8位')
|
||||
if (!checks.lowercase) suggestions.push('包含小写字母')
|
||||
if (!checks.uppercase) suggestions.push('包含大写字母')
|
||||
if (!checks.numbers) suggestions.push('包含数字')
|
||||
if (!checks.symbols) suggestions.push('包含特殊字符')
|
||||
|
||||
return suggestions
|
||||
}
|
||||
}
|
||||
|
||||
// 导出所有加密工具
|
||||
export default {
|
||||
base64,
|
||||
md5,
|
||||
sha256,
|
||||
simpleEncrypt,
|
||||
random,
|
||||
password
|
||||
}
|
||||
423
mini_program/common/utils/format.js
Normal file
423
mini_program/common/utils/format.js
Normal file
@@ -0,0 +1,423 @@
|
||||
// 格式化工具函数
|
||||
|
||||
/**
|
||||
* 格式化时间
|
||||
* @param {number|string|Date} timestamp 时间戳或时间字符串
|
||||
* @param {string} format 格式化模板
|
||||
* @returns {string} 格式化后的时间字符串
|
||||
*/
|
||||
export function formatTime(timestamp, format = 'YYYY-MM-DD HH:mm:ss') {
|
||||
if (!timestamp) return ''
|
||||
|
||||
const date = new Date(timestamp)
|
||||
|
||||
if (isNaN(date.getTime())) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const year = date.getFullYear()
|
||||
const month = date.getMonth() + 1
|
||||
const day = date.getDate()
|
||||
const hour = date.getHours()
|
||||
const minute = date.getMinutes()
|
||||
const second = date.getSeconds()
|
||||
|
||||
return format
|
||||
.replace('YYYY', year)
|
||||
.replace('MM', month.toString().padStart(2, '0'))
|
||||
.replace('DD', day.toString().padStart(2, '0'))
|
||||
.replace('HH', hour.toString().padStart(2, '0'))
|
||||
.replace('mm', minute.toString().padStart(2, '0'))
|
||||
.replace('ss', second.toString().padStart(2, '0'))
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化日期
|
||||
* @param {number|string|Date} timestamp 时间戳或时间字符串
|
||||
* @param {string} format 格式化模板
|
||||
* @returns {string} 格式化后的日期字符串
|
||||
*/
|
||||
export function formatDate(timestamp, format = 'YYYY-MM-DD') {
|
||||
return formatTime(timestamp, format)
|
||||
}
|
||||
|
||||
/**
|
||||
* 相对时间格式化
|
||||
* @param {number|string|Date} timestamp 时间戳
|
||||
* @returns {string} 相对时间字符串
|
||||
*/
|
||||
export function formatRelativeTime(timestamp) {
|
||||
if (!timestamp) return ''
|
||||
|
||||
const date = new Date(timestamp)
|
||||
const now = new Date()
|
||||
const diff = now - date
|
||||
|
||||
if (diff < 0) {
|
||||
return '未来时间'
|
||||
}
|
||||
|
||||
const minute = 60 * 1000
|
||||
const hour = 60 * minute
|
||||
const day = 24 * hour
|
||||
const week = 7 * day
|
||||
const month = 30 * day
|
||||
const year = 365 * day
|
||||
|
||||
if (diff < minute) {
|
||||
return '刚刚'
|
||||
} else if (diff < hour) {
|
||||
return `${Math.floor(diff / minute)}分钟前`
|
||||
} else if (diff < day) {
|
||||
return `${Math.floor(diff / hour)}小时前`
|
||||
} else if (diff < week) {
|
||||
return `${Math.floor(diff / day)}天前`
|
||||
} else if (diff < month) {
|
||||
return `${Math.floor(diff / week)}周前`
|
||||
} else if (diff < year) {
|
||||
return `${Math.floor(diff / month)}个月前`
|
||||
} else {
|
||||
return `${Math.floor(diff / year)}年前`
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化数字
|
||||
* @param {number} number 数字
|
||||
* @param {number} decimals 小数位数
|
||||
* @param {string} thousandsSeparator 千位分隔符
|
||||
* @param {string} decimalSeparator 小数分隔符
|
||||
* @returns {string} 格式化后的数字字符串
|
||||
*/
|
||||
export function formatNumber(number, decimals = 2, thousandsSeparator = ',', decimalSeparator = '.') {
|
||||
if (isNaN(number) || number === null || number === undefined) {
|
||||
return '0'
|
||||
}
|
||||
|
||||
const num = parseFloat(number)
|
||||
const parts = num.toFixed(decimals).split('.')
|
||||
|
||||
// 添加千位分隔符
|
||||
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, thousandsSeparator)
|
||||
|
||||
return parts.join(decimalSeparator)
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化货币
|
||||
* @param {number} amount 金额
|
||||
* @param {string} currency 货币符号
|
||||
* @param {number} decimals 小数位数
|
||||
* @returns {string} 格式化后的货币字符串
|
||||
*/
|
||||
export function formatCurrency(amount, currency = '¥', decimals = 2) {
|
||||
const formattedAmount = formatNumber(amount, decimals)
|
||||
return `${currency}${formattedAmount}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化百分比
|
||||
* @param {number} value 数值
|
||||
* @param {number} decimals 小数位数
|
||||
* @returns {string} 格式化后的百分比字符串
|
||||
*/
|
||||
export function formatPercentage(value, decimals = 2) {
|
||||
if (isNaN(value) || value === null || value === undefined) {
|
||||
return '0%'
|
||||
}
|
||||
|
||||
return `${(value * 100).toFixed(decimals)}%`
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化文件大小
|
||||
* @param {number} bytes 字节数
|
||||
* @param {number} decimals 小数位数
|
||||
* @returns {string} 格式化后的文件大小字符串
|
||||
*/
|
||||
export function formatFileSize(bytes, decimals = 2) {
|
||||
if (bytes === 0) return '0 B'
|
||||
|
||||
const k = 1024
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
|
||||
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(decimals))} ${sizes[i]}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化手机号
|
||||
* @param {string} phone 手机号
|
||||
* @param {string} separator 分隔符
|
||||
* @returns {string} 格式化后的手机号
|
||||
*/
|
||||
export function formatPhone(phone, separator = ' ') {
|
||||
if (!phone) return ''
|
||||
|
||||
const cleanPhone = phone.replace(/\D/g, '')
|
||||
|
||||
if (cleanPhone.length === 11) {
|
||||
return `${cleanPhone.slice(0, 3)}${separator}${cleanPhone.slice(3, 7)}${separator}${cleanPhone.slice(7)}`
|
||||
}
|
||||
|
||||
return phone
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐藏手机号中间4位
|
||||
* @param {string} phone 手机号
|
||||
* @returns {string} 隐藏后的手机号
|
||||
*/
|
||||
export function hidePhone(phone) {
|
||||
if (!phone) return ''
|
||||
|
||||
const cleanPhone = phone.replace(/\D/g, '')
|
||||
|
||||
if (cleanPhone.length === 11) {
|
||||
return `${cleanPhone.slice(0, 3)}****${cleanPhone.slice(7)}`
|
||||
}
|
||||
|
||||
return phone
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化身份证号
|
||||
* @param {string} idCard 身份证号
|
||||
* @returns {string} 格式化后的身份证号
|
||||
*/
|
||||
export function formatIdCard(idCard) {
|
||||
if (!idCard) return ''
|
||||
|
||||
const cleanIdCard = idCard.replace(/\s/g, '')
|
||||
|
||||
if (cleanIdCard.length === 18) {
|
||||
return `${cleanIdCard.slice(0, 6)} ${cleanIdCard.slice(6, 14)} ${cleanIdCard.slice(14)}`
|
||||
} else if (cleanIdCard.length === 15) {
|
||||
return `${cleanIdCard.slice(0, 6)} ${cleanIdCard.slice(6, 12)} ${cleanIdCard.slice(12)}`
|
||||
}
|
||||
|
||||
return idCard
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐藏身份证号中间部分
|
||||
* @param {string} idCard 身份证号
|
||||
* @returns {string} 隐藏后的身份证号
|
||||
*/
|
||||
export function hideIdCard(idCard) {
|
||||
if (!idCard) return ''
|
||||
|
||||
const cleanIdCard = idCard.replace(/\s/g, '')
|
||||
|
||||
if (cleanIdCard.length === 18) {
|
||||
return `${cleanIdCard.slice(0, 6)}********${cleanIdCard.slice(14)}`
|
||||
} else if (cleanIdCard.length === 15) {
|
||||
return `${cleanIdCard.slice(0, 6)}*****${cleanIdCard.slice(11)}`
|
||||
}
|
||||
|
||||
return idCard
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化银行卡号
|
||||
* @param {string} cardNumber 银行卡号
|
||||
* @param {string} separator 分隔符
|
||||
* @returns {string} 格式化后的银行卡号
|
||||
*/
|
||||
export function formatBankCard(cardNumber, separator = ' ') {
|
||||
if (!cardNumber) return ''
|
||||
|
||||
const cleanNumber = cardNumber.replace(/\D/g, '')
|
||||
|
||||
return cleanNumber.replace(/(.{4})/g, `$1${separator}`).trim()
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐藏银行卡号中间部分
|
||||
* @param {string} cardNumber 银行卡号
|
||||
* @returns {string} 隐藏后的银行卡号
|
||||
*/
|
||||
export function hideBankCard(cardNumber) {
|
||||
if (!cardNumber) return ''
|
||||
|
||||
const cleanNumber = cardNumber.replace(/\D/g, '')
|
||||
|
||||
if (cleanNumber.length >= 8) {
|
||||
return `${cleanNumber.slice(0, 4)} **** **** ${cleanNumber.slice(-4)}`
|
||||
}
|
||||
|
||||
return cardNumber
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化地址
|
||||
* @param {object} address 地址对象
|
||||
* @returns {string} 格式化后的地址字符串
|
||||
*/
|
||||
export function formatAddress(address) {
|
||||
if (!address) return ''
|
||||
|
||||
const parts = []
|
||||
|
||||
if (address.province) parts.push(address.province)
|
||||
if (address.city) parts.push(address.city)
|
||||
if (address.district) parts.push(address.district)
|
||||
if (address.street) parts.push(address.street)
|
||||
if (address.detail) parts.push(address.detail)
|
||||
|
||||
return parts.join('')
|
||||
}
|
||||
|
||||
/**
|
||||
* 截断文本
|
||||
* @param {string} text 文本
|
||||
* @param {number} length 最大长度
|
||||
* @param {string} suffix 后缀
|
||||
* @returns {string} 截断后的文本
|
||||
*/
|
||||
export function truncateText(text, length = 50, suffix = '...') {
|
||||
if (!text) return ''
|
||||
|
||||
if (text.length <= length) {
|
||||
return text
|
||||
}
|
||||
|
||||
return text.slice(0, length) + suffix
|
||||
}
|
||||
|
||||
/**
|
||||
* 首字母大写
|
||||
* @param {string} str 字符串
|
||||
* @returns {string} 首字母大写的字符串
|
||||
*/
|
||||
export function capitalize(str) {
|
||||
if (!str) return ''
|
||||
|
||||
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase()
|
||||
}
|
||||
|
||||
/**
|
||||
* 驼峰转下划线
|
||||
* @param {string} str 驼峰字符串
|
||||
* @returns {string} 下划线字符串
|
||||
*/
|
||||
export function camelToSnake(str) {
|
||||
if (!str) return ''
|
||||
|
||||
return str.replace(/([A-Z])/g, '_$1').toLowerCase()
|
||||
}
|
||||
|
||||
/**
|
||||
* 下划线转驼峰
|
||||
* @param {string} str 下划线字符串
|
||||
* @returns {string} 驼峰字符串
|
||||
*/
|
||||
export function snakeToCamel(str) {
|
||||
if (!str) return ''
|
||||
|
||||
return str.replace(/_([a-z])/g, (match, letter) => letter.toUpperCase())
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化距离
|
||||
* @param {number} distance 距离(米)
|
||||
* @returns {string} 格式化后的距离字符串
|
||||
*/
|
||||
export function formatDistance(distance) {
|
||||
if (isNaN(distance) || distance < 0) {
|
||||
return '未知距离'
|
||||
}
|
||||
|
||||
if (distance < 1000) {
|
||||
return `${Math.round(distance)}m`
|
||||
} else {
|
||||
return `${(distance / 1000).toFixed(1)}km`
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化重量
|
||||
* @param {number} weight 重量(克)
|
||||
* @param {string} unit 单位
|
||||
* @returns {string} 格式化后的重量字符串
|
||||
*/
|
||||
export function formatWeight(weight, unit = 'auto') {
|
||||
if (isNaN(weight) || weight < 0) {
|
||||
return '0g'
|
||||
}
|
||||
|
||||
if (unit === 'auto') {
|
||||
if (weight < 1000) {
|
||||
return `${Math.round(weight)}g`
|
||||
} else if (weight < 1000000) {
|
||||
return `${(weight / 1000).toFixed(1)}kg`
|
||||
} else {
|
||||
return `${(weight / 1000000).toFixed(1)}t`
|
||||
}
|
||||
}
|
||||
|
||||
switch (unit) {
|
||||
case 'g':
|
||||
return `${Math.round(weight)}g`
|
||||
case 'kg':
|
||||
return `${(weight / 1000).toFixed(1)}kg`
|
||||
case 't':
|
||||
return `${(weight / 1000000).toFixed(1)}t`
|
||||
default:
|
||||
return `${weight}${unit}`
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化温度
|
||||
* @param {number} temperature 温度
|
||||
* @param {string} unit 单位(C/F)
|
||||
* @returns {string} 格式化后的温度字符串
|
||||
*/
|
||||
export function formatTemperature(temperature, unit = 'C') {
|
||||
if (isNaN(temperature)) {
|
||||
return '--°C'
|
||||
}
|
||||
|
||||
const temp = parseFloat(temperature).toFixed(1)
|
||||
return `${temp}°${unit}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化湿度
|
||||
* @param {number} humidity 湿度
|
||||
* @returns {string} 格式化后的湿度字符串
|
||||
*/
|
||||
export function formatHumidity(humidity) {
|
||||
if (isNaN(humidity)) {
|
||||
return '--%'
|
||||
}
|
||||
|
||||
return `${Math.round(humidity)}%`
|
||||
}
|
||||
|
||||
// 默认导出所有格式化函数
|
||||
export default {
|
||||
formatTime,
|
||||
formatDate,
|
||||
formatRelativeTime,
|
||||
formatNumber,
|
||||
formatCurrency,
|
||||
formatPercentage,
|
||||
formatFileSize,
|
||||
formatPhone,
|
||||
hidePhone,
|
||||
formatIdCard,
|
||||
hideIdCard,
|
||||
formatBankCard,
|
||||
hideBankCard,
|
||||
formatAddress,
|
||||
truncateText,
|
||||
capitalize,
|
||||
camelToSnake,
|
||||
snakeToCamel,
|
||||
formatDistance,
|
||||
formatWeight,
|
||||
formatTemperature,
|
||||
formatHumidity
|
||||
}
|
||||
470
mini_program/common/utils/logger.js
Normal file
470
mini_program/common/utils/logger.js
Normal file
@@ -0,0 +1,470 @@
|
||||
/**
|
||||
* 日志工具类
|
||||
* 提供统一的日志记录功能,支持不同级别的日志输出
|
||||
*/
|
||||
|
||||
import { LOG_CONFIG, CONFIG } from '@/common/config/app.config.js'
|
||||
|
||||
// 日志级别枚举
|
||||
export const LOG_LEVELS = {
|
||||
ERROR: 0,
|
||||
WARN: 1,
|
||||
INFO: 2,
|
||||
DEBUG: 3
|
||||
}
|
||||
|
||||
// 日志级别名称映射
|
||||
const LEVEL_NAMES = {
|
||||
[LOG_LEVELS.ERROR]: 'ERROR',
|
||||
[LOG_LEVELS.WARN]: 'WARN',
|
||||
[LOG_LEVELS.INFO]: 'INFO',
|
||||
[LOG_LEVELS.DEBUG]: 'DEBUG'
|
||||
}
|
||||
|
||||
// 日志级别颜色映射
|
||||
const LEVEL_COLORS = {
|
||||
[LOG_LEVELS.ERROR]: '#ff4757',
|
||||
[LOG_LEVELS.WARN]: '#faad14',
|
||||
[LOG_LEVELS.INFO]: '#1890ff',
|
||||
[LOG_LEVELS.DEBUG]: '#52c41a'
|
||||
}
|
||||
|
||||
// 当前日志级别
|
||||
const CURRENT_LEVEL = LOG_LEVELS[LOG_CONFIG.CURRENT_LEVEL.toUpperCase()] || LOG_LEVELS.INFO
|
||||
|
||||
// 日志缓存队列
|
||||
let logQueue = []
|
||||
let flushTimer = null
|
||||
|
||||
/**
|
||||
* 日志记录器类
|
||||
*/
|
||||
class Logger {
|
||||
constructor(module = 'APP') {
|
||||
this.module = module
|
||||
this.startTime = Date.now()
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录错误日志
|
||||
* @param {string} message - 日志消息
|
||||
* @param {any} data - 附加数据
|
||||
* @param {Error} error - 错误对象
|
||||
*/
|
||||
error(message, data = null, error = null) {
|
||||
this._log(LOG_LEVELS.ERROR, message, data, error)
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录警告日志
|
||||
* @param {string} message - 日志消息
|
||||
* @param {any} data - 附加数据
|
||||
*/
|
||||
warn(message, data = null) {
|
||||
this._log(LOG_LEVELS.WARN, message, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录信息日志
|
||||
* @param {string} message - 日志消息
|
||||
* @param {any} data - 附加数据
|
||||
*/
|
||||
info(message, data = null) {
|
||||
this._log(LOG_LEVELS.INFO, message, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录调试日志
|
||||
* @param {string} message - 日志消息
|
||||
* @param {any} data - 附加数据
|
||||
*/
|
||||
debug(message, data = null) {
|
||||
this._log(LOG_LEVELS.DEBUG, message, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录性能日志
|
||||
* @param {string} operation - 操作名称
|
||||
* @param {number} duration - 耗时(毫秒)
|
||||
* @param {any} data - 附加数据
|
||||
*/
|
||||
performance(operation, duration, data = null) {
|
||||
this._log(LOG_LEVELS.INFO, `Performance: ${operation}`, {
|
||||
duration: `${duration}ms`,
|
||||
...data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录API请求日志
|
||||
* @param {string} method - 请求方法
|
||||
* @param {string} url - 请求URL
|
||||
* @param {any} params - 请求参数
|
||||
* @param {number} status - 响应状态码
|
||||
* @param {number} duration - 请求耗时
|
||||
*/
|
||||
api(method, url, params, status, duration) {
|
||||
const level = status >= 400 ? LOG_LEVELS.ERROR : LOG_LEVELS.INFO
|
||||
this._log(level, `API Request: ${method} ${url}`, {
|
||||
params,
|
||||
status,
|
||||
duration: `${duration}ms`
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录用户行为日志
|
||||
* @param {string} action - 用户行为
|
||||
* @param {any} data - 行为数据
|
||||
*/
|
||||
userAction(action, data = null) {
|
||||
this._log(LOG_LEVELS.INFO, `User Action: ${action}`, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录页面访问日志
|
||||
* @param {string} page - 页面路径
|
||||
* @param {any} params - 页面参数
|
||||
*/
|
||||
pageView(page, params = null) {
|
||||
this._log(LOG_LEVELS.INFO, `Page View: ${page}`, params)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建子模块日志记录器
|
||||
* @param {string} subModule - 子模块名称
|
||||
* @returns {Logger} 子模块日志记录器
|
||||
*/
|
||||
createChild(subModule) {
|
||||
return new Logger(`${this.module}:${subModule}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 内部日志记录方法
|
||||
* @private
|
||||
*/
|
||||
_log(level, message, data = null, error = null) {
|
||||
// 检查日志级别
|
||||
if (level > CURRENT_LEVEL) {
|
||||
return
|
||||
}
|
||||
|
||||
const timestamp = new Date().toISOString()
|
||||
const levelName = LEVEL_NAMES[level]
|
||||
const logEntry = {
|
||||
timestamp,
|
||||
level: levelName,
|
||||
module: this.module,
|
||||
message,
|
||||
data,
|
||||
error: error ? {
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
stack: error.stack
|
||||
} : null,
|
||||
userAgent: this._getUserAgent(),
|
||||
url: this._getCurrentUrl()
|
||||
}
|
||||
|
||||
// 控制台输出
|
||||
if (LOG_CONFIG.OUTPUT.console) {
|
||||
this._consoleOutput(level, logEntry)
|
||||
}
|
||||
|
||||
// 添加到队列
|
||||
if (LOG_CONFIG.OUTPUT.remote) {
|
||||
this._addToQueue(logEntry)
|
||||
}
|
||||
|
||||
// 文件输出(小程序不支持)
|
||||
if (LOG_CONFIG.OUTPUT.file && typeof wx === 'undefined') {
|
||||
this._fileOutput(logEntry)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 控制台输出
|
||||
* @private
|
||||
*/
|
||||
_consoleOutput(level, logEntry) {
|
||||
const { timestamp, level: levelName, module, message, data, error } = logEntry
|
||||
const color = LEVEL_COLORS[level]
|
||||
const prefix = `[${timestamp}] [${levelName}] [${module}]`
|
||||
|
||||
if (typeof console !== 'undefined') {
|
||||
const args = [`%c${prefix} ${message}`, `color: ${color}; font-weight: bold;`]
|
||||
|
||||
if (data) {
|
||||
args.push('\nData:', data)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
args.push('\nError:', error)
|
||||
}
|
||||
|
||||
switch (level) {
|
||||
case LOG_LEVELS.ERROR:
|
||||
console.error(...args)
|
||||
break
|
||||
case LOG_LEVELS.WARN:
|
||||
console.warn(...args)
|
||||
break
|
||||
case LOG_LEVELS.INFO:
|
||||
console.info(...args)
|
||||
break
|
||||
case LOG_LEVELS.DEBUG:
|
||||
console.debug(...args)
|
||||
break
|
||||
default:
|
||||
console.log(...args)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加到远程日志队列
|
||||
* @private
|
||||
*/
|
||||
_addToQueue(logEntry) {
|
||||
logQueue.push(logEntry)
|
||||
|
||||
// 达到批量大小时立即发送
|
||||
if (logQueue.length >= LOG_CONFIG.REMOTE.batchSize) {
|
||||
this._flushLogs()
|
||||
} else {
|
||||
// 设置定时发送
|
||||
this._scheduleFlush()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 调度日志发送
|
||||
* @private
|
||||
*/
|
||||
_scheduleFlush() {
|
||||
if (flushTimer) {
|
||||
return
|
||||
}
|
||||
|
||||
flushTimer = setTimeout(() => {
|
||||
this._flushLogs()
|
||||
}, LOG_CONFIG.REMOTE.flushInterval)
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送日志到远程服务器
|
||||
* @private
|
||||
*/
|
||||
_flushLogs() {
|
||||
if (logQueue.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const logs = [...logQueue]
|
||||
logQueue = []
|
||||
|
||||
if (flushTimer) {
|
||||
clearTimeout(flushTimer)
|
||||
flushTimer = null
|
||||
}
|
||||
|
||||
// 发送日志
|
||||
this._sendLogs(logs).catch(error => {
|
||||
console.error('Failed to send logs:', error)
|
||||
// 发送失败时重新加入队列
|
||||
logQueue.unshift(...logs)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送日志到服务器
|
||||
* @private
|
||||
*/
|
||||
async _sendLogs(logs) {
|
||||
try {
|
||||
const response = await uni.request({
|
||||
url: LOG_CONFIG.REMOTE.url,
|
||||
method: 'POST',
|
||||
data: {
|
||||
logs,
|
||||
appInfo: this._getAppInfo()
|
||||
},
|
||||
header: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
timeout: 10000
|
||||
})
|
||||
|
||||
if (response.statusCode !== 200) {
|
||||
throw new Error(`HTTP ${response.statusCode}: ${response.data}`)
|
||||
}
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户代理信息
|
||||
* @private
|
||||
*/
|
||||
_getUserAgent() {
|
||||
try {
|
||||
const systemInfo = uni.getSystemInfoSync()
|
||||
return `${systemInfo.platform} ${systemInfo.system} ${systemInfo.version}`
|
||||
} catch (error) {
|
||||
return 'Unknown'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前页面URL
|
||||
* @private
|
||||
*/
|
||||
_getCurrentUrl() {
|
||||
try {
|
||||
const pages = getCurrentPages()
|
||||
if (pages.length > 0) {
|
||||
const currentPage = pages[pages.length - 1]
|
||||
return currentPage.route || 'Unknown'
|
||||
}
|
||||
} catch (error) {
|
||||
// ignore
|
||||
}
|
||||
return 'Unknown'
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取应用信息
|
||||
* @private
|
||||
*/
|
||||
_getAppInfo() {
|
||||
try {
|
||||
const systemInfo = uni.getSystemInfoSync()
|
||||
return {
|
||||
platform: systemInfo.platform,
|
||||
system: systemInfo.system,
|
||||
version: systemInfo.version,
|
||||
appVersion: CONFIG.APP_VERSION || '1.0.0',
|
||||
timestamp: Date.now()
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
platform: 'Unknown',
|
||||
system: 'Unknown',
|
||||
version: 'Unknown',
|
||||
appVersion: '1.0.0',
|
||||
timestamp: Date.now()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件输出(仅在非小程序环境下可用)
|
||||
* @private
|
||||
*/
|
||||
_fileOutput(logEntry) {
|
||||
// 小程序环境下不支持文件操作
|
||||
if (typeof wx !== 'undefined' || typeof uni !== 'undefined') {
|
||||
return
|
||||
}
|
||||
|
||||
// 在其他环境下可以实现文件日志
|
||||
console.log('File logging not implemented for this environment')
|
||||
}
|
||||
}
|
||||
|
||||
// 默认日志记录器实例
|
||||
const defaultLogger = new Logger('APP')
|
||||
|
||||
// 导出日志记录器工厂函数
|
||||
export const createLogger = (module) => {
|
||||
return new Logger(module)
|
||||
}
|
||||
|
||||
// 导出便捷方法
|
||||
export const log = {
|
||||
error: (message, data, error) => defaultLogger.error(message, data, error),
|
||||
warn: (message, data) => defaultLogger.warn(message, data),
|
||||
info: (message, data) => defaultLogger.info(message, data),
|
||||
debug: (message, data) => defaultLogger.debug(message, data),
|
||||
performance: (operation, duration, data) => defaultLogger.performance(operation, duration, data),
|
||||
api: (method, url, params, status, duration) => defaultLogger.api(method, url, params, status, duration),
|
||||
userAction: (action, data) => defaultLogger.userAction(action, data),
|
||||
pageView: (page, params) => defaultLogger.pageView(page, params)
|
||||
}
|
||||
|
||||
// 全局错误处理
|
||||
if (typeof uni !== 'undefined') {
|
||||
uni.onError((error) => {
|
||||
defaultLogger.error('Global Error', null, new Error(error))
|
||||
})
|
||||
|
||||
uni.onUnhandledRejection((event) => {
|
||||
defaultLogger.error('Unhandled Promise Rejection', {
|
||||
reason: event.reason
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 页面性能监控
|
||||
export const performanceMonitor = {
|
||||
/**
|
||||
* 开始性能监控
|
||||
* @param {string} name - 监控名称
|
||||
* @returns {function} 结束监控的函数
|
||||
*/
|
||||
start(name) {
|
||||
const startTime = Date.now()
|
||||
|
||||
return (data = null) => {
|
||||
const duration = Date.now() - startTime
|
||||
defaultLogger.performance(name, duration, data)
|
||||
return duration
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 监控异步操作
|
||||
* @param {string} name - 操作名称
|
||||
* @param {Promise} promise - 异步操作
|
||||
* @returns {Promise} 原始Promise
|
||||
*/
|
||||
async monitor(name, promise) {
|
||||
const startTime = Date.now()
|
||||
|
||||
try {
|
||||
const result = await promise
|
||||
const duration = Date.now() - startTime
|
||||
defaultLogger.performance(name, duration, { success: true })
|
||||
return result
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime
|
||||
defaultLogger.performance(name, duration, { success: false, error: error.message })
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 监控函数执行
|
||||
* @param {string} name - 函数名称
|
||||
* @param {function} fn - 要监控的函数
|
||||
* @returns {any} 函数执行结果
|
||||
*/
|
||||
measureSync(name, fn) {
|
||||
const startTime = Date.now()
|
||||
|
||||
try {
|
||||
const result = fn()
|
||||
const duration = Date.now() - startTime
|
||||
defaultLogger.performance(name, duration, { success: true })
|
||||
return result
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime
|
||||
defaultLogger.performance(name, duration, { success: false, error: error.message })
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 导出默认日志记录器
|
||||
export default defaultLogger
|
||||
470
mini_program/common/utils/permission.js
Normal file
470
mini_program/common/utils/permission.js
Normal file
@@ -0,0 +1,470 @@
|
||||
/**
|
||||
* 权限管理工具类
|
||||
* 提供用户权限验证、角色管理等功能
|
||||
*/
|
||||
|
||||
import { PERMISSION_CONFIG } from '@/common/config/app.config.js'
|
||||
import { useUserStore } from '@/common/store/modules/user.js'
|
||||
import { createLogger } from './logger.js'
|
||||
|
||||
const logger = createLogger('Permission')
|
||||
|
||||
/**
|
||||
* 权限管理器类
|
||||
*/
|
||||
class PermissionManager {
|
||||
constructor() {
|
||||
this.userStore = null
|
||||
this.permissionCache = new Map()
|
||||
this.roleCache = new Map()
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化权限管理器
|
||||
*/
|
||||
init() {
|
||||
try {
|
||||
this.userStore = useUserStore()
|
||||
this.clearCache()
|
||||
logger.info('Permission manager initialized')
|
||||
} catch (error) {
|
||||
logger.error('Failed to initialize permission manager', null, error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否有指定权限
|
||||
* @param {string|Array} permissions - 权限或权限数组
|
||||
* @param {string} operator - 操作符:'and'(所有权限)或 'or'(任一权限)
|
||||
* @returns {boolean} 是否有权限
|
||||
*/
|
||||
hasPermission(permissions, operator = 'and') {
|
||||
try {
|
||||
if (!this.userStore) {
|
||||
this.init()
|
||||
}
|
||||
|
||||
const user = this.userStore.userInfo
|
||||
if (!user || !user.id) {
|
||||
logger.debug('User not logged in, permission denied')
|
||||
return false
|
||||
}
|
||||
|
||||
// 超级管理员拥有所有权限
|
||||
if (this.isSuperAdmin(user)) {
|
||||
return true
|
||||
}
|
||||
|
||||
const userPermissions = this.getUserPermissions(user)
|
||||
const requiredPermissions = Array.isArray(permissions) ? permissions : [permissions]
|
||||
|
||||
if (operator === 'and') {
|
||||
return requiredPermissions.every(permission => this.checkSinglePermission(permission, userPermissions))
|
||||
} else {
|
||||
return requiredPermissions.some(permission => this.checkSinglePermission(permission, userPermissions))
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error checking permission', { permissions, operator }, error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否有指定角色
|
||||
* @param {string|Array} roles - 角色或角色数组
|
||||
* @param {string} operator - 操作符:'and'(所有角色)或 'or'(任一角色)
|
||||
* @returns {boolean} 是否有角色
|
||||
*/
|
||||
hasRole(roles, operator = 'or') {
|
||||
try {
|
||||
if (!this.userStore) {
|
||||
this.init()
|
||||
}
|
||||
|
||||
const user = this.userStore.userInfo
|
||||
if (!user || !user.id) {
|
||||
return false
|
||||
}
|
||||
|
||||
const userRoles = this.getUserRoles(user)
|
||||
const requiredRoles = Array.isArray(roles) ? roles : [roles]
|
||||
|
||||
if (operator === 'and') {
|
||||
return requiredRoles.every(role => userRoles.includes(role))
|
||||
} else {
|
||||
return requiredRoles.some(role => userRoles.includes(role))
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error checking role', { roles, operator }, error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为超级管理员
|
||||
* @param {Object} user - 用户信息
|
||||
* @returns {boolean} 是否为超级管理员
|
||||
*/
|
||||
isSuperAdmin(user = null) {
|
||||
try {
|
||||
const targetUser = user || (this.userStore && this.userStore.userInfo)
|
||||
if (!targetUser) {
|
||||
return false
|
||||
}
|
||||
|
||||
const userRoles = this.getUserRoles(targetUser)
|
||||
return userRoles.includes(PERMISSION_CONFIG.ROLES.SUPER_ADMIN)
|
||||
} catch (error) {
|
||||
logger.error('Error checking super admin', { user }, error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户权限列表
|
||||
* @param {Object} user - 用户信息
|
||||
* @returns {Array} 权限列表
|
||||
*/
|
||||
getUserPermissions(user) {
|
||||
try {
|
||||
const cacheKey = `permissions_${user.id}`
|
||||
|
||||
if (this.permissionCache.has(cacheKey)) {
|
||||
return this.permissionCache.get(cacheKey)
|
||||
}
|
||||
|
||||
const userRoles = this.getUserRoles(user)
|
||||
const permissions = new Set()
|
||||
|
||||
// 根据角色获取权限
|
||||
userRoles.forEach(role => {
|
||||
const rolePermissions = PERMISSION_CONFIG.ROLE_PERMISSIONS[role] || []
|
||||
rolePermissions.forEach(permission => {
|
||||
if (permission === '*') {
|
||||
// 添加所有权限
|
||||
Object.values(PERMISSION_CONFIG.PERMISSIONS).forEach(p => permissions.add(p))
|
||||
} else if (permission.endsWith(':*')) {
|
||||
// 添加模块所有权限
|
||||
const module = permission.replace(':*', '')
|
||||
Object.values(PERMISSION_CONFIG.PERMISSIONS)
|
||||
.filter(p => p.startsWith(module + ':'))
|
||||
.forEach(p => permissions.add(p))
|
||||
} else {
|
||||
permissions.add(permission)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 添加用户特定权限
|
||||
if (user.permissions && Array.isArray(user.permissions)) {
|
||||
user.permissions.forEach(permission => permissions.add(permission))
|
||||
}
|
||||
|
||||
const result = Array.from(permissions)
|
||||
this.permissionCache.set(cacheKey, result)
|
||||
|
||||
return result
|
||||
} catch (error) {
|
||||
logger.error('Error getting user permissions', { user }, error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户角色列表
|
||||
* @param {Object} user - 用户信息
|
||||
* @returns {Array} 角色列表
|
||||
*/
|
||||
getUserRoles(user) {
|
||||
try {
|
||||
const cacheKey = `roles_${user.id}`
|
||||
|
||||
if (this.roleCache.has(cacheKey)) {
|
||||
return this.roleCache.get(cacheKey)
|
||||
}
|
||||
|
||||
let roles = []
|
||||
|
||||
// 从用户信息中获取角色
|
||||
if (user.roles && Array.isArray(user.roles)) {
|
||||
roles = [...user.roles]
|
||||
} else if (user.role) {
|
||||
roles = [user.role]
|
||||
}
|
||||
|
||||
// 默认用户角色
|
||||
if (roles.length === 0) {
|
||||
roles = [PERMISSION_CONFIG.ROLES.USER]
|
||||
}
|
||||
|
||||
this.roleCache.set(cacheKey, roles)
|
||||
return roles
|
||||
} catch (error) {
|
||||
logger.error('Error getting user roles', { user }, error)
|
||||
return [PERMISSION_CONFIG.ROLES.USER]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查单个权限
|
||||
* @private
|
||||
*/
|
||||
checkSinglePermission(permission, userPermissions) {
|
||||
// 直接匹配
|
||||
if (userPermissions.includes(permission)) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 通配符匹配
|
||||
const parts = permission.split(':')
|
||||
if (parts.length === 2) {
|
||||
const [module, action] = parts
|
||||
const wildcardPermission = `${module}:*`
|
||||
if (userPermissions.includes(wildcardPermission)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除权限缓存
|
||||
*/
|
||||
clearCache() {
|
||||
this.permissionCache.clear()
|
||||
this.roleCache.clear()
|
||||
logger.debug('Permission cache cleared')
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新用户权限
|
||||
* @param {string} userId - 用户ID
|
||||
*/
|
||||
refreshUserPermissions(userId) {
|
||||
const permissionKey = `permissions_${userId}`
|
||||
const roleKey = `roles_${userId}`
|
||||
|
||||
this.permissionCache.delete(permissionKey)
|
||||
this.roleCache.delete(roleKey)
|
||||
|
||||
logger.debug('User permissions refreshed', { userId })
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取权限描述
|
||||
* @param {string} permission - 权限标识
|
||||
* @returns {string} 权限描述
|
||||
*/
|
||||
getPermissionDescription(permission) {
|
||||
const descriptions = {
|
||||
// 用户管理
|
||||
'user:create': '创建用户',
|
||||
'user:read': '查看用户',
|
||||
'user:update': '更新用户',
|
||||
'user:delete': '删除用户',
|
||||
|
||||
// 牲畜管理
|
||||
'livestock:create': '添加牲畜',
|
||||
'livestock:read': '查看牲畜',
|
||||
'livestock:update': '更新牲畜',
|
||||
'livestock:delete': '删除牲畜',
|
||||
|
||||
// 健康管理
|
||||
'health:create': '添加健康记录',
|
||||
'health:read': '查看健康记录',
|
||||
'health:update': '更新健康记录',
|
||||
'health:delete': '删除健康记录',
|
||||
|
||||
// 交易管理
|
||||
'trade:create': '创建交易',
|
||||
'trade:read': '查看交易',
|
||||
'trade:update': '更新交易',
|
||||
'trade:delete': '删除交易',
|
||||
|
||||
// 财务管理
|
||||
'finance:create': '创建财务记录',
|
||||
'finance:read': '查看财务记录',
|
||||
'finance:update': '更新财务记录',
|
||||
'finance:delete': '删除财务记录',
|
||||
|
||||
// 报表管理
|
||||
'report:create': '创建报表',
|
||||
'report:read': '查看报表',
|
||||
'report:export': '导出报表',
|
||||
|
||||
// 系统管理
|
||||
'system:config': '系统配置',
|
||||
'system:log': '系统日志',
|
||||
'system:backup': '系统备份'
|
||||
}
|
||||
|
||||
return descriptions[permission] || permission
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取角色描述
|
||||
* @param {string} role - 角色标识
|
||||
* @returns {string} 角色描述
|
||||
*/
|
||||
getRoleDescription(role) {
|
||||
const descriptions = {
|
||||
[PERMISSION_CONFIG.ROLES.SUPER_ADMIN]: '超级管理员',
|
||||
[PERMISSION_CONFIG.ROLES.ADMIN]: '管理员',
|
||||
[PERMISSION_CONFIG.ROLES.MANAGER]: '经理',
|
||||
[PERMISSION_CONFIG.ROLES.OPERATOR]: '操作员',
|
||||
[PERMISSION_CONFIG.ROLES.VIEWER]: '查看者',
|
||||
[PERMISSION_CONFIG.ROLES.USER]: '普通用户'
|
||||
}
|
||||
|
||||
return descriptions[role] || role
|
||||
}
|
||||
}
|
||||
|
||||
// 创建权限管理器实例
|
||||
const permissionManager = new PermissionManager()
|
||||
|
||||
// 权限检查装饰器
|
||||
export const requirePermission = (permissions, operator = 'and') => {
|
||||
return (target, propertyKey, descriptor) => {
|
||||
const originalMethod = descriptor.value
|
||||
|
||||
descriptor.value = function (...args) {
|
||||
if (!permissionManager.hasPermission(permissions, operator)) {
|
||||
logger.warn('Permission denied', {
|
||||
method: propertyKey,
|
||||
permissions,
|
||||
operator
|
||||
})
|
||||
|
||||
uni.showToast({
|
||||
title: '权限不足',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
})
|
||||
|
||||
return Promise.reject(new Error('Permission denied'))
|
||||
}
|
||||
|
||||
return originalMethod.apply(this, args)
|
||||
}
|
||||
|
||||
return descriptor
|
||||
}
|
||||
}
|
||||
|
||||
// 角色检查装饰器
|
||||
export const requireRole = (roles, operator = 'or') => {
|
||||
return (target, propertyKey, descriptor) => {
|
||||
const originalMethod = descriptor.value
|
||||
|
||||
descriptor.value = function (...args) {
|
||||
if (!permissionManager.hasRole(roles, operator)) {
|
||||
logger.warn('Role check failed', {
|
||||
method: propertyKey,
|
||||
roles,
|
||||
operator
|
||||
})
|
||||
|
||||
uni.showToast({
|
||||
title: '角色权限不足',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
})
|
||||
|
||||
return Promise.reject(new Error('Role check failed'))
|
||||
}
|
||||
|
||||
return originalMethod.apply(this, args)
|
||||
}
|
||||
|
||||
return descriptor
|
||||
}
|
||||
}
|
||||
|
||||
// 页面权限检查混入
|
||||
export const permissionMixin = {
|
||||
onLoad() {
|
||||
// 检查页面权限
|
||||
if (this.$options.permissions) {
|
||||
const { permissions, operator = 'and', redirect = '/pages/login/login' } = this.$options.permissions
|
||||
|
||||
if (!permissionManager.hasPermission(permissions, operator)) {
|
||||
logger.warn('Page permission denied', {
|
||||
page: this.$mp.page.route,
|
||||
permissions,
|
||||
operator
|
||||
})
|
||||
|
||||
uni.showToast({
|
||||
title: '页面权限不足',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
uni.redirectTo({ url: redirect })
|
||||
}, 2000)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 导出便捷方法
|
||||
export const permission = {
|
||||
/**
|
||||
* 检查权限
|
||||
*/
|
||||
check: (permissions, operator) => permissionManager.hasPermission(permissions, operator),
|
||||
|
||||
/**
|
||||
* 检查角色
|
||||
*/
|
||||
checkRole: (roles, operator) => permissionManager.hasRole(roles, operator),
|
||||
|
||||
/**
|
||||
* 是否为超级管理员
|
||||
*/
|
||||
isSuperAdmin: () => permissionManager.isSuperAdmin(),
|
||||
|
||||
/**
|
||||
* 获取用户权限
|
||||
*/
|
||||
getUserPermissions: () => {
|
||||
const user = permissionManager.userStore?.userInfo
|
||||
return user ? permissionManager.getUserPermissions(user) : []
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取用户角色
|
||||
*/
|
||||
getUserRoles: () => {
|
||||
const user = permissionManager.userStore?.userInfo
|
||||
return user ? permissionManager.getUserRoles(user) : []
|
||||
},
|
||||
|
||||
/**
|
||||
* 清除缓存
|
||||
*/
|
||||
clearCache: () => permissionManager.clearCache(),
|
||||
|
||||
/**
|
||||
* 刷新权限
|
||||
*/
|
||||
refresh: (userId) => permissionManager.refreshUserPermissions(userId),
|
||||
|
||||
/**
|
||||
* 获取权限描述
|
||||
*/
|
||||
getPermissionDesc: (permission) => permissionManager.getPermissionDescription(permission),
|
||||
|
||||
/**
|
||||
* 获取角色描述
|
||||
*/
|
||||
getRoleDesc: (role) => permissionManager.getRoleDescription(role)
|
||||
}
|
||||
|
||||
// 导出权限管理器
|
||||
export default permissionManager
|
||||
310
mini_program/common/utils/request.js
Normal file
310
mini_program/common/utils/request.js
Normal file
@@ -0,0 +1,310 @@
|
||||
// 统一请求封装
|
||||
class Request {
|
||||
constructor() {
|
||||
this.baseURL = ''
|
||||
this.timeout = 10000
|
||||
this.header = {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
|
||||
// 设置基础URL
|
||||
setBaseURL(url) {
|
||||
this.baseURL = url
|
||||
}
|
||||
|
||||
// 设置请求头
|
||||
setHeader(header) {
|
||||
this.header = { ...this.header, ...header }
|
||||
}
|
||||
|
||||
// 请求拦截器
|
||||
requestInterceptor(config) {
|
||||
// 添加token
|
||||
const token = uni.getStorageSync('token')
|
||||
if (token) {
|
||||
config.header = config.header || {}
|
||||
config.header.Authorization = 'Bearer ' + token
|
||||
}
|
||||
|
||||
// 添加基础URL
|
||||
if (config.url && !config.url.startsWith('http')) {
|
||||
config.url = this.baseURL + config.url
|
||||
}
|
||||
|
||||
// 显示加载提示
|
||||
if (config.loading !== false) {
|
||||
uni.showLoading({
|
||||
title: config.loadingText || '加载中...',
|
||||
mask: true
|
||||
})
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
// 响应拦截器
|
||||
responseInterceptor(response, config) {
|
||||
// 隐藏加载提示
|
||||
if (config.loading !== false) {
|
||||
uni.hideLoading()
|
||||
}
|
||||
|
||||
const { data, statusCode } = response
|
||||
|
||||
// HTTP状态码检查
|
||||
if (statusCode !== 200) {
|
||||
this.handleError({
|
||||
code: statusCode,
|
||||
message: '网络请求失败'
|
||||
}, config)
|
||||
return Promise.reject(response)
|
||||
}
|
||||
|
||||
// 业务状态码检查
|
||||
if (data.code !== undefined && data.code !== 200) {
|
||||
this.handleError(data, config)
|
||||
return Promise.reject(data)
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
// 错误处理
|
||||
handleError(error, config) {
|
||||
const { code, message } = error
|
||||
|
||||
// 根据错误码处理
|
||||
switch (code) {
|
||||
case 401:
|
||||
// token过期,清除本地存储并跳转登录
|
||||
uni.removeStorageSync('token')
|
||||
uni.removeStorageSync('userInfo')
|
||||
uni.showToast({
|
||||
title: '登录已过期,请重新登录',
|
||||
icon: 'none'
|
||||
})
|
||||
setTimeout(() => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/auth/login'
|
||||
})
|
||||
}, 1500)
|
||||
break
|
||||
case 403:
|
||||
uni.showToast({
|
||||
title: '权限不足',
|
||||
icon: 'none'
|
||||
})
|
||||
break
|
||||
case 404:
|
||||
uni.showToast({
|
||||
title: '请求的资源不存在',
|
||||
icon: 'none'
|
||||
})
|
||||
break
|
||||
case 500:
|
||||
uni.showToast({
|
||||
title: '服务器内部错误',
|
||||
icon: 'none'
|
||||
})
|
||||
break
|
||||
default:
|
||||
if (config.showError !== false) {
|
||||
uni.showToast({
|
||||
title: message || '请求失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 通用请求方法
|
||||
request(config) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 请求拦截
|
||||
config = this.requestInterceptor(config)
|
||||
|
||||
uni.request({
|
||||
...config,
|
||||
success: (response) => {
|
||||
try {
|
||||
const result = this.responseInterceptor(response, config)
|
||||
resolve(result)
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
},
|
||||
fail: (error) => {
|
||||
// 隐藏加载提示
|
||||
if (config.loading !== false) {
|
||||
uni.hideLoading()
|
||||
}
|
||||
|
||||
this.handleError({
|
||||
code: 0,
|
||||
message: '网络连接失败'
|
||||
}, config)
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// GET请求
|
||||
get(url, data = {}, config = {}) {
|
||||
return this.request({
|
||||
url,
|
||||
method: 'GET',
|
||||
data,
|
||||
...config
|
||||
})
|
||||
}
|
||||
|
||||
// POST请求
|
||||
post(url, data = {}, config = {}) {
|
||||
return this.request({
|
||||
url,
|
||||
method: 'POST',
|
||||
data,
|
||||
...config
|
||||
})
|
||||
}
|
||||
|
||||
// PUT请求
|
||||
put(url, data = {}, config = {}) {
|
||||
return this.request({
|
||||
url,
|
||||
method: 'PUT',
|
||||
data,
|
||||
...config
|
||||
})
|
||||
}
|
||||
|
||||
// DELETE请求
|
||||
delete(url, data = {}, config = {}) {
|
||||
return this.request({
|
||||
url,
|
||||
method: 'DELETE',
|
||||
data,
|
||||
...config
|
||||
})
|
||||
}
|
||||
|
||||
// 上传文件
|
||||
upload(url, filePath, formData = {}, config = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 添加token
|
||||
const token = uni.getStorageSync('token')
|
||||
const header = { ...this.header }
|
||||
if (token) {
|
||||
header.Authorization = 'Bearer ' + token
|
||||
}
|
||||
|
||||
// 添加基础URL
|
||||
if (!url.startsWith('http')) {
|
||||
url = this.baseURL + url
|
||||
}
|
||||
|
||||
// 显示上传进度
|
||||
if (config.showProgress !== false) {
|
||||
uni.showLoading({
|
||||
title: '上传中...',
|
||||
mask: true
|
||||
})
|
||||
}
|
||||
|
||||
uni.uploadFile({
|
||||
url,
|
||||
filePath,
|
||||
name: config.name || 'file',
|
||||
formData,
|
||||
header,
|
||||
success: (response) => {
|
||||
if (config.showProgress !== false) {
|
||||
uni.hideLoading()
|
||||
}
|
||||
|
||||
try {
|
||||
const data = JSON.parse(response.data)
|
||||
if (data.code === 200) {
|
||||
resolve(data)
|
||||
} else {
|
||||
this.handleError(data, config)
|
||||
reject(data)
|
||||
}
|
||||
} catch (error) {
|
||||
this.handleError({
|
||||
code: 0,
|
||||
message: '上传失败'
|
||||
}, config)
|
||||
reject(error)
|
||||
}
|
||||
},
|
||||
fail: (error) => {
|
||||
if (config.showProgress !== false) {
|
||||
uni.hideLoading()
|
||||
}
|
||||
|
||||
this.handleError({
|
||||
code: 0,
|
||||
message: '上传失败'
|
||||
}, config)
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 下载文件
|
||||
download(url, config = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 添加基础URL
|
||||
if (!url.startsWith('http')) {
|
||||
url = this.baseURL + url
|
||||
}
|
||||
|
||||
// 显示下载进度
|
||||
if (config.showProgress !== false) {
|
||||
uni.showLoading({
|
||||
title: '下载中...',
|
||||
mask: true
|
||||
})
|
||||
}
|
||||
|
||||
uni.downloadFile({
|
||||
url,
|
||||
success: (response) => {
|
||||
if (config.showProgress !== false) {
|
||||
uni.hideLoading()
|
||||
}
|
||||
|
||||
if (response.statusCode === 200) {
|
||||
resolve(response)
|
||||
} else {
|
||||
this.handleError({
|
||||
code: response.statusCode,
|
||||
message: '下载失败'
|
||||
}, config)
|
||||
reject(response)
|
||||
}
|
||||
},
|
||||
fail: (error) => {
|
||||
if (config.showProgress !== false) {
|
||||
uni.hideLoading()
|
||||
}
|
||||
|
||||
this.handleError({
|
||||
code: 0,
|
||||
message: '下载失败'
|
||||
}, config)
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 创建实例
|
||||
const request = new Request()
|
||||
|
||||
export default request
|
||||
632
mini_program/common/utils/storage.js
Normal file
632
mini_program/common/utils/storage.js
Normal file
@@ -0,0 +1,632 @@
|
||||
/**
|
||||
* 本地存储工具类
|
||||
* 提供统一的本地存储接口,支持同步和异步操作
|
||||
*/
|
||||
|
||||
// 存储键名常量
|
||||
export const STORAGE_KEYS = {
|
||||
// 用户相关
|
||||
USER_TOKEN: 'user_token',
|
||||
USER_INFO: 'user_info',
|
||||
USER_SETTINGS: 'user_settings',
|
||||
|
||||
// 应用配置
|
||||
APP_CONFIG: 'app_config',
|
||||
APP_VERSION: 'app_version',
|
||||
LANGUAGE: 'language',
|
||||
THEME: 'theme',
|
||||
|
||||
// 业务数据
|
||||
SEARCH_HISTORY: 'search_history',
|
||||
BROWSE_HISTORY: 'browse_history',
|
||||
FAVORITES: 'favorites',
|
||||
CART_DATA: 'cart_data',
|
||||
FORM_DRAFT: 'form_draft',
|
||||
|
||||
// 缓存数据
|
||||
API_CACHE: 'api_cache',
|
||||
IMAGE_CACHE: 'image_cache',
|
||||
LOCATION_CACHE: 'location_cache'
|
||||
}
|
||||
|
||||
// 存储配置
|
||||
const STORAGE_CONFIG = {
|
||||
// 默认过期时间(毫秒)
|
||||
DEFAULT_EXPIRE_TIME: 7 * 24 * 60 * 60 * 1000, // 7天
|
||||
// 最大存储条目数
|
||||
MAX_ITEMS: 1000,
|
||||
// 存储大小限制(字节)
|
||||
MAX_SIZE: 10 * 1024 * 1024, // 10MB
|
||||
// 键名前缀
|
||||
KEY_PREFIX: 'xlxumu_'
|
||||
}
|
||||
|
||||
/**
|
||||
* 存储项包装器
|
||||
*/
|
||||
class StorageItem {
|
||||
constructor(value, options = {}) {
|
||||
this.value = value
|
||||
this.timestamp = Date.now()
|
||||
this.expireTime = options.expireTime || null
|
||||
this.version = options.version || '1.0.0'
|
||||
this.metadata = options.metadata || {}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否过期
|
||||
*/
|
||||
isExpired() {
|
||||
if (!this.expireTime) return false
|
||||
return Date.now() > this.timestamp + this.expireTime
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取剩余有效时间
|
||||
*/
|
||||
getRemainingTime() {
|
||||
if (!this.expireTime) return Infinity
|
||||
return Math.max(0, this.timestamp + this.expireTime - Date.now())
|
||||
}
|
||||
|
||||
/**
|
||||
* 序列化为JSON
|
||||
*/
|
||||
toJSON() {
|
||||
return {
|
||||
value: this.value,
|
||||
timestamp: this.timestamp,
|
||||
expireTime: this.expireTime,
|
||||
version: this.version,
|
||||
metadata: this.metadata
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从JSON反序列化
|
||||
*/
|
||||
static fromJSON(json) {
|
||||
const item = new StorageItem(json.value)
|
||||
item.timestamp = json.timestamp
|
||||
item.expireTime = json.expireTime
|
||||
item.version = json.version || '1.0.0'
|
||||
item.metadata = json.metadata || {}
|
||||
return item
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 存储管理器类
|
||||
*/
|
||||
class StorageManager {
|
||||
constructor(options = {}) {
|
||||
this.config = { ...STORAGE_CONFIG, ...options }
|
||||
this.cache = new Map() // 内存缓存
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成完整的存储键名
|
||||
*/
|
||||
getFullKey(key) {
|
||||
return `${this.config.KEY_PREFIX}${key}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步设置存储项
|
||||
*/
|
||||
setSync(key, value, options = {}) {
|
||||
try {
|
||||
const fullKey = this.getFullKey(key)
|
||||
const item = new StorageItem(value, {
|
||||
expireTime: options.expireTime || this.config.DEFAULT_EXPIRE_TIME,
|
||||
version: options.version,
|
||||
metadata: options.metadata
|
||||
})
|
||||
|
||||
const serialized = JSON.stringify(item.toJSON())
|
||||
|
||||
// 检查存储大小
|
||||
if (serialized.length > this.config.MAX_SIZE) {
|
||||
throw new Error('存储项过大')
|
||||
}
|
||||
|
||||
uni.setStorageSync(fullKey, serialized)
|
||||
|
||||
// 更新内存缓存
|
||||
this.cache.set(key, item)
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('存储设置失败:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步设置存储项
|
||||
*/
|
||||
async set(key, value, options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const fullKey = this.getFullKey(key)
|
||||
const item = new StorageItem(value, {
|
||||
expireTime: options.expireTime || this.config.DEFAULT_EXPIRE_TIME,
|
||||
version: options.version,
|
||||
metadata: options.metadata
|
||||
})
|
||||
|
||||
const serialized = JSON.stringify(item.toJSON())
|
||||
|
||||
// 检查存储大小
|
||||
if (serialized.length > this.config.MAX_SIZE) {
|
||||
throw new Error('存储项过大')
|
||||
}
|
||||
|
||||
uni.setStorage({
|
||||
key: fullKey,
|
||||
data: serialized,
|
||||
success: () => {
|
||||
// 更新内存缓存
|
||||
this.cache.set(key, item)
|
||||
resolve(true)
|
||||
},
|
||||
fail: (error) => {
|
||||
console.error('存储设置失败:', error)
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('存储设置失败:', error)
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步获取存储项
|
||||
*/
|
||||
getSync(key, defaultValue = null) {
|
||||
try {
|
||||
// 先检查内存缓存
|
||||
if (this.cache.has(key)) {
|
||||
const item = this.cache.get(key)
|
||||
if (!item.isExpired()) {
|
||||
return item.value
|
||||
} else {
|
||||
// 过期则删除
|
||||
this.cache.delete(key)
|
||||
this.removeSync(key)
|
||||
}
|
||||
}
|
||||
|
||||
const fullKey = this.getFullKey(key)
|
||||
const serialized = uni.getStorageSync(fullKey)
|
||||
|
||||
if (!serialized) {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
const itemData = JSON.parse(serialized)
|
||||
const item = StorageItem.fromJSON(itemData)
|
||||
|
||||
// 检查是否过期
|
||||
if (item.isExpired()) {
|
||||
this.removeSync(key)
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// 更新内存缓存
|
||||
this.cache.set(key, item)
|
||||
|
||||
return item.value
|
||||
} catch (error) {
|
||||
console.error('存储获取失败:', error)
|
||||
return defaultValue
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步获取存储项
|
||||
*/
|
||||
async get(key, defaultValue = null) {
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
// 先检查内存缓存
|
||||
if (this.cache.has(key)) {
|
||||
const item = this.cache.get(key)
|
||||
if (!item.isExpired()) {
|
||||
resolve(item.value)
|
||||
return
|
||||
} else {
|
||||
// 过期则删除
|
||||
this.cache.delete(key)
|
||||
this.remove(key)
|
||||
}
|
||||
}
|
||||
|
||||
const fullKey = this.getFullKey(key)
|
||||
|
||||
uni.getStorage({
|
||||
key: fullKey,
|
||||
success: (res) => {
|
||||
try {
|
||||
const itemData = JSON.parse(res.data)
|
||||
const item = StorageItem.fromJSON(itemData)
|
||||
|
||||
// 检查是否过期
|
||||
if (item.isExpired()) {
|
||||
this.remove(key)
|
||||
resolve(defaultValue)
|
||||
return
|
||||
}
|
||||
|
||||
// 更新内存缓存
|
||||
this.cache.set(key, item)
|
||||
|
||||
resolve(item.value)
|
||||
} catch (error) {
|
||||
console.error('存储数据解析失败:', error)
|
||||
resolve(defaultValue)
|
||||
}
|
||||
},
|
||||
fail: () => {
|
||||
resolve(defaultValue)
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('存储获取失败:', error)
|
||||
resolve(defaultValue)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步删除存储项
|
||||
*/
|
||||
removeSync(key) {
|
||||
try {
|
||||
const fullKey = this.getFullKey(key)
|
||||
uni.removeStorageSync(fullKey)
|
||||
this.cache.delete(key)
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('存储删除失败:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步删除存储项
|
||||
*/
|
||||
async remove(key) {
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
const fullKey = this.getFullKey(key)
|
||||
|
||||
uni.removeStorage({
|
||||
key: fullKey,
|
||||
success: () => {
|
||||
this.cache.delete(key)
|
||||
resolve(true)
|
||||
},
|
||||
fail: (error) => {
|
||||
console.error('存储删除失败:', error)
|
||||
resolve(false)
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('存储删除失败:', error)
|
||||
resolve(false)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查存储项是否存在
|
||||
*/
|
||||
hasSync(key) {
|
||||
try {
|
||||
const fullKey = this.getFullKey(key)
|
||||
const value = uni.getStorageSync(fullKey)
|
||||
return !!value
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步检查存储项是否存在
|
||||
*/
|
||||
async has(key) {
|
||||
return new Promise((resolve) => {
|
||||
const fullKey = this.getFullKey(key)
|
||||
|
||||
uni.getStorage({
|
||||
key: fullKey,
|
||||
success: () => resolve(true),
|
||||
fail: () => resolve(false)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取存储项信息
|
||||
*/
|
||||
getInfoSync(key) {
|
||||
try {
|
||||
const fullKey = this.getFullKey(key)
|
||||
const serialized = uni.getStorageSync(fullKey)
|
||||
|
||||
if (!serialized) {
|
||||
return null
|
||||
}
|
||||
|
||||
const itemData = JSON.parse(serialized)
|
||||
const item = StorageItem.fromJSON(itemData)
|
||||
|
||||
return {
|
||||
key,
|
||||
size: serialized.length,
|
||||
timestamp: item.timestamp,
|
||||
expireTime: item.expireTime,
|
||||
remainingTime: item.getRemainingTime(),
|
||||
isExpired: item.isExpired(),
|
||||
version: item.version,
|
||||
metadata: item.metadata
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取存储信息失败:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有存储键名
|
||||
*/
|
||||
getAllKeysSync() {
|
||||
try {
|
||||
const info = uni.getStorageInfoSync()
|
||||
const prefix = this.config.KEY_PREFIX
|
||||
|
||||
return info.keys
|
||||
.filter(key => key.startsWith(prefix))
|
||||
.map(key => key.substring(prefix.length))
|
||||
} catch (error) {
|
||||
console.error('获取存储键名失败:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理过期的存储项
|
||||
*/
|
||||
cleanExpiredSync() {
|
||||
try {
|
||||
const keys = this.getAllKeysSync()
|
||||
let cleanedCount = 0
|
||||
|
||||
keys.forEach(key => {
|
||||
const info = this.getInfoSync(key)
|
||||
if (info && info.isExpired) {
|
||||
this.removeSync(key)
|
||||
cleanedCount++
|
||||
}
|
||||
})
|
||||
|
||||
console.log(`清理了 ${cleanedCount} 个过期存储项`)
|
||||
return cleanedCount
|
||||
} catch (error) {
|
||||
console.error('清理过期存储失败:', error)
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取存储使用情况
|
||||
*/
|
||||
getStorageInfoSync() {
|
||||
try {
|
||||
const info = uni.getStorageInfoSync()
|
||||
const keys = this.getAllKeysSync()
|
||||
|
||||
let totalSize = 0
|
||||
let expiredCount = 0
|
||||
|
||||
keys.forEach(key => {
|
||||
const itemInfo = this.getInfoSync(key)
|
||||
if (itemInfo) {
|
||||
totalSize += itemInfo.size
|
||||
if (itemInfo.isExpired) {
|
||||
expiredCount++
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
totalKeys: keys.length,
|
||||
totalSize,
|
||||
expiredCount,
|
||||
currentSize: info.currentSize,
|
||||
limitSize: info.limitSize,
|
||||
usageRate: info.limitSize > 0 ? (info.currentSize / info.limitSize) : 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取存储信息失败:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有存储
|
||||
*/
|
||||
clearAllSync() {
|
||||
try {
|
||||
const keys = this.getAllKeysSync()
|
||||
keys.forEach(key => this.removeSync(key))
|
||||
this.cache.clear()
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('清空存储失败:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量设置存储项
|
||||
*/
|
||||
setBatchSync(items) {
|
||||
const results = {}
|
||||
|
||||
Object.keys(items).forEach(key => {
|
||||
results[key] = this.setSync(key, items[key])
|
||||
})
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量获取存储项
|
||||
*/
|
||||
getBatchSync(keys, defaultValue = null) {
|
||||
const results = {}
|
||||
|
||||
keys.forEach(key => {
|
||||
results[key] = this.getSync(key, defaultValue)
|
||||
})
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出存储数据
|
||||
*/
|
||||
exportSync() {
|
||||
try {
|
||||
const keys = this.getAllKeysSync()
|
||||
const data = {}
|
||||
|
||||
keys.forEach(key => {
|
||||
const value = this.getSync(key)
|
||||
if (value !== null) {
|
||||
data[key] = value
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
version: '1.0.0',
|
||||
timestamp: Date.now(),
|
||||
data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('导出存储数据失败:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入存储数据
|
||||
*/
|
||||
importSync(exportData) {
|
||||
try {
|
||||
if (!exportData || !exportData.data) {
|
||||
throw new Error('无效的导入数据')
|
||||
}
|
||||
|
||||
const { data } = exportData
|
||||
let successCount = 0
|
||||
|
||||
Object.keys(data).forEach(key => {
|
||||
if (this.setSync(key, data[key])) {
|
||||
successCount++
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
total: Object.keys(data).length,
|
||||
success: successCount,
|
||||
failed: Object.keys(data).length - successCount
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('导入存储数据失败:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建默认存储管理器实例
|
||||
const storage = new StorageManager()
|
||||
|
||||
// 便捷方法
|
||||
export const setStorage = (key, value, options) => storage.set(key, value, options)
|
||||
export const getStorage = (key, defaultValue) => storage.get(key, defaultValue)
|
||||
export const removeStorage = (key) => storage.remove(key)
|
||||
export const hasStorage = (key) => storage.has(key)
|
||||
|
||||
export const setStorageSync = (key, value, options) => storage.setSync(key, value, options)
|
||||
export const getStorageSync = (key, defaultValue) => storage.getSync(key, defaultValue)
|
||||
export const removeStorageSync = (key) => storage.removeSync(key)
|
||||
export const hasStorageSync = (key) => storage.hasSync(key)
|
||||
|
||||
// 特定业务的存储方法
|
||||
export const userStorage = {
|
||||
setToken: (token) => setStorageSync(STORAGE_KEYS.USER_TOKEN, token),
|
||||
getToken: () => getStorageSync(STORAGE_KEYS.USER_TOKEN),
|
||||
removeToken: () => removeStorageSync(STORAGE_KEYS.USER_TOKEN),
|
||||
|
||||
setUserInfo: (userInfo) => setStorageSync(STORAGE_KEYS.USER_INFO, userInfo),
|
||||
getUserInfo: () => getStorageSync(STORAGE_KEYS.USER_INFO),
|
||||
removeUserInfo: () => removeStorageSync(STORAGE_KEYS.USER_INFO),
|
||||
|
||||
setSettings: (settings) => setStorageSync(STORAGE_KEYS.USER_SETTINGS, settings),
|
||||
getSettings: () => getStorageSync(STORAGE_KEYS.USER_SETTINGS, {}),
|
||||
|
||||
clear: () => {
|
||||
removeStorageSync(STORAGE_KEYS.USER_TOKEN)
|
||||
removeStorageSync(STORAGE_KEYS.USER_INFO)
|
||||
removeStorageSync(STORAGE_KEYS.USER_SETTINGS)
|
||||
}
|
||||
}
|
||||
|
||||
export const appStorage = {
|
||||
setConfig: (config) => setStorageSync(STORAGE_KEYS.APP_CONFIG, config),
|
||||
getConfig: () => getStorageSync(STORAGE_KEYS.APP_CONFIG, {}),
|
||||
|
||||
setLanguage: (language) => setStorageSync(STORAGE_KEYS.LANGUAGE, language),
|
||||
getLanguage: () => getStorageSync(STORAGE_KEYS.LANGUAGE, 'zh-CN'),
|
||||
|
||||
setTheme: (theme) => setStorageSync(STORAGE_KEYS.THEME, theme),
|
||||
getTheme: () => getStorageSync(STORAGE_KEYS.THEME, 'light')
|
||||
}
|
||||
|
||||
export const cacheStorage = {
|
||||
setApiCache: (key, data, expireTime = 5 * 60 * 1000) => {
|
||||
const cacheData = getStorageSync(STORAGE_KEYS.API_CACHE, {})
|
||||
cacheData[key] = {
|
||||
data,
|
||||
timestamp: Date.now(),
|
||||
expireTime
|
||||
}
|
||||
setStorageSync(STORAGE_KEYS.API_CACHE, cacheData)
|
||||
},
|
||||
|
||||
getApiCache: (key) => {
|
||||
const cacheData = getStorageSync(STORAGE_KEYS.API_CACHE, {})
|
||||
const item = cacheData[key]
|
||||
|
||||
if (!item) return null
|
||||
|
||||
if (Date.now() > item.timestamp + item.expireTime) {
|
||||
delete cacheData[key]
|
||||
setStorageSync(STORAGE_KEYS.API_CACHE, cacheData)
|
||||
return null
|
||||
}
|
||||
|
||||
return item.data
|
||||
},
|
||||
|
||||
clearApiCache: () => removeStorageSync(STORAGE_KEYS.API_CACHE)
|
||||
}
|
||||
|
||||
// 导出存储管理器类和实例
|
||||
export { StorageManager, StorageItem }
|
||||
export default storage
|
||||
613
mini_program/common/utils/uni-helper.js
Normal file
613
mini_program/common/utils/uni-helper.js
Normal file
@@ -0,0 +1,613 @@
|
||||
/**
|
||||
* uni-app 通用工具函数
|
||||
* 封装常用的 uni-app API,提供更便捷的调用方式
|
||||
*/
|
||||
|
||||
/**
|
||||
* 显示消息提示框
|
||||
* @param {string} title - 提示的内容
|
||||
* @param {string} icon - 图标类型:success/error/loading/none
|
||||
* @param {number} duration - 提示的延迟时间,单位毫秒
|
||||
* @param {boolean} mask - 是否显示透明蒙层,防止触摸穿透
|
||||
*/
|
||||
export const showToast = (title, icon = 'none', duration = 2000, mask = false) => {
|
||||
return new Promise((resolve) => {
|
||||
uni.showToast({
|
||||
title,
|
||||
icon,
|
||||
duration,
|
||||
mask,
|
||||
success: resolve,
|
||||
fail: resolve
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示 loading 提示框
|
||||
* @param {string} title - 提示的内容
|
||||
* @param {boolean} mask - 是否显示透明蒙层,防止触摸穿透
|
||||
*/
|
||||
export const showLoading = (title = '加载中...', mask = true) => {
|
||||
return new Promise((resolve) => {
|
||||
uni.showLoading({
|
||||
title,
|
||||
mask,
|
||||
success: resolve,
|
||||
fail: resolve
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐藏 loading 提示框
|
||||
*/
|
||||
export const hideLoading = () => {
|
||||
return new Promise((resolve) => {
|
||||
uni.hideLoading({
|
||||
success: resolve,
|
||||
fail: resolve
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示模态弹窗
|
||||
* @param {Object} options - 配置选项
|
||||
*/
|
||||
export const showModal = (options = {}) => {
|
||||
const defaultOptions = {
|
||||
title: '提示',
|
||||
content: '',
|
||||
showCancel: true,
|
||||
cancelText: '取消',
|
||||
confirmText: '确定',
|
||||
cancelColor: '#000000',
|
||||
confirmColor: '#576B95'
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
uni.showModal({
|
||||
...defaultOptions,
|
||||
...options,
|
||||
success: resolve,
|
||||
fail: resolve
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示操作菜单
|
||||
* @param {Array} itemList - 按钮的文字数组
|
||||
* @param {string} itemColor - 按钮的文字颜色
|
||||
*/
|
||||
export const showActionSheet = (itemList, itemColor = '#000000') => {
|
||||
return new Promise((resolve) => {
|
||||
uni.showActionSheet({
|
||||
itemList,
|
||||
itemColor,
|
||||
success: resolve,
|
||||
fail: resolve
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置导航栏标题
|
||||
* @param {string} title - 页面标题
|
||||
*/
|
||||
export const setNavigationBarTitle = (title) => {
|
||||
return new Promise((resolve) => {
|
||||
uni.setNavigationBarTitle({
|
||||
title,
|
||||
success: resolve,
|
||||
fail: resolve
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置导航栏颜色
|
||||
* @param {string} frontColor - 前景颜色值,包括按钮、标题、状态栏的颜色
|
||||
* @param {string} backgroundColor - 背景颜色值
|
||||
* @param {Object} animation - 动画效果
|
||||
*/
|
||||
export const setNavigationBarColor = (frontColor, backgroundColor, animation = {}) => {
|
||||
return new Promise((resolve) => {
|
||||
uni.setNavigationBarColor({
|
||||
frontColor,
|
||||
backgroundColor,
|
||||
animation,
|
||||
success: resolve,
|
||||
fail: resolve
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 页面跳转
|
||||
* @param {string} url - 需要跳转的应用内非 tabBar 的页面的路径
|
||||
* @param {Object} params - 页面参数
|
||||
*/
|
||||
export const navigateTo = (url, params = {}) => {
|
||||
let fullUrl = url
|
||||
if (Object.keys(params).length > 0) {
|
||||
const queryString = Object.keys(params)
|
||||
.map(key => `${key}=${encodeURIComponent(params[key])}`)
|
||||
.join('&')
|
||||
fullUrl += `?${queryString}`
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
uni.navigateTo({
|
||||
url: fullUrl,
|
||||
success: resolve,
|
||||
fail: resolve
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭当前页面,跳转到应用内的某个页面
|
||||
* @param {string} url - 需要跳转的应用内非 tabBar 的页面的路径
|
||||
* @param {Object} params - 页面参数
|
||||
*/
|
||||
export const redirectTo = (url, params = {}) => {
|
||||
let fullUrl = url
|
||||
if (Object.keys(params).length > 0) {
|
||||
const queryString = Object.keys(params)
|
||||
.map(key => `${key}=${encodeURIComponent(params[key])}`)
|
||||
.join('&')
|
||||
fullUrl += `?${queryString}`
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
uni.redirectTo({
|
||||
url: fullUrl,
|
||||
success: resolve,
|
||||
fail: resolve
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转到 tabBar 页面
|
||||
* @param {string} url - 需要跳转的 tabBar 页面的路径
|
||||
*/
|
||||
export const switchTab = (url) => {
|
||||
return new Promise((resolve) => {
|
||||
uni.switchTab({
|
||||
url,
|
||||
success: resolve,
|
||||
fail: resolve
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭当前页面,返回上一页面或多级页面
|
||||
* @param {number} delta - 返回的页面数,如果 delta 大于现有页面数,则返回到首页
|
||||
*/
|
||||
export const navigateBack = (delta = 1) => {
|
||||
return new Promise((resolve) => {
|
||||
uni.navigateBack({
|
||||
delta,
|
||||
success: resolve,
|
||||
fail: resolve
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭所有页面,打开到应用内的某个页面
|
||||
* @param {string} url - 需要跳转的应用内页面路径
|
||||
* @param {Object} params - 页面参数
|
||||
*/
|
||||
export const reLaunch = (url, params = {}) => {
|
||||
let fullUrl = url
|
||||
if (Object.keys(params).length > 0) {
|
||||
const queryString = Object.keys(params)
|
||||
.map(key => `${key}=${encodeURIComponent(params[key])}`)
|
||||
.join('&')
|
||||
fullUrl += `?${queryString}`
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
uni.reLaunch({
|
||||
url: fullUrl,
|
||||
success: resolve,
|
||||
fail: resolve
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前页面栈
|
||||
*/
|
||||
export const getCurrentPages = () => {
|
||||
return getCurrentPages()
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取系统信息
|
||||
*/
|
||||
export const getSystemInfo = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.getSystemInfo({
|
||||
success: resolve,
|
||||
fail: reject
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取系统信息同步版本
|
||||
*/
|
||||
export const getSystemInfoSync = () => {
|
||||
return uni.getSystemInfoSync()
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取网络类型
|
||||
*/
|
||||
export const getNetworkType = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.getNetworkType({
|
||||
success: resolve,
|
||||
fail: reject
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听网络状态变化
|
||||
* @param {Function} callback - 网络状态变化的回调函数
|
||||
*/
|
||||
export const onNetworkStatusChange = (callback) => {
|
||||
uni.onNetworkStatusChange(callback)
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消监听网络状态变化
|
||||
* @param {Function} callback - 网络状态变化的回调函数
|
||||
*/
|
||||
export const offNetworkStatusChange = (callback) => {
|
||||
uni.offNetworkStatusChange(callback)
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择图片
|
||||
* @param {Object} options - 配置选项
|
||||
*/
|
||||
export const chooseImage = (options = {}) => {
|
||||
const defaultOptions = {
|
||||
count: 9,
|
||||
sizeType: ['original', 'compressed'],
|
||||
sourceType: ['album', 'camera']
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.chooseImage({
|
||||
...defaultOptions,
|
||||
...options,
|
||||
success: resolve,
|
||||
fail: reject
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 预览图片
|
||||
* @param {Array} urls - 需要预览的图片链接列表
|
||||
* @param {number} current - 当前显示图片的索引
|
||||
*/
|
||||
export const previewImage = (urls, current = 0) => {
|
||||
return new Promise((resolve) => {
|
||||
uni.previewImage({
|
||||
urls,
|
||||
current,
|
||||
success: resolve,
|
||||
fail: resolve
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存图片到系统相册
|
||||
* @param {string} filePath - 图片文件路径
|
||||
*/
|
||||
export const saveImageToPhotosAlbum = (filePath) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.saveImageToPhotosAlbum({
|
||||
filePath,
|
||||
success: resolve,
|
||||
fail: reject
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件
|
||||
* @param {Object} options - 配置选项
|
||||
*/
|
||||
export const uploadFile = (options) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const uploadTask = uni.uploadFile({
|
||||
...options,
|
||||
success: resolve,
|
||||
fail: reject
|
||||
})
|
||||
|
||||
// 返回上传任务,可以监听上传进度
|
||||
return uploadTask
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载文件
|
||||
* @param {Object} options - 配置选项
|
||||
*/
|
||||
export const downloadFile = (options) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const downloadTask = uni.downloadFile({
|
||||
...options,
|
||||
success: resolve,
|
||||
fail: reject
|
||||
})
|
||||
|
||||
// 返回下载任务,可以监听下载进度
|
||||
return downloadTask
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取位置信息
|
||||
* @param {Object} options - 配置选项
|
||||
*/
|
||||
export const getLocation = (options = {}) => {
|
||||
const defaultOptions = {
|
||||
type: 'wgs84',
|
||||
altitude: false
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.getLocation({
|
||||
...defaultOptions,
|
||||
...options,
|
||||
success: resolve,
|
||||
fail: reject
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开地图选择位置
|
||||
* @param {Object} options - 配置选项
|
||||
*/
|
||||
export const chooseLocation = (options = {}) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.chooseLocation({
|
||||
...options,
|
||||
success: resolve,
|
||||
fail: reject
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用应用内置地图查看位置
|
||||
* @param {Object} options - 配置选项
|
||||
*/
|
||||
export const openLocation = (options) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.openLocation({
|
||||
...options,
|
||||
success: resolve,
|
||||
fail: reject
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫码
|
||||
* @param {Object} options - 配置选项
|
||||
*/
|
||||
export const scanCode = (options = {}) => {
|
||||
const defaultOptions = {
|
||||
onlyFromCamera: false,
|
||||
scanType: ['barCode', 'qrCode']
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.scanCode({
|
||||
...defaultOptions,
|
||||
...options,
|
||||
success: resolve,
|
||||
fail: reject
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置剪贴板内容
|
||||
* @param {string} data - 需要设置的内容
|
||||
*/
|
||||
export const setClipboardData = (data) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.setClipboardData({
|
||||
data,
|
||||
success: resolve,
|
||||
fail: reject
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取剪贴板内容
|
||||
*/
|
||||
export const getClipboardData = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.getClipboardData({
|
||||
success: resolve,
|
||||
fail: reject
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 拨打电话
|
||||
* @param {string} phoneNumber - 需要拨打的电话号码
|
||||
*/
|
||||
export const makePhoneCall = (phoneNumber) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.makePhoneCall({
|
||||
phoneNumber,
|
||||
success: resolve,
|
||||
fail: reject
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 震动
|
||||
* @param {string} type - 震动强度类型,有效值为:heavy、medium、light
|
||||
*/
|
||||
export const vibrateShort = (type = 'medium') => {
|
||||
return new Promise((resolve) => {
|
||||
uni.vibrateShort({
|
||||
type,
|
||||
success: resolve,
|
||||
fail: resolve
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 长震动
|
||||
*/
|
||||
export const vibrateLong = () => {
|
||||
return new Promise((resolve) => {
|
||||
uni.vibrateLong({
|
||||
success: resolve,
|
||||
fail: resolve
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户信息
|
||||
* @param {Object} options - 配置选项
|
||||
*/
|
||||
export const getUserInfo = (options = {}) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.getUserInfo({
|
||||
...options,
|
||||
success: resolve,
|
||||
fail: reject
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户设置
|
||||
*/
|
||||
export const getSetting = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.getSetting({
|
||||
success: resolve,
|
||||
fail: reject
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 调起客户端小程序设置界面
|
||||
*/
|
||||
export const openSetting = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.openSetting({
|
||||
success: resolve,
|
||||
fail: reject
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 提前向用户发起授权请求
|
||||
* @param {string} scope - 需要获取权限的 scope
|
||||
*/
|
||||
export const authorize = (scope) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.authorize({
|
||||
scope,
|
||||
success: resolve,
|
||||
fail: reject
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 页面滚动到指定位置
|
||||
* @param {number} scrollTop - 滚动到页面的目标位置
|
||||
* @param {number} duration - 滚动动画的时长
|
||||
*/
|
||||
export const pageScrollTo = (scrollTop, duration = 300) => {
|
||||
return new Promise((resolve) => {
|
||||
uni.pageScrollTo({
|
||||
scrollTop,
|
||||
duration,
|
||||
success: resolve,
|
||||
fail: resolve
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建动画实例
|
||||
* @param {Object} options - 配置选项
|
||||
*/
|
||||
export const createAnimation = (options = {}) => {
|
||||
const defaultOptions = {
|
||||
duration: 400,
|
||||
timingFunction: 'linear',
|
||||
delay: 0,
|
||||
transformOrigin: '50% 50% 0'
|
||||
}
|
||||
|
||||
return uni.createAnimation({
|
||||
...defaultOptions,
|
||||
...options
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 节流函数
|
||||
* @param {Function} func - 需要节流的函数
|
||||
* @param {number} delay - 延迟时间
|
||||
*/
|
||||
export const throttle = (func, delay = 300) => {
|
||||
let timer = null
|
||||
return function (...args) {
|
||||
if (!timer) {
|
||||
timer = setTimeout(() => {
|
||||
func.apply(this, args)
|
||||
timer = null
|
||||
}, delay)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 防抖函数
|
||||
* @param {Function} func - 需要防抖的函数
|
||||
* @param {number} delay - 延迟时间
|
||||
*/
|
||||
export const debounce = (func, delay = 300) => {
|
||||
let timer = null
|
||||
return function (...args) {
|
||||
if (timer) clearTimeout(timer)
|
||||
timer = setTimeout(() => {
|
||||
func.apply(this, args)
|
||||
}, delay)
|
||||
}
|
||||
}
|
||||
497
mini_program/common/utils/validation.js
Normal file
497
mini_program/common/utils/validation.js
Normal file
@@ -0,0 +1,497 @@
|
||||
/**
|
||||
* 表单验证工具类
|
||||
*/
|
||||
|
||||
// 验证规则类型
|
||||
export const VALIDATION_TYPES = {
|
||||
REQUIRED: 'required',
|
||||
EMAIL: 'email',
|
||||
PHONE: 'phone',
|
||||
ID_CARD: 'idCard',
|
||||
PASSWORD: 'password',
|
||||
NUMBER: 'number',
|
||||
INTEGER: 'integer',
|
||||
DECIMAL: 'decimal',
|
||||
MIN_LENGTH: 'minLength',
|
||||
MAX_LENGTH: 'maxLength',
|
||||
MIN_VALUE: 'minValue',
|
||||
MAX_VALUE: 'maxValue',
|
||||
PATTERN: 'pattern',
|
||||
CUSTOM: 'custom'
|
||||
}
|
||||
|
||||
// 内置验证规则
|
||||
const BUILT_IN_RULES = {
|
||||
// 必填验证
|
||||
[VALIDATION_TYPES.REQUIRED]: {
|
||||
validator: (value) => {
|
||||
if (Array.isArray(value)) {
|
||||
return value.length > 0
|
||||
}
|
||||
return value !== null && value !== undefined && String(value).trim() !== ''
|
||||
},
|
||||
message: '此字段为必填项'
|
||||
},
|
||||
|
||||
// 邮箱验证
|
||||
[VALIDATION_TYPES.EMAIL]: {
|
||||
validator: (value) => {
|
||||
if (!value) return true
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||
return emailRegex.test(value)
|
||||
},
|
||||
message: '请输入正确的邮箱地址'
|
||||
},
|
||||
|
||||
// 手机号验证
|
||||
[VALIDATION_TYPES.PHONE]: {
|
||||
validator: (value) => {
|
||||
if (!value) return true
|
||||
const phoneRegex = /^1[3-9]\d{9}$/
|
||||
return phoneRegex.test(value)
|
||||
},
|
||||
message: '请输入正确的手机号码'
|
||||
},
|
||||
|
||||
// 身份证验证
|
||||
[VALIDATION_TYPES.ID_CARD]: {
|
||||
validator: (value) => {
|
||||
if (!value) return true
|
||||
const idCardRegex = /^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/
|
||||
return idCardRegex.test(value)
|
||||
},
|
||||
message: '请输入正确的身份证号码'
|
||||
},
|
||||
|
||||
// 密码验证(至少8位,包含字母和数字)
|
||||
[VALIDATION_TYPES.PASSWORD]: {
|
||||
validator: (value) => {
|
||||
if (!value) return true
|
||||
const passwordRegex = /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d@$!%*#?&]{8,}$/
|
||||
return passwordRegex.test(value)
|
||||
},
|
||||
message: '密码至少8位,需包含字母和数字'
|
||||
},
|
||||
|
||||
// 数字验证
|
||||
[VALIDATION_TYPES.NUMBER]: {
|
||||
validator: (value) => {
|
||||
if (!value) return true
|
||||
return !isNaN(Number(value))
|
||||
},
|
||||
message: '请输入有效的数字'
|
||||
},
|
||||
|
||||
// 整数验证
|
||||
[VALIDATION_TYPES.INTEGER]: {
|
||||
validator: (value) => {
|
||||
if (!value) return true
|
||||
return Number.isInteger(Number(value))
|
||||
},
|
||||
message: '请输入整数'
|
||||
},
|
||||
|
||||
// 小数验证
|
||||
[VALIDATION_TYPES.DECIMAL]: {
|
||||
validator: (value) => {
|
||||
if (!value) return true
|
||||
const decimalRegex = /^\d+(\.\d+)?$/
|
||||
return decimalRegex.test(value)
|
||||
},
|
||||
message: '请输入有效的小数'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证器类
|
||||
*/
|
||||
export class Validator {
|
||||
constructor() {
|
||||
this.rules = {}
|
||||
this.errors = {}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加验证规则
|
||||
* @param {string} field 字段名
|
||||
* @param {Array} rules 验证规则数组
|
||||
*/
|
||||
addRule(field, rules) {
|
||||
this.rules[field] = Array.isArray(rules) ? rules : [rules]
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量添加验证规则
|
||||
* @param {Object} rulesMap 规则映射对象
|
||||
*/
|
||||
addRules(rulesMap) {
|
||||
Object.keys(rulesMap).forEach(field => {
|
||||
this.addRule(field, rulesMap[field])
|
||||
})
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证单个字段
|
||||
* @param {string} field 字段名
|
||||
* @param {any} value 字段值
|
||||
* @param {Object} data 完整数据对象
|
||||
* @returns {Object} 验证结果
|
||||
*/
|
||||
validateField(field, value, data = {}) {
|
||||
const fieldRules = this.rules[field] || []
|
||||
const errors = []
|
||||
|
||||
for (const rule of fieldRules) {
|
||||
const result = this.executeRule(rule, value, data, field)
|
||||
if (!result.valid) {
|
||||
errors.push(result.message)
|
||||
if (rule.stopOnFirstError !== false) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors,
|
||||
field
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证所有字段
|
||||
* @param {Object} data 数据对象
|
||||
* @returns {Object} 验证结果
|
||||
*/
|
||||
validate(data) {
|
||||
const errors = {}
|
||||
let isValid = true
|
||||
|
||||
Object.keys(this.rules).forEach(field => {
|
||||
const value = this.getFieldValue(data, field)
|
||||
const result = this.validateField(field, value, data)
|
||||
|
||||
if (!result.valid) {
|
||||
errors[field] = result.errors
|
||||
isValid = false
|
||||
}
|
||||
})
|
||||
|
||||
this.errors = errors
|
||||
return {
|
||||
valid: isValid,
|
||||
errors
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行单个验证规则
|
||||
* @param {Object} rule 验证规则
|
||||
* @param {any} value 字段值
|
||||
* @param {Object} data 完整数据
|
||||
* @param {string} field 字段名
|
||||
* @returns {Object} 执行结果
|
||||
*/
|
||||
executeRule(rule, value, data, field) {
|
||||
let validator, message
|
||||
|
||||
if (typeof rule === 'string') {
|
||||
// 字符串规则,使用内置规则
|
||||
const builtInRule = BUILT_IN_RULES[rule]
|
||||
if (!builtInRule) {
|
||||
throw new Error(`未知的验证规则: ${rule}`)
|
||||
}
|
||||
validator = builtInRule.validator
|
||||
message = builtInRule.message
|
||||
} else if (typeof rule === 'function') {
|
||||
// 函数规则
|
||||
validator = rule
|
||||
message = '验证失败'
|
||||
} else if (typeof rule === 'object') {
|
||||
// 对象规则
|
||||
if (rule.type && BUILT_IN_RULES[rule.type]) {
|
||||
const builtInRule = BUILT_IN_RULES[rule.type]
|
||||
validator = this.createParameterizedValidator(rule.type, rule, builtInRule.validator)
|
||||
message = rule.message || builtInRule.message
|
||||
} else if (rule.validator) {
|
||||
validator = rule.validator
|
||||
message = rule.message || '验证失败'
|
||||
} else {
|
||||
throw new Error('无效的验证规则对象')
|
||||
}
|
||||
} else {
|
||||
throw new Error('验证规则必须是字符串、函数或对象')
|
||||
}
|
||||
|
||||
try {
|
||||
const isValid = validator(value, data, field)
|
||||
return {
|
||||
valid: isValid,
|
||||
message: isValid ? null : message
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
valid: false,
|
||||
message: `验证过程中发生错误: ${error.message}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建参数化验证器
|
||||
* @param {string} type 验证类型
|
||||
* @param {Object} rule 规则对象
|
||||
* @param {Function} baseValidator 基础验证器
|
||||
* @returns {Function} 参数化验证器
|
||||
*/
|
||||
createParameterizedValidator(type, rule, baseValidator) {
|
||||
switch (type) {
|
||||
case VALIDATION_TYPES.MIN_LENGTH:
|
||||
return (value) => {
|
||||
if (!value) return true
|
||||
return String(value).length >= rule.min
|
||||
}
|
||||
|
||||
case VALIDATION_TYPES.MAX_LENGTH:
|
||||
return (value) => {
|
||||
if (!value) return true
|
||||
return String(value).length <= rule.max
|
||||
}
|
||||
|
||||
case VALIDATION_TYPES.MIN_VALUE:
|
||||
return (value) => {
|
||||
if (!value) return true
|
||||
return Number(value) >= rule.min
|
||||
}
|
||||
|
||||
case VALIDATION_TYPES.MAX_VALUE:
|
||||
return (value) => {
|
||||
if (!value) return true
|
||||
return Number(value) <= rule.max
|
||||
}
|
||||
|
||||
case VALIDATION_TYPES.PATTERN:
|
||||
return (value) => {
|
||||
if (!value) return true
|
||||
const regex = rule.pattern instanceof RegExp ? rule.pattern : new RegExp(rule.pattern)
|
||||
return regex.test(value)
|
||||
}
|
||||
|
||||
default:
|
||||
return baseValidator
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取字段值(支持嵌套字段)
|
||||
* @param {Object} data 数据对象
|
||||
* @param {string} field 字段路径
|
||||
* @returns {any} 字段值
|
||||
*/
|
||||
getFieldValue(data, field) {
|
||||
if (field.includes('.')) {
|
||||
return field.split('.').reduce((obj, key) => obj && obj[key], data)
|
||||
}
|
||||
return data[field]
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取字段错误信息
|
||||
* @param {string} field 字段名
|
||||
* @returns {Array} 错误信息数组
|
||||
*/
|
||||
getFieldErrors(field) {
|
||||
return this.errors[field] || []
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取第一个字段错误信息
|
||||
* @param {string} field 字段名
|
||||
* @returns {string|null} 错误信息
|
||||
*/
|
||||
getFirstFieldError(field) {
|
||||
const errors = this.getFieldErrors(field)
|
||||
return errors.length > 0 ? errors[0] : null
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除验证错误
|
||||
* @param {string} field 字段名,不传则清除所有
|
||||
*/
|
||||
clearErrors(field) {
|
||||
if (field) {
|
||||
delete this.errors[field]
|
||||
} else {
|
||||
this.errors = {}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有错误
|
||||
* @param {string} field 字段名,不传则检查所有
|
||||
* @returns {boolean} 是否有错误
|
||||
*/
|
||||
hasErrors(field) {
|
||||
if (field) {
|
||||
return this.getFieldErrors(field).length > 0
|
||||
}
|
||||
return Object.keys(this.errors).length > 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建验证器实例
|
||||
* @param {Object} rules 验证规则
|
||||
* @returns {Validator} 验证器实例
|
||||
*/
|
||||
export function createValidator(rules = {}) {
|
||||
const validator = new Validator()
|
||||
validator.addRules(rules)
|
||||
return validator
|
||||
}
|
||||
|
||||
/**
|
||||
* 快速验证函数
|
||||
* @param {Object} data 数据对象
|
||||
* @param {Object} rules 验证规则
|
||||
* @returns {Object} 验证结果
|
||||
*/
|
||||
export function validate(data, rules) {
|
||||
const validator = createValidator(rules)
|
||||
return validator.validate(data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 常用验证规则预设
|
||||
*/
|
||||
export const COMMON_RULES = {
|
||||
// 用户名规则
|
||||
username: [
|
||||
VALIDATION_TYPES.REQUIRED,
|
||||
{ type: VALIDATION_TYPES.MIN_LENGTH, min: 3, message: '用户名至少3个字符' },
|
||||
{ type: VALIDATION_TYPES.MAX_LENGTH, max: 20, message: '用户名最多20个字符' },
|
||||
{ type: VALIDATION_TYPES.PATTERN, pattern: /^[a-zA-Z0-9_]+$/, message: '用户名只能包含字母、数字和下划线' }
|
||||
],
|
||||
|
||||
// 邮箱规则
|
||||
email: [
|
||||
VALIDATION_TYPES.REQUIRED,
|
||||
VALIDATION_TYPES.EMAIL
|
||||
],
|
||||
|
||||
// 手机号规则
|
||||
phone: [
|
||||
VALIDATION_TYPES.REQUIRED,
|
||||
VALIDATION_TYPES.PHONE
|
||||
],
|
||||
|
||||
// 密码规则
|
||||
password: [
|
||||
VALIDATION_TYPES.REQUIRED,
|
||||
VALIDATION_TYPES.PASSWORD
|
||||
],
|
||||
|
||||
// 确认密码规则
|
||||
confirmPassword: [
|
||||
VALIDATION_TYPES.REQUIRED,
|
||||
{
|
||||
validator: (value, data) => value === data.password,
|
||||
message: '两次输入的密码不一致'
|
||||
}
|
||||
],
|
||||
|
||||
// 身份证规则
|
||||
idCard: [
|
||||
VALIDATION_TYPES.REQUIRED,
|
||||
VALIDATION_TYPES.ID_CARD
|
||||
],
|
||||
|
||||
// 姓名规则
|
||||
name: [
|
||||
VALIDATION_TYPES.REQUIRED,
|
||||
{ type: VALIDATION_TYPES.MIN_LENGTH, min: 2, message: '姓名至少2个字符' },
|
||||
{ type: VALIDATION_TYPES.MAX_LENGTH, max: 20, message: '姓名最多20个字符' },
|
||||
{ type: VALIDATION_TYPES.PATTERN, pattern: /^[\u4e00-\u9fa5a-zA-Z\s]+$/, message: '姓名只能包含中文、英文和空格' }
|
||||
],
|
||||
|
||||
// 年龄规则
|
||||
age: [
|
||||
VALIDATION_TYPES.REQUIRED,
|
||||
VALIDATION_TYPES.INTEGER,
|
||||
{ type: VALIDATION_TYPES.MIN_VALUE, min: 1, message: '年龄必须大于0' },
|
||||
{ type: VALIDATION_TYPES.MAX_VALUE, max: 150, message: '年龄不能超过150' }
|
||||
],
|
||||
|
||||
// 金额规则
|
||||
amount: [
|
||||
VALIDATION_TYPES.REQUIRED,
|
||||
VALIDATION_TYPES.DECIMAL,
|
||||
{ type: VALIDATION_TYPES.MIN_VALUE, min: 0, message: '金额不能为负数' }
|
||||
],
|
||||
|
||||
// 验证码规则
|
||||
verifyCode: [
|
||||
VALIDATION_TYPES.REQUIRED,
|
||||
{ type: VALIDATION_TYPES.PATTERN, pattern: /^\d{4,6}$/, message: '验证码为4-6位数字' }
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义验证器示例
|
||||
*/
|
||||
export const CUSTOM_VALIDATORS = {
|
||||
// 银行卡号验证
|
||||
bankCard: {
|
||||
validator: (value) => {
|
||||
if (!value) return true
|
||||
// Luhn算法验证银行卡号
|
||||
const digits = value.replace(/\D/g, '')
|
||||
if (digits.length < 13 || digits.length > 19) return false
|
||||
|
||||
let sum = 0
|
||||
let isEven = false
|
||||
|
||||
for (let i = digits.length - 1; i >= 0; i--) {
|
||||
let digit = parseInt(digits[i])
|
||||
|
||||
if (isEven) {
|
||||
digit *= 2
|
||||
if (digit > 9) {
|
||||
digit -= 9
|
||||
}
|
||||
}
|
||||
|
||||
sum += digit
|
||||
isEven = !isEven
|
||||
}
|
||||
|
||||
return sum % 10 === 0
|
||||
},
|
||||
message: '请输入正确的银行卡号'
|
||||
},
|
||||
|
||||
// 统一社会信用代码验证
|
||||
socialCreditCode: {
|
||||
validator: (value) => {
|
||||
if (!value) return true
|
||||
const regex = /^[0-9A-HJ-NPQRTUWXY]{2}\d{6}[0-9A-HJ-NPQRTUWXY]{10}$/
|
||||
return regex.test(value)
|
||||
},
|
||||
message: '请输入正确的统一社会信用代码'
|
||||
},
|
||||
|
||||
// 车牌号验证
|
||||
licensePlate: {
|
||||
validator: (value) => {
|
||||
if (!value) return true
|
||||
const regex = /^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-Z][A-Z0-9]{4}[A-Z0-9挂学警港澳]$/
|
||||
return regex.test(value)
|
||||
},
|
||||
message: '请输入正确的车牌号'
|
||||
}
|
||||
}
|
||||
|
||||
// 导出默认验证器实例
|
||||
export default new Validator()
|
||||
340
mini_program/common/utils/validator.js
Normal file
340
mini_program/common/utils/validator.js
Normal file
@@ -0,0 +1,340 @@
|
||||
// 表单验证工具类
|
||||
class Validator {
|
||||
constructor() {
|
||||
this.rules = {}
|
||||
this.messages = {}
|
||||
this.errors = {}
|
||||
}
|
||||
|
||||
// 添加验证规则
|
||||
addRule(field, rules) {
|
||||
this.rules[field] = Array.isArray(rules) ? rules : [rules]
|
||||
return this
|
||||
}
|
||||
|
||||
// 添加错误消息
|
||||
addMessage(field, message) {
|
||||
this.messages[field] = message
|
||||
return this
|
||||
}
|
||||
|
||||
// 验证单个字段
|
||||
validateField(field, value) {
|
||||
const rules = this.rules[field] || []
|
||||
const errors = []
|
||||
|
||||
for (const rule of rules) {
|
||||
const result = this.applyRule(rule, value, field)
|
||||
if (result !== true) {
|
||||
errors.push(result)
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
this.errors[field] = errors
|
||||
return false
|
||||
} else {
|
||||
delete this.errors[field]
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// 验证所有字段
|
||||
validate(data) {
|
||||
this.errors = {}
|
||||
let isValid = true
|
||||
|
||||
Object.keys(this.rules).forEach(field => {
|
||||
const value = data[field]
|
||||
if (!this.validateField(field, value)) {
|
||||
isValid = false
|
||||
}
|
||||
})
|
||||
|
||||
return isValid
|
||||
}
|
||||
|
||||
// 应用验证规则
|
||||
applyRule(rule, value, field) {
|
||||
if (typeof rule === 'function') {
|
||||
return rule(value, field)
|
||||
}
|
||||
|
||||
if (typeof rule === 'object') {
|
||||
const { type, message, ...params } = rule
|
||||
const validator = this.getValidator(type)
|
||||
|
||||
if (validator) {
|
||||
const result = validator(value, params)
|
||||
return result === true ? true : (message || this.getDefaultMessage(type, params))
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof rule === 'string') {
|
||||
const validator = this.getValidator(rule)
|
||||
if (validator) {
|
||||
const result = validator(value)
|
||||
return result === true ? true : this.getDefaultMessage(rule)
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// 获取验证器
|
||||
getValidator(type) {
|
||||
const validators = {
|
||||
// 必填
|
||||
required: (value) => {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return false
|
||||
}
|
||||
if (Array.isArray(value) && value.length === 0) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
|
||||
// 邮箱
|
||||
email: (value) => {
|
||||
if (!value) return true
|
||||
const emailReg = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||
return emailReg.test(value)
|
||||
},
|
||||
|
||||
// 手机号
|
||||
phone: (value) => {
|
||||
if (!value) return true
|
||||
const phoneReg = /^1[3-9]\d{9}$/
|
||||
return phoneReg.test(value)
|
||||
},
|
||||
|
||||
// 身份证号
|
||||
idCard: (value) => {
|
||||
if (!value) return true
|
||||
const idCardReg = /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/
|
||||
return idCardReg.test(value)
|
||||
},
|
||||
|
||||
// 最小长度
|
||||
minLength: (value, { min }) => {
|
||||
if (!value) return true
|
||||
return value.length >= min
|
||||
},
|
||||
|
||||
// 最大长度
|
||||
maxLength: (value, { max }) => {
|
||||
if (!value) return true
|
||||
return value.length <= max
|
||||
},
|
||||
|
||||
// 长度范围
|
||||
length: (value, { min, max }) => {
|
||||
if (!value) return true
|
||||
const len = value.length
|
||||
return len >= min && len <= max
|
||||
},
|
||||
|
||||
// 最小值
|
||||
min: (value, { min }) => {
|
||||
if (value === null || value === undefined || value === '') return true
|
||||
return Number(value) >= min
|
||||
},
|
||||
|
||||
// 最大值
|
||||
max: (value, { max }) => {
|
||||
if (value === null || value === undefined || value === '') return true
|
||||
return Number(value) <= max
|
||||
},
|
||||
|
||||
// 数值范围
|
||||
range: (value, { min, max }) => {
|
||||
if (value === null || value === undefined || value === '') return true
|
||||
const num = Number(value)
|
||||
return num >= min && num <= max
|
||||
},
|
||||
|
||||
// 正则表达式
|
||||
pattern: (value, { pattern }) => {
|
||||
if (!value) return true
|
||||
const reg = new RegExp(pattern)
|
||||
return reg.test(value)
|
||||
},
|
||||
|
||||
// 数字
|
||||
number: (value) => {
|
||||
if (!value) return true
|
||||
return !isNaN(Number(value))
|
||||
},
|
||||
|
||||
// 整数
|
||||
integer: (value) => {
|
||||
if (!value) return true
|
||||
return Number.isInteger(Number(value))
|
||||
},
|
||||
|
||||
// 正数
|
||||
positive: (value) => {
|
||||
if (!value) return true
|
||||
return Number(value) > 0
|
||||
},
|
||||
|
||||
// URL
|
||||
url: (value) => {
|
||||
if (!value) return true
|
||||
const urlReg = /^https?:\/\/.+/
|
||||
return urlReg.test(value)
|
||||
},
|
||||
|
||||
// 中文
|
||||
chinese: (value) => {
|
||||
if (!value) return true
|
||||
const chineseReg = /^[\u4e00-\u9fa5]+$/
|
||||
return chineseReg.test(value)
|
||||
},
|
||||
|
||||
// 英文
|
||||
english: (value) => {
|
||||
if (!value) return true
|
||||
const englishReg = /^[a-zA-Z]+$/
|
||||
return englishReg.test(value)
|
||||
},
|
||||
|
||||
// 字母数字
|
||||
alphanumeric: (value) => {
|
||||
if (!value) return true
|
||||
const alphanumericReg = /^[a-zA-Z0-9]+$/
|
||||
return alphanumericReg.test(value)
|
||||
}
|
||||
}
|
||||
|
||||
return validators[type]
|
||||
}
|
||||
|
||||
// 获取默认错误消息
|
||||
getDefaultMessage(type, params = {}) {
|
||||
const messages = {
|
||||
required: '此字段为必填项',
|
||||
email: '请输入有效的邮箱地址',
|
||||
phone: '请输入有效的手机号码',
|
||||
idCard: '请输入有效的身份证号码',
|
||||
minLength: `最少输入${params.min}个字符`,
|
||||
maxLength: `最多输入${params.max}个字符`,
|
||||
length: `请输入${params.min}-${params.max}个字符`,
|
||||
min: `最小值为${params.min}`,
|
||||
max: `最大值为${params.max}`,
|
||||
range: `请输入${params.min}-${params.max}之间的数值`,
|
||||
pattern: '格式不正确',
|
||||
number: '请输入有效的数字',
|
||||
integer: '请输入整数',
|
||||
positive: '请输入正数',
|
||||
url: '请输入有效的URL地址',
|
||||
chinese: '请输入中文',
|
||||
english: '请输入英文',
|
||||
alphanumeric: '请输入字母或数字'
|
||||
}
|
||||
|
||||
return messages[type] || '验证失败'
|
||||
}
|
||||
|
||||
// 获取错误信息
|
||||
getErrors() {
|
||||
return this.errors
|
||||
}
|
||||
|
||||
// 获取第一个错误信息
|
||||
getFirstError() {
|
||||
const fields = Object.keys(this.errors)
|
||||
if (fields.length > 0) {
|
||||
const field = fields[0]
|
||||
const errors = this.errors[field]
|
||||
return errors[0]
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// 清除错误信息
|
||||
clearErrors() {
|
||||
this.errors = {}
|
||||
return this
|
||||
}
|
||||
|
||||
// 清除指定字段错误信息
|
||||
clearFieldError(field) {
|
||||
delete this.errors[field]
|
||||
return this
|
||||
}
|
||||
|
||||
// 检查是否有错误
|
||||
hasErrors() {
|
||||
return Object.keys(this.errors).length > 0
|
||||
}
|
||||
|
||||
// 检查指定字段是否有错误
|
||||
hasFieldError(field) {
|
||||
return !!this.errors[field]
|
||||
}
|
||||
}
|
||||
|
||||
// 创建验证器实例
|
||||
const createValidator = () => {
|
||||
return new Validator()
|
||||
}
|
||||
|
||||
// 快速验证方法
|
||||
const validate = {
|
||||
// 验证邮箱
|
||||
email: (value) => {
|
||||
const emailReg = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||
return emailReg.test(value)
|
||||
},
|
||||
|
||||
// 验证手机号
|
||||
phone: (value) => {
|
||||
const phoneReg = /^1[3-9]\d{9}$/
|
||||
return phoneReg.test(value)
|
||||
},
|
||||
|
||||
// 验证身份证号
|
||||
idCard: (value) => {
|
||||
const idCardReg = /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/
|
||||
return idCardReg.test(value)
|
||||
},
|
||||
|
||||
// 验证密码强度
|
||||
password: (value) => {
|
||||
// 至少8位,包含大小写字母和数字
|
||||
const passwordReg = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d@$!%*?&]{8,}$/
|
||||
return passwordReg.test(value)
|
||||
},
|
||||
|
||||
// 验证URL
|
||||
url: (value) => {
|
||||
const urlReg = /^https?:\/\/.+/
|
||||
return urlReg.test(value)
|
||||
},
|
||||
|
||||
// 验证IP地址
|
||||
ip: (value) => {
|
||||
const ipReg = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/
|
||||
return ipReg.test(value)
|
||||
},
|
||||
|
||||
// 验证银行卡号
|
||||
bankCard: (value) => {
|
||||
const bankCardReg = /^[1-9]\d{12,18}$/
|
||||
return bankCardReg.test(value)
|
||||
},
|
||||
|
||||
// 验证车牌号
|
||||
licensePlate: (value) => {
|
||||
const licensePlateReg = /^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领A-Z]{1}[A-Z]{1}[A-Z0-9]{4}[A-Z0-9挂学警港澳]{1}$/
|
||||
return licensePlateReg.test(value)
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
Validator,
|
||||
createValidator,
|
||||
validate
|
||||
}
|
||||
Reference in New Issue
Block a user