0930-m
@ -10,4 +10,8 @@ onHide(() => {
|
||||
console.log("App Hide");
|
||||
});
|
||||
</script>
|
||||
<style></style>
|
||||
<style>
|
||||
body{
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -44,7 +44,8 @@
|
||||
"ios" : {},
|
||||
/* SDK配置 */
|
||||
"sdkConfigs" : {}
|
||||
}
|
||||
},
|
||||
"nativePlugins" : {}
|
||||
},
|
||||
/* 快应用特有相关 */
|
||||
"quickapp" : {},
|
||||
|
||||
101
src/pages.json
@ -42,6 +42,90 @@
|
||||
"navigationBarTitleText": "我的"
|
||||
}
|
||||
}
|
||||
,
|
||||
{
|
||||
"path": "pages/fixed-assets/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "固定资产",
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/fixed-assets/detail",
|
||||
"style": {
|
||||
"navigationBarTitleText": "资产详情"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/fixed-assets/maintenance-info",
|
||||
"style": {
|
||||
"navigationBarTitleText": "维修信息"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/fixed-assets/my-borrows",
|
||||
"style": {
|
||||
"navigationBarTitleText": "我的借用"
|
||||
}
|
||||
}
|
||||
,
|
||||
{
|
||||
"path": "pages/fixed-assets/inventory-plan",
|
||||
"style": {
|
||||
"navigationBarTitleText": "资产盘点计划"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/fixed-assets/inventory-end",
|
||||
"style": {
|
||||
"navigationBarTitleText": "盘点计划(已结束)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/fixed-assets/inventory-ing",
|
||||
"style": {
|
||||
"navigationBarTitleText": "盘点计划(进行中)"
|
||||
}
|
||||
}
|
||||
,
|
||||
{
|
||||
"path": "pages/consumables/plan",
|
||||
"style": {
|
||||
"navigationBarTitleText": "易耗品领用计划"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/consumables/distribution",
|
||||
"style": {
|
||||
"navigationBarTitleText": "发放物资"
|
||||
}
|
||||
}
|
||||
,
|
||||
{
|
||||
"path": "pages/consumables/inventory-plan",
|
||||
"style": {
|
||||
"navigationBarTitleText": "易耗品盘点计划"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/consumables/inventory-end",
|
||||
"style": {
|
||||
"navigationBarTitleText": "易耗品盘点(已结束)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/consumables/inventory-ing",
|
||||
"style": {
|
||||
"navigationBarTitleText": "易耗品盘点(进行中)"
|
||||
}
|
||||
}
|
||||
,
|
||||
{
|
||||
"path": "pages/fixed-assets/scan",
|
||||
"style": {
|
||||
"navigationBarTitleText": "扫码查看信息"
|
||||
}
|
||||
}
|
||||
],
|
||||
"globalStyle": {
|
||||
"navigationBarTextStyle": "black",
|
||||
@ -55,30 +139,31 @@
|
||||
"backgroundColor": "#ffffff",
|
||||
"borderStyle": "black",
|
||||
"height": "64px",
|
||||
"fontSize": "12px",
|
||||
"list": [
|
||||
{
|
||||
"pagePath": "pages/index/index",
|
||||
"text": "首页",
|
||||
"iconPath": "static/icons/home.png",
|
||||
"selectedIconPath": "static/icons/home.png"
|
||||
"iconPath": "static/icons/tabbar/home.png",
|
||||
"selectedIconPath": "static/icons/tabbar/home-selected.png"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/message/index",
|
||||
"text": "消息",
|
||||
"iconPath": "static/icons/messages.png",
|
||||
"selectedIconPath": "static/icons/messages.png"
|
||||
"iconPath": "static/icons/tabbar/messages.png",
|
||||
"selectedIconPath": "static/icons/tabbar/messages-selected.png"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/task/index",
|
||||
"text": "任务",
|
||||
"iconPath": "static/icons/tasks.png",
|
||||
"selectedIconPath": "static/icons/tasks.png"
|
||||
"iconPath": "static/icons/tabbar/tasks.png",
|
||||
"selectedIconPath": "static/icons/tabbar/tasks-selected.png"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/mine/index",
|
||||
"text": "我的",
|
||||
"iconPath": "static/icons/profile.png",
|
||||
"selectedIconPath": "static/icons/profile.png"
|
||||
"iconPath": "static/icons/tabbar/profile.png",
|
||||
"selectedIconPath": "static/icons/tabbar/profile-selected.png"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
712
src/pages/api/fixedAssets.js
Normal file
@ -0,0 +1,712 @@
|
||||
const BASE_URL = 'http://10.3.1.212:8089';//定义后端基础接口地址
|
||||
import config from '../../config.js'; // 引入配置文件
|
||||
|
||||
|
||||
|
||||
|
||||
// 根据手机号搜索用户列表
|
||||
export function searchUserByMobile(mobile) {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
url: `${BASE_URL}/api/assetInformation/getUserListByUserMobile`,
|
||||
method: 'GET',
|
||||
header: getHeaders(),
|
||||
data: {
|
||||
mobile: mobile
|
||||
},
|
||||
success: (res) => {
|
||||
console.log(res);
|
||||
|
||||
if (res.statusCode === 200) {
|
||||
resolve(res.data);
|
||||
} else {
|
||||
reject(res);
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
//——————————————————————————————————————————————————————时间————————————————————————————————————————————————————
|
||||
// 数字补零工具函数(辅助日期格式化)
|
||||
const formatNumber = n => {
|
||||
n = n.toString()// 1. 将数字转为字符串(如 5 → "5",12 → "12")
|
||||
return n[1] ? n : '0' + n// 2. 补零逻辑:若字符串长度<2(如 "5"),则在前面加"0" → "05";否则直接返回(如 "12")
|
||||
}
|
||||
|
||||
//时间格式化函数(仅返回时分秒)
|
||||
const formatTime = date => {
|
||||
date = new Date(date);// 1. 将输入的日期参数(可能是时间戳/字符串)转为标准 Date 对象
|
||||
const hour = date.getHours()// 2. 获取小时(0-23
|
||||
const minute = date.getMinutes()// 3. 获取分钟(0-59)
|
||||
const second = date.getSeconds()// 4. 获取秒(0-59)
|
||||
// 5. 拼接为 "时:分:秒" 格式(调用 formatNumber 补零),如 "09:05:03
|
||||
return `${formatNumber(hour)}:${formatNumber(minute)}:${formatNumber(second)}`
|
||||
}
|
||||
|
||||
|
||||
|
||||
//日期格式化函数(仅返回年月日)
|
||||
const formatDate = date => {
|
||||
date = new Date(date);// 1. 转为标准 Date 对象
|
||||
const year = date.getFullYear()// 2. 获取完整年份(如 2024)
|
||||
const month = date.getMonth() + 1// 3. 获取月份(注意:Date 中月份是 0-11,需+1 才是实际月份,如 2→3月
|
||||
const day = date.getDate()// 4. 获取日期(1-31)
|
||||
// 5. 拼接为 "年-月-日" 格式(补零),如 "2024-03-15"
|
||||
return `${year}-${formatNumber(month)}-${formatNumber(day)}`
|
||||
}
|
||||
|
||||
//完整日期时间格式化函数(年月日 + 时分秒)
|
||||
const formatDateTime = date => {
|
||||
return `${formatDate(date)} ${formatTime(date)}`
|
||||
}
|
||||
//——————————————————————————————————————————————————————————————————————————————————————————————————————————————————
|
||||
|
||||
|
||||
//用户信息初始化与判断
|
||||
let staff = uni.getStorageSync('staff'); // 1. 从 UniApp 本地缓存中读取 "staff"(用户信息,如ID、医院ID、手机号等)
|
||||
let staffisnull = (staff == null || staff == undefined || staff == '');// 2. 判断用户信息是否为空(null/undefined/空字符串)
|
||||
|
||||
|
||||
//统一请求头配置函数(核心!!!!!!!!!!!!!!!!!!!!所有接口共用)
|
||||
export function getHeaders() {
|
||||
const staff = uni.getStorageSync('staff') || null// 1. 再次读取用户信息(避免缓存过期,确保最新)
|
||||
// console.log("用户信息:",staff)// 2. 控制台打印用户信息,用于开发调试
|
||||
const staffisnull = !staff // 3. 简化判断:用户信息为空则为 true,否则为 false
|
||||
// 4. 返回请求头对象(接口请求时会携带这些信息,供后端验证)
|
||||
return {
|
||||
'content-type': 'application/json',// ① 声明请求体格式为 JSON(后端需按 JSON 解析数据)
|
||||
//'Authorization': staffisnull ? null : `Bearer ${uni.getStorageSync('token')}`,
|
||||
'hosp-ID': staffisnull ? config.defaultHospId : staff.hosp_id,// ③ 医院ID:用户未登录用配置文件的默认值,登录后用用户所属医院ID
|
||||
'user-ID': staffisnull ? null : staff.id,// ④ 用户ID:用户未登录为 null,登录后用用户ID(后端用于识别请求用户)
|
||||
'datetime': formatDateTime(new Date()),// ⑤ 请求时间:当前完整日期时间(后端可用于校验请求时效性)
|
||||
'createby': staffisnull ? null : staff.id// ⑥ 创建人ID:与 user-ID 类似,用于后端记录操作人
|
||||
}////核心作用:统一所有接口的请求头格式,避免每个接口重复写头信息,同时支持 “登录 / 未登录” 状态的动态适配。
|
||||
}
|
||||
|
||||
//————————————————————————————————————————————————————固定资产相关————————————————————————————————————————————————————————
|
||||
//分页请求固定资产数据
|
||||
//作用:前端加载固定资产列表时调用(如 “资产列表页” 分页加载数据)。
|
||||
|
||||
|
||||
// 所有接口函数均遵循 “返回 Promise + 封装 uni.request” 的模式,目的是:
|
||||
|
||||
// 统一接口调用方式(用 async/await 或 .then() 处理异步结果);
|
||||
// 统一成功 / 失败判断逻辑(以 HTTP 状态码 200 为成功标准);
|
||||
// 失败时通过 reject 抛出错误,便于前端捕获处理(如提示 “请求失败”)。
|
||||
export function fetchFixedAssets(pageNum, pageSize) {
|
||||
return new Promise((resolve, reject) => { // 返回 Promise,支持异步调用
|
||||
uni.request({ // UniApp 内置的网络请求方法
|
||||
url: `${BASE_URL}/api/assetInformation/page?pageNum=${pageNum}&pageSize=${pageSize}`,// 接口路径:拼接基础地址+分页参数(pageNum=页码,pageSize=每页条数)
|
||||
method: 'POST',// 请求方法:POST
|
||||
data: {// 请求体:空查询条件(后端可能支持按条件筛选,此处默认无筛选)
|
||||
query:{},
|
||||
},
|
||||
|
||||
header:getHeaders(),// 请求头:调用上面的 getHeaders() 获取统一头信息
|
||||
|
||||
success: (res) => { // 请求成功回调(后端有响应,无论业务成功与否)
|
||||
if (res.statusCode === 200) {// 若 HTTP 状态码为 200(表示网络请求成功)
|
||||
resolve(res.data); // 根据你的接口返回结构调整 // 将后端返回的业务数据传给前端(前端用 .then() 接收)
|
||||
} else { // HTTP 状态码非 200(如 404 接口不存在、500 后端报错)
|
||||
reject(res); // 抛出错误(前端用 .catch() 捕获)
|
||||
}
|
||||
},
|
||||
fail: (err) => {// 请求失败回调(如网络超时、断网)
|
||||
reject(err);// 抛出错误
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
//按名称搜索固定资产
|
||||
//分页请求固定资产数据
|
||||
//作用:前端通过 “资产名称” 搜索特定固定资产时调用(如搜索 “电脑”“打印机”)
|
||||
export function searchFixedAssets(name) {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
url: `${BASE_URL}/api/assetInformation/list`,// 接口路径:资产搜索接口
|
||||
method: 'POST',
|
||||
data: {
|
||||
assetName:name // 请求体:传递搜索关键词(assetName=资产名称)
|
||||
},
|
||||
|
||||
header:getHeaders(),
|
||||
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
resolve(res.data); // 根据你的接口返回结构调整
|
||||
} else {
|
||||
reject(res);
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
//通过id获取设备信息
|
||||
export function fetchAssetInfoById(id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
url: `${BASE_URL}/api/assetInformation/list`, // 接口路径:获取单条资产信息
|
||||
method: 'POST',
|
||||
header:getHeaders(),
|
||||
data:{
|
||||
|
||||
"assetSn":id // 请求体:传递资产编号(assetSn=资产唯一编号)
|
||||
|
||||
},
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
resolve(res.data); // 根据你的接口返回结构调整
|
||||
} else {
|
||||
reject(res);
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
//通过id获取设备维修信息
|
||||
export function fetchMaintenanceInfoById(id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
url: `${BASE_URL}/api/asset/maintenance/getByAssetId/${id}`, // 接口路径:拼接资产ID(RESTful 风格)
|
||||
method: 'GET', // 请求方法:GET(此处用 GET 传递路径参数)
|
||||
header:getHeaders(),
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
resolve(res.data); // 根据你的接口返回结构调整
|
||||
} else {
|
||||
reject(res);
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
//借用资产
|
||||
export function borrowAssetById(addDTO) {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
url: `${BASE_URL}/api/asset/borrow/add`, // 接口路径:资产借用申请接口
|
||||
method: 'POST',
|
||||
header: getHeaders(),
|
||||
data: { // 请求体:传递借用所需的所有参数(从前端表单收集的信息)
|
||||
assetId: addDTO.assetId, // 资产ID
|
||||
assetName: addDTO.assetName, // 资产名称
|
||||
borrowDate: addDTO.borrowDate,// 借用日期(需前端传递格式化后的日期)
|
||||
borrowerId: addDTO.borrowerId,// 借用人ID
|
||||
borrowerMobile: addDTO.borrowerMobile, // 借用人手机号
|
||||
lenderId: addDTO.lenderId,// 出借人ID
|
||||
lenderMobile: addDTO.lenderMobile,// 出借人手机号
|
||||
note: addDTO.note,// 备注(可选)
|
||||
registrantDate: addDTO.registrantDate,// 登记日期
|
||||
registrantId: addDTO.registrantId,// 登记人ID
|
||||
returnDate: addDTO.returnDate// 预计归还日期
|
||||
},
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
resolve(res.data);
|
||||
} else {
|
||||
reject(res);
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
//查询个人借用信息
|
||||
//作用:前端 “个人借用记录页” 加载数据时调用(如显示当前用户借了哪些资产)。
|
||||
export function borrowInfo(borrowerId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
url: `${BASE_URL}/api/asset/borrow/list`,// 接口路径:个人借用记录列表
|
||||
method: 'POST',
|
||||
header: getHeaders(),
|
||||
data: {
|
||||
borrowerId:borrowerId // 请求体:传递借用人ID(查询该用户的所有借用记录)
|
||||
},
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
resolve(res.data);
|
||||
} else {
|
||||
reject(res);
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
//归还资产
|
||||
//作用:前端点击 “归还” 按钮时调用(将资产状态从 “已借出” 更新为 “已归还”)。
|
||||
export function returnAsset(id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
url: `${BASE_URL}/api/asset/borrow/returnAsset?assetId=${id}`, // 接口路径:拼接资产ID(URL参数)
|
||||
method: 'PUT', // 请求方法:PUT(通常用于“更新资源状态”,此处更新资产为“已归还”)
|
||||
header:getHeaders(),
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
resolve(res.data); // 根据你的接口返回结构调整
|
||||
} else {
|
||||
reject(res);
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
//————————————————————————————————————————————————业务接口函数(易耗品相关)——————————————————————————————————————————
|
||||
//易耗品领用
|
||||
//作用:前端提交 “易耗品领用表单” 时调用(完成领用申请)。
|
||||
export function getConsumption(addDTO) {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
url: `${BASE_URL}/api/consumable-distribution-item/add`, //接口路径:易耗品领用提交
|
||||
method: 'POST',
|
||||
header:getHeaders(),
|
||||
data:{ // 请求体:易耗品领用所需参数(数量、组别ID、备注等)
|
||||
actualQuantity:addDTO.actualQuantity, // 实际领用数量
|
||||
groupId:addDTO.groupId,// 所属组别ID
|
||||
notes:addDTO.notes,// 备注
|
||||
planId:addDTO.planId,// 发放计划ID
|
||||
recipientMobile:addDTO.recipientMobile,// 领取人手机号
|
||||
skuId:addDTO.skuId// 易耗品SKU编号(唯一标识)
|
||||
},
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
resolve(res.data); // 根据你的接口返回结构调整
|
||||
} else {
|
||||
reject(res);
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
//根据条码查询sku Id
|
||||
export function getConsumptionSkuId(barcode) {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
url: `${BASE_URL}/api/consumable/sku/list`,// 接口路径:条码查询SKU
|
||||
method: 'POST',
|
||||
header:getHeaders(),
|
||||
data:{
|
||||
barcode:barcode // 请求体:传递易耗品条码(通过条码获取对应的SKU ID)
|
||||
},
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
resolve(res.data); // 根据你的接口返回结构调整
|
||||
} else {
|
||||
reject(res);
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
//查询商品库存
|
||||
export function getConsumptionQuantity(goodsInfo) {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
url: `${BASE_URL}/api/consumable-inventory/findByCondition`, // 接口路径:库存查询
|
||||
method: 'POST',
|
||||
header:getHeaders(),
|
||||
data:{ // 请求体:库存查询条件(SKU ID、仓库ID、位置ID)
|
||||
skuId:goodsInfo.skuId,
|
||||
locationId:goodsInfo.locationId,
|
||||
warehouseId:goodsInfo.warehouseId
|
||||
|
||||
},
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
resolve(res.data); // 根据你的接口返回结构调整
|
||||
} else {
|
||||
reject(res);
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
//——————————————————————————————————————————易耗品领用计划??????????????????????????????
|
||||
//获取组别信息
|
||||
export function getGroupsInfo(mobile) { //参数 mobile 表示“组长手机号”
|
||||
return new Promise((resolve, reject) => { // 返回 Promise 对象,支持异步调用(用 async/await 或 .then() 处理结果)
|
||||
// 调用 UniApp 的网络请求 API
|
||||
uni.request({
|
||||
// 请求地址:拼接基础地址 + 易耗品组别查询接口
|
||||
// 接口作用:根据条件查询易耗品组别(此处条件是组长手机号)
|
||||
url: `${BASE_URL}/api/consumable-groups/findByCondition`,
|
||||
method: 'POST',
|
||||
header:getHeaders(), // 请求头:使用统一的 getHeaders() 配置(携带用户、医院等信息)
|
||||
data:{ // 请求体:传递查询条件
|
||||
groupHeadMobile:mobile // 按“组长手机号”查询对应的组别(如组长管理的易耗品分组)
|
||||
},
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
resolve(res.data); // 根据你的接口返回结构调整
|
||||
} else {
|
||||
reject(res);
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
//获取发放计划信息
|
||||
export function getPlanInfo(id,pageNum, pageSize) {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
// 请求地址:拼接基础地址 + 发放计划接口 + 分页参数(pageNum/pageSize)
|
||||
url: `${BASE_URL}/api/consumable-distribution-plan/findByCondition?pageNum=${pageNum}&pageSize=${pageSize}`,
|
||||
method: 'POST',
|
||||
header:getHeaders(),
|
||||
data:{
|
||||
hospId:id // 按“医院ID”查询该医院的易耗品发放计划(如“2024年Q1耗材发放计划”)
|
||||
},
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
resolve(res.data); // 根据你的接口返回结构调整
|
||||
} else {
|
||||
reject(res);
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
//获取易耗品领用详情
|
||||
export function getConsumptionDetail(id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
// 请求地址:拼接基础地址 + 领用详情接口 + 领用记录ID(RESTful风格)
|
||||
url: `${BASE_URL}/api/consumable-distribution-item/app_getDistributeItem/${id}`,
|
||||
method: 'GET', // 请求方法:GET(查询详情常用GET)
|
||||
header:getHeaders(),
|
||||
data:{
|
||||
// GET请求无需请求体,参数通过URL路径传递
|
||||
},
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
resolve(res.data); // 根据你的接口返回结构调整
|
||||
} else {
|
||||
reject(res);
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
//确认易耗品领用
|
||||
export function consumptonConfirm(id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
url: `${BASE_URL}/api/consumable-distribution-item/app_getDistributeItem/${id}`,
|
||||
method: 'POST', // 请求方法:POST(用于更新状态,确认领用
|
||||
header:getHeaders(),
|
||||
data:{
|
||||
|
||||
},
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
resolve(res.data); // 根据你的接口返回结构调整
|
||||
} else {
|
||||
reject(res);
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
//————————————————————————————————————————————————资产盘点————————————————————————————————————————————————————————————
|
||||
//新增固定资产盘点
|
||||
export function addInventory(addDTO) {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
url: `${BASE_URL}/api/asset/inventory/add`, // 接口路径:固定资产盘点记录提交接口
|
||||
method: 'POST', // 请求方法:POST(新增数据用POST)
|
||||
data: { // 请求体:盘点记录所需的核心参数(从前端盘点表单收集)
|
||||
assetId:addDTO.assetId, // 被盘点的固定资产ID
|
||||
assetName:addDTO.assetName, // 固定资产名称(冗余字段,便于后端校验)
|
||||
inventoryDate:addDTO.inventoryDate, // 盘点日期(格式化后的日期时间)
|
||||
inventoryManId:addDTO.inventoryManId, // 盘点人ID(当前操作用户ID)
|
||||
inventoryManMobile:addDTO.inventoryManMobile, // 盘点人手机号(用于身份校验)
|
||||
inventoryPic:addDTO.inventoryPic, // 盘点照片(可选,如资产实物照片,通常是Base64或图片URL)
|
||||
inventoryPlanId:addDTO.inventoryPlanId, // 所属盘点计划ID(关联到具体的盘点任务)
|
||||
inventoryResult:addDTO.inventoryResult, // 盘点结果(如“正常”“缺失”“损坏”等枚举值)
|
||||
note:addDTO.note // 盘点备注(可选,记录特殊情况,如“资产外观有划痕”)
|
||||
},
|
||||
|
||||
header:getHeaders(), // 统一请求头(携带用户、医院等信息)
|
||||
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
resolve(res.data); // 成功时返回后端的盘点记录ID或成功信息
|
||||
} else {
|
||||
reject(res);
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
//获取固定资产盘点计划信息
|
||||
export function getInventoryPlan(pageNum, pageSize) {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
// 接口路径:固定资产盘点计划分页接口,拼接页码(pageNum)和每页条数(pageSize)
|
||||
url: `${BASE_URL}/api/asset/inventoryPlan/page?pageNum=${pageNum}&pageSize=${pageSize}`,
|
||||
method: 'POST',
|
||||
header:getHeaders(),
|
||||
data:{
|
||||
// 请求体:空(后端可能默认返回当前用户有权限的盘点计划,无需额外筛选条件)
|
||||
},
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
resolve(res.data); // 根据你的接口返回结构调整
|
||||
} else {
|
||||
reject(res);
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
//获取盘点详情(计划结束)
|
||||
//作用:前端点击 “已结束” 的盘点计划时调用(查看该计划的完整盘点结果,如哪些资产正常、哪些异常)。
|
||||
export function getInventoryDetailEnd(id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
|
||||
url: `${BASE_URL}/api/asset/inventory/app_getInventoryPlan/${id}`,
|
||||
// 接口路径:拼接盘点计划ID,专门获取“进行中”计划的详情(与已结束计划接口区分开)
|
||||
method: 'GET',
|
||||
header:getHeaders(),
|
||||
data:{
|
||||
|
||||
},
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
resolve(res.data); // 返回进行中计划的详情(如待盘点资产列表、已盘点进度)
|
||||
} else {
|
||||
reject(res);
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
//获取盘点详情(计划进行中)
|
||||
//作用:前端点击 “进行中” 的盘点计划时调用(查看待盘点的资产列表,继续完成未盘点的任务)。
|
||||
export function getInventoryDetailIng(id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
// 接口路径:拼接盘点计划ID,专门获取“进行中”计划的详情(与已结束计划接口区分开)
|
||||
url: `${BASE_URL}/api/asset/inventory/app_getInventoryPlan2/${id}`,
|
||||
method: 'GET',
|
||||
header:getHeaders(),
|
||||
data:{
|
||||
|
||||
},
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
resolve(res.data); // 根据你的接口返回结构调整
|
||||
} else {
|
||||
reject(res);
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
//盘点
|
||||
export function inventory(addDTO) {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
url: `${BASE_URL}/api/asset/inventory/add`, // 与“addInventory”接口路径相同,可能是后端兼容的简化版
|
||||
method: 'POST',
|
||||
header:getHeaders(),
|
||||
data:{ // 请求体:简化后的参数(去掉了assetName、note等非必填项,inventoryDate注释后可能由后端自动填充)
|
||||
assetId:addDTO.assetId, // 被盘点资产ID(必填)
|
||||
inventoryManId:addDTO.inventoryManId, // 盘点人ID(必填)
|
||||
inventoryManMobile:addDTO.inventoryManMobile, // 盘点人手机号(必填)
|
||||
inventoryPic:addDTO.inventoryPic, // 盘点照片(可选)
|
||||
inventoryPlanId:addDTO.inventoryPlanId, // 所属计划ID(必填)
|
||||
inventoryResult:addDTO.inventoryResult, // 盘点结果(必填)
|
||||
//inventoryDate:addDTO.inventoryDate // 注释后,后端可能自动用当前时间作为盘点日期
|
||||
},
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
resolve(res.data); // 根据你的接口返回结构调整
|
||||
} else {
|
||||
reject(res);
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
//————————————————————————————————————————————————易耗品盘点——————————————————————————————————————————————————————
|
||||
//获取消耗品盘点计划信息
|
||||
//作用:前端 “易耗品盘点计划页” 加载数据时调用(与固定资产盘点计划逻辑一致,仅业务类型不同)。
|
||||
export function getConsumpationPlan(pageNum, pageSize) {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
// 接口路径:易耗品盘点计划分页接口,名称区分于固定资产计划
|
||||
url: `${BASE_URL}/api/consumable-check-plan/findByConditionWithPage?pageNum=${pageNum}&pageSize=${pageSize}`,
|
||||
method: 'POST',
|
||||
header:getHeaders(),
|
||||
data:{
|
||||
// 空请求体(后端默认返回当前用户的易耗品盘点计划)
|
||||
},
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
resolve(res.data); // 返回易耗品盘点计划的分页数据
|
||||
} else {
|
||||
reject(res);
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
//获取消耗品盘点详情(计划结束)
|
||||
export function getConsumpationInventoryDetailEnd(id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
// 接口路径:拼接易耗品盘点计划ID,专门获取“已结束”计划的详情
|
||||
url: `${BASE_URL}/api/consumable-check-item/app_getInventoryPlan/${id}`,
|
||||
method: 'GET',
|
||||
header:getHeaders(),
|
||||
data:{
|
||||
|
||||
},
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
resolve(res.data); // 返回已结束计划的详情(如易耗品盘点总数、盈亏统计)
|
||||
} else {
|
||||
reject(res);
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
//获取消耗品盘点详情(计划进行中)
|
||||
//作用:前端查看 “进行中” 的易耗品盘点计划时调用(对应固定资产的 “getInventoryDetailIng”)。
|
||||
export function getConsumpationInventoryDetailIng(id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
// 接口路径:拼接易耗品盘点计划ID,专门获取“进行中”计划的详情
|
||||
url: `${BASE_URL}/api/consumable-check-item/app_getInventoryPlan2/${id}`,
|
||||
method: 'GET',
|
||||
header:getHeaders(),
|
||||
data:{
|
||||
|
||||
},
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
resolve(res.data); // 返回进行中计划的详情(如待盘点易耗品列表、已盘点进度)
|
||||
} else {
|
||||
reject(res);
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
//盘点消耗品
|
||||
export function addComInventory(addDTO) {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
url: `${BASE_URL}/api/consumable-check-item/add`, // 接口路径:易耗品盘点记录提交接口
|
||||
method: 'POST',
|
||||
header:getHeaders(),
|
||||
data:{ // 请求体:易耗品盘点的核心参数(与固定资产盘点参数差异在于“数量”字段)
|
||||
checkPlanId:addDTO.checkPlanId, // 所属易耗品盘点计划ID
|
||||
checkResult:addDTO.checkResult, // 盘点结果(如“账实相符”“盘盈”“盘亏”)
|
||||
inventoryId:addDTO.inventoryId, // 库存ID(关联到具体的易耗品库存记录)
|
||||
quantity:addDTO.quantity // 实际盘点数量(易耗品需统计数量,固定资产通常是“有无”)
|
||||
|
||||
},
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
resolve(res.data); // 根据你的接口返回结构调整
|
||||
} else {
|
||||
reject(res);
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
70
src/pages/api/inventory.js
Normal file
@ -0,0 +1,70 @@
|
||||
const BASE_URL = 'http://10.3.1.212:8089';
|
||||
const config = require('config.js');
|
||||
const formatTime = date => {
|
||||
date = new Date(date);
|
||||
const hour = date.getHours()
|
||||
const minute = date.getMinutes()
|
||||
const second = date.getSeconds()
|
||||
|
||||
return `${formatNumber(hour)}:${formatNumber(minute)}:${formatNumber(second)}`
|
||||
}
|
||||
|
||||
const formatNumber = n => {
|
||||
n = n.toString()
|
||||
return n[1] ? n : '0' + n
|
||||
}
|
||||
|
||||
const formatDate = date => {
|
||||
date = new Date(date);
|
||||
const year = date.getFullYear()
|
||||
const month = date.getMonth() + 1
|
||||
const day = date.getDate()
|
||||
|
||||
return `${year}-${formatNumber(month)}-${formatNumber(day)}`
|
||||
}
|
||||
|
||||
const formatDateTime = date => {
|
||||
return `${formatDate(date)} ${formatTime(date)}`
|
||||
}
|
||||
let staff = uni.getStorageSync('staff');
|
||||
let staffisnull = (staff == null || staff == undefined || staff == '');
|
||||
export function getHeaders() {
|
||||
const staff = uni.getStorageSync('staff') || null
|
||||
const staffisnull = !staff
|
||||
|
||||
return {
|
||||
'content-type': 'application/json',
|
||||
'Authorization': staffisnull ? null : `Bearer ${uni.getStorageSync('token')}`,
|
||||
'hosp-ID': staffisnull ? config.defaultHospId : staff.hosp_id,
|
||||
'user-ID': staffisnull ? null : staff.id,
|
||||
'datetime': formatDateTime(new Date()),
|
||||
'createby': staffisnull ? null : staff.id
|
||||
}
|
||||
}
|
||||
|
||||
//分页请求固定资产盘点信息
|
||||
export function fetchAssetInventory(pageNum, pageSize) {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
url: `${BASE_URL}/api/asset/inventory/page?pageNum=${pageNum}&pageSize=${pageSize}`,
|
||||
method: 'POST',
|
||||
data: {
|
||||
query:{},
|
||||
},
|
||||
|
||||
header:getHeaders(),
|
||||
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
resolve(res.data); // 根据你的接口返回结构调整
|
||||
} else {
|
||||
reject(res);
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
144
src/pages/consumables/distribution.vue
Normal file
@ -0,0 +1,144 @@
|
||||
<template>
|
||||
<scroll-view scroll-y class="page">
|
||||
<view v-for="item in detailPlan" :key="item.Id || item.id" class="item-card">
|
||||
<view class="row"><text class="label">物资名称:</text><text class="value">{{ item.goodsName }}</text></view>
|
||||
<view class="row"><text class="label">发放数量:</text><text class="value highlight">{{ item.actualQuantity
|
||||
}}</text></view>
|
||||
<view class="row"><text class="label">领用小组:</text><text class="value">{{ item.groupName }}</text></view>
|
||||
<view class="row"><text class="label">备注:</text><text class="value">{{ item.notes || '无' }}</text></view>
|
||||
<view class="row"><text class="label">发放计划:</text><text class="value">{{ item.planTitle }}</text></view>
|
||||
|
||||
<view class="action">
|
||||
<button v-if="item.isDistributed == 0" size="mini" class="primary" @tap="reciept(item)">领用</button>
|
||||
<view v-else class="done">已领用</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="detailPlan.length === 0 && !loading" class="empty">暂无更多数据</view>
|
||||
</scroll-view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
// @ts-ignore JS模块无类型声明
|
||||
import { getConsumptionDetail, consumptonConfirm } from '@/pages/api/fixedAssets.js'
|
||||
|
||||
type Item = Record<string, any>
|
||||
|
||||
const detailPlan = ref<Item[]>([])
|
||||
const loading = ref(false)
|
||||
const planId = ref<string | number>('')
|
||||
|
||||
async function loadDetail(id: string | number) {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getConsumptionDetail(id)
|
||||
const list: any[] = (res?.data?.records)
|
||||
|| (res?.data?.list)
|
||||
|| (Array.isArray(res?.data) ? res.data : [])
|
||||
|| []
|
||||
detailPlan.value = list
|
||||
} catch (e) {
|
||||
console.error('获取发放详情失败', e)
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function reciept(item: any) {
|
||||
uni.showModal({
|
||||
content: '确认领用?',
|
||||
success: async (res: any) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
const r = await consumptonConfirm(item.id || item.Id)
|
||||
const code = r?.code ?? r?.data?.code
|
||||
if (code === 0) {
|
||||
await loadDetail(planId.value)
|
||||
uni.showToast({ title: '领用成功', icon: 'success' })
|
||||
} else {
|
||||
uni.showToast({ title: '领用失败', icon: 'none' })
|
||||
}
|
||||
} catch (err) {
|
||||
uni.showToast({ title: '领用失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onLoad((query) => {
|
||||
const id = query?.id
|
||||
if (!id) {
|
||||
uni.showToast({ title: '缺少计划ID', icon: 'none' })
|
||||
return
|
||||
}
|
||||
planId.value = id
|
||||
loadDetail(id)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
padding: 24rpx;
|
||||
background: #f5f7fb;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.item-card {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 20rpx;
|
||||
margin-bottom: 16rpx;
|
||||
box-shadow: 0 6rpx 16rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: #888;
|
||||
min-width: 160rpx;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
color: #e53935;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.action {
|
||||
margin-top: 10rpx;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.done {
|
||||
color: #2e7d32;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.empty {
|
||||
color: #9a9aa0;
|
||||
text-align: center;
|
||||
padding: 24rpx 0;
|
||||
}
|
||||
|
||||
button.primary {
|
||||
background: #4b7aff;
|
||||
color: #fff;
|
||||
border-radius: 8rpx;
|
||||
padding: 0rpx 24rpx;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
67
src/pages/consumables/inventory-end.vue
Normal file
@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<scroll-view scroll-y class="page">
|
||||
<view v-for="item in detailList" :key="item.id" class="card">
|
||||
<view class="head">{{ item.goodsName }}</view>
|
||||
<view class="body">
|
||||
<view class="row">📦 盘点数量:<text class="value">{{ item.quantity }}</text></view>
|
||||
<view class="row">📅 盘点日期:<text class="value">{{ item.checkDate }}</text></view>
|
||||
<view class="row">👤 盘点人:<text class="value">{{ item.checkManName }}</text></view>
|
||||
<view class="row">📞 盘点人电话:<text class="value">{{ item.checkManMobile }}</text></view>
|
||||
<view class="row">结果:
|
||||
<text class="result lose" v-if="item.checkResult==0">盘亏</text>
|
||||
<text class="result normal" v-else-if="item.checkResult==1">账实相符</text>
|
||||
<text class="result win" v-else-if="item.checkResult==2">盘盈</text>
|
||||
<text class="result unknown" v-else>未知</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="detailList.length===0 && !loading" class="empty">暂无更多数据</view>
|
||||
</scroll-view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
// @ts-ignore JS模块无类型声明
|
||||
import { getConsumpationInventoryDetailEnd } from '@/pages/api/fixedAssets.js'
|
||||
|
||||
const detailList = ref<any[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
async function loadList(planId: string | number) {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getConsumpationInventoryDetailEnd(planId)
|
||||
const list: any[] = (res?.data?.list) || (res?.data?.records) || (Array.isArray(res?.data) ? res.data : []) || []
|
||||
detailList.value = list
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onLoad((query) => {
|
||||
const planId = query?.planId
|
||||
if (!planId) {
|
||||
uni.showToast({ title: '缺少计划ID', icon: 'none' })
|
||||
return
|
||||
}
|
||||
loadList(planId)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page { min-height: 100vh; padding: 24rpx; background: #f5f7fb; box-sizing: border-box;}
|
||||
.card { background: #fff; border-radius: 16rpx; padding: 20rpx; margin-bottom: 16rpx; box-shadow: 0 6rpx 16rpx rgba(0,0,0,0.06); }
|
||||
.head { font-size: 30rpx; color: #222; font-weight: 600; margin-bottom: 10rpx; }
|
||||
.row { margin: 6rpx 0; color: #444; }
|
||||
.value { color: #333; }
|
||||
.result { padding: 2rpx 8rpx; border-radius: 999rpx; font-size: 22rpx; }
|
||||
.lose { color: #e53935; background: #ffebee; }
|
||||
.normal { color: #2e7d32; background: #e8f5e9; }
|
||||
.win { color: #1e88e5; background: #e3f2fd; }
|
||||
.unknown { color: #757575; background: #eeeeee; }
|
||||
.empty { color: #9a9aa0; text-align: center; padding: 24rpx 0; }
|
||||
</style>
|
||||
139
src/pages/consumables/inventory-ing.vue
Normal file
@ -0,0 +1,139 @@
|
||||
<template>
|
||||
<scroll-view scroll-y class="page">
|
||||
<view v-for="item in detailList" :key="item.id" class="card">
|
||||
<view class="head">📦 {{ item.goodsName }}</view>
|
||||
<view class="body">
|
||||
<view class="row">库存数量:<text class="value">{{ item.quantity }}</text></view>
|
||||
<view class="row">当前状态:<text class="status">{{ item.checkResult || '未盘点' }}</text></view>
|
||||
</view>
|
||||
|
||||
<view class="actions" v-if="item.checkResult === '未盘点'">
|
||||
<button size="mini" class="check" @tap="openCheckModal(item)">去盘点</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="detailList.length===0 && !loading" class="empty">暂无更多数据</view>
|
||||
|
||||
<!-- 盘点弹窗 -->
|
||||
<view v-if="showModal" class="modal-mask">
|
||||
<view class="modal">
|
||||
<view class="modal-title">盘点物资:{{ currentItem?.goodsName }}</view>
|
||||
<view class="modal-field">
|
||||
<text class="label">盘点数量:</text>
|
||||
<input type="number" v-model="checkForm.quantity" placeholder="请输入盘点数量" class="modal-input" />
|
||||
</view>
|
||||
<view class="modal-field">
|
||||
<text class="label">盘点结果:</text>
|
||||
<picker :range="statusOptions" :value="statusIndex" @change="onStatusChange">
|
||||
<view class="picker">{{ statusOptions[statusIndex] }}</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="modal-actions">
|
||||
<button class="cancel" @tap="closeModal">取消</button>
|
||||
<button class="confirm" @tap="submitCheck">确认</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
// @ts-ignore JS模块无类型声明
|
||||
import { getConsumpationInventoryDetailIng, addComInventory } from '@/pages/api/fixedAssets.js'
|
||||
|
||||
type Item = Record<string, any>
|
||||
|
||||
const planId = ref<string | number>('')
|
||||
const detailList = ref<Item[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
const showModal = ref(false)
|
||||
const currentItem = ref<Item | null>(null)
|
||||
const statusOptions = ['盘亏', '账实相符', '盘盈']
|
||||
const statusIndex = ref(1)
|
||||
const checkForm = ref<{ quantity: string; status: number }>({ quantity: '', status: 1 })
|
||||
|
||||
async function loadList(id: string | number) {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getConsumpationInventoryDetailIng(id)
|
||||
const list: any[] = (res?.data?.list) || (res?.data?.records) || (Array.isArray(res?.data) ? res.data : []) || []
|
||||
detailList.value = list
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openCheckModal(item: Item) {
|
||||
currentItem.value = item
|
||||
showModal.value = true
|
||||
checkForm.value = { quantity: '', status: 1 }
|
||||
statusIndex.value = 1
|
||||
}
|
||||
|
||||
function onStatusChange(e: any) {
|
||||
statusIndex.value = e.detail.value
|
||||
checkForm.value.status = parseInt(e.detail.value)
|
||||
}
|
||||
|
||||
function closeModal() { showModal.value = false }
|
||||
|
||||
async function submitCheck() {
|
||||
if (!checkForm.value.quantity) {
|
||||
uni.showToast({ title: '请输入盘点数量', icon: 'none' })
|
||||
return
|
||||
}
|
||||
try {
|
||||
await addComInventory({
|
||||
checkPlanId: planId.value,
|
||||
checkResult: checkForm.value.status,
|
||||
inventoryId: currentItem.value?.id,
|
||||
quantity: checkForm.value.quantity
|
||||
})
|
||||
uni.showToast({ title: '盘点成功', icon: 'none' })
|
||||
showModal.value = false
|
||||
await loadList(planId.value)
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '盘点失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
onLoad((query) => {
|
||||
const id = query?.planId
|
||||
if (!id) {
|
||||
uni.showToast({ title: '缺少计划ID', icon: 'none' })
|
||||
return
|
||||
}
|
||||
planId.value = id
|
||||
loadList(id)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page { min-height: 100vh; padding: 24rpx; background: #f5f7fb; box-sizing: border-box;}
|
||||
.card { background: #fff; border-radius: 16rpx; padding: 20rpx; margin-bottom: 16rpx; box-shadow: 0 6rpx 16rpx rgba(0,0,0,0.06); }
|
||||
.head { font-size: 30rpx; color: #222; font-weight: 600; margin-bottom: 10rpx; }
|
||||
.row { margin: 6rpx 0; color: #444; }
|
||||
.value { color: #333; }
|
||||
.status { color: #757575; }
|
||||
.actions { margin-top: 8rpx; display: flex; justify-content: flex-end; }
|
||||
button.check { background: #4b7aff; color: #fff; border-radius: 8rpx; padding: 6rpx 16rpx; }
|
||||
.empty { color: #9a9aa0; text-align: center; padding: 24rpx 0; }
|
||||
|
||||
/* modal */
|
||||
.modal-mask { position: fixed; inset: 0; background: rgba(0,0,0,0.4); display: flex; align-items: center; justify-content: center; }
|
||||
.modal { width: 86%; background: #fff; border-radius: 16rpx; padding: 24rpx; }
|
||||
.modal-title { font-size: 30rpx; color: #222; font-weight: 600; margin-bottom: 16rpx; }
|
||||
.modal-field { display: flex; align-items: center; margin-bottom: 12rpx; }
|
||||
.label { color: #666; min-width: 160rpx; }
|
||||
.modal-input { flex: 1; border: 2rpx solid #e0e0e0; border-radius: 8rpx; padding: 8rpx 12rpx; }
|
||||
.picker { border: 2rpx solid #e0e0e0; border-radius: 8rpx; padding: 10rpx 12rpx; color: #333; }
|
||||
.modal-actions { margin-top: 16rpx; display: flex; justify-content: flex-end; gap: 16rpx; }
|
||||
button.cancel { background: #eceff1; color: #333; border-radius: 8rpx; padding: 8rpx 16rpx; }
|
||||
button.confirm { background: #4b7aff; color: #fff; border-radius: 8rpx; padding: 8rpx 16rpx; }
|
||||
</style>
|
||||
101
src/pages/consumables/inventory-plan.vue
Normal file
@ -0,0 +1,101 @@
|
||||
<template>
|
||||
<scroll-view scroll-y class="page">
|
||||
<view v-if="inventoryPlanList.length === 0 && !loading" class="empty">暂无更多数据</view>
|
||||
|
||||
<view class="inventory-list">
|
||||
<view class="inventory-item" v-for="item in inventoryPlanList" :key="item.inventoryId || item.id" @tap="gotoPlanDetail(item)">
|
||||
<view class="header">
|
||||
<text class="title">📋 盘点计划</text>
|
||||
<text class="status-tag" :class="statusClass(item.status)">{{ item.status || '未知' }}</text>
|
||||
</view>
|
||||
<view class="content">
|
||||
<view class="row">开始日期:<text class="value">{{ item.checkStartDate || '-' }}</text></view>
|
||||
<view class="row">结束日期:<text class="value">{{ item.checkEndDate || '-' }}</text></view>
|
||||
<view class="row">创建人:<text class="value">{{ item.createByName || '-' }}</text></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="load-status">
|
||||
<text v-if="loading">加载中...</text>
|
||||
<text v-else-if="noMore">没有更多数据了</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { onLoad, onReachBottom } from '@dcloudio/uni-app'
|
||||
// @ts-ignore JS模块无类型声明
|
||||
import { getConsumpationPlan } from '@/pages/api/fixedAssets.js'
|
||||
|
||||
type Plan = Record<string, any>
|
||||
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const loading = ref(false)
|
||||
const noMore = ref(false)
|
||||
const inventoryPlanList = ref<Plan[]>([])
|
||||
|
||||
function statusClass(status?: string) {
|
||||
if (status === '已结束') return 'ended'
|
||||
if (status === '正在进行') return 'running'
|
||||
return 'unknown'
|
||||
}
|
||||
|
||||
async function fetchInventoryList() {
|
||||
if (loading.value || noMore.value) return
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getConsumpationPlan(page.value, pageSize.value)
|
||||
const list: any[] = (res?.data?.list) || (res?.data?.records) || (Array.isArray(res?.data) ? res.data : []) || []
|
||||
inventoryPlanList.value = inventoryPlanList.value.concat(list)
|
||||
if (list.length < pageSize.value) noMore.value = true
|
||||
} catch (e) {
|
||||
console.error('获取易耗品盘点计划失败', e)
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function gotoPlanDetail(item: any) {
|
||||
const planId = item?.id || item?.inventoryId
|
||||
if (!planId && planId !== 0) {
|
||||
uni.showToast({ title: '缺少计划ID', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (item.status === '已结束') {
|
||||
uni.navigateTo({ url: `/pages/consumables/inventory-end?planId=${planId}` })
|
||||
} else {
|
||||
uni.navigateTo({ url: `/pages/consumables/inventory-ing?planId=${planId}` })
|
||||
}
|
||||
}
|
||||
|
||||
onLoad(() => {
|
||||
fetchInventoryList()
|
||||
})
|
||||
|
||||
onReachBottom(() => {
|
||||
if (!loading.value && !noMore.value) {
|
||||
page.value += 1
|
||||
fetchInventoryList()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page { min-height: 100vh; background: #f5f7fb; padding: 24rpx; box-sizing: border-box; }
|
||||
.empty { color: #9a9aa0; text-align: center; padding: 24rpx 0; }
|
||||
.inventory-list { display: flex; flex-direction: column; gap: 16rpx; }
|
||||
.inventory-item { background: #fff; border-radius: 16rpx; padding: 20rpx; box-shadow: 0 6rpx 16rpx rgba(0,0,0,0.06); }
|
||||
.header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12rpx; }
|
||||
.title { font-size: 30rpx; color: #222; font-weight: 600; }
|
||||
.status-tag { font-size: 24rpx; padding: 4rpx 10rpx; border-radius: 999rpx; }
|
||||
.status-tag.running { color: #1e88e5; background: #e3f2fd; }
|
||||
.status-tag.ended { color: #2e7d32; background: #e8f5e9; }
|
||||
.status-tag.unknown { color: #757575; background: #eeeeee; }
|
||||
.content .row { margin-top: 6rpx; color: #444; }
|
||||
.value { color: #333; }
|
||||
.load-status { text-align: center; color: #9a9aa0; padding: 20rpx 0; }
|
||||
</style>
|
||||
159
src/pages/consumables/plan.vue
Normal file
@ -0,0 +1,159 @@
|
||||
<template>
|
||||
<scroll-view scroll-y class="page">
|
||||
<view v-if="planList.length === 0 && !loading" class="empty">暂无更多数据</view>
|
||||
|
||||
<view v-for="plan in planList" :key="plan.id" class="plan-card">
|
||||
<view class="plan-header">
|
||||
<text class="plan-title">{{ plan.planTitle }}</text>
|
||||
<text class="plan-action" @tap="goToDistribution(plan.id)">发放物资</text>
|
||||
</view>
|
||||
|
||||
<view class="plan-info">
|
||||
<view class="info-item">
|
||||
<text class="label">医院:</text>
|
||||
<text class="value">{{ plan.hospName }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="label">周期类型:</text>
|
||||
<text class="value">{{ plan.periodType }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="label">发放数量:</text>
|
||||
<text class="value">{{ plan.quantityPerPeriod }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="plan-time">
|
||||
<view class="info-item">
|
||||
<text class="label">上次分配:</text>
|
||||
<text class="value">{{ plan.lastDistributionDate || '-' }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="label">下次分配:</text>
|
||||
<text class="value">{{ plan.nextDistributionDate || '-' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="plan-footer">
|
||||
<text class="created">创建人:{{ plan.createdByName }}({{ plan.createdByMobile }})</text>
|
||||
<text class="time">创建时间:{{ plan.createdTime }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="load-status">
|
||||
<text v-if="loading">加载中...</text>
|
||||
<text v-else-if="noMore">没有更多数据了</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { onLoad, onReachBottom } from '@dcloudio/uni-app'
|
||||
// @ts-ignore JS模块无类型声明
|
||||
import * as api from '@/pages/api/fixedAssets.js'
|
||||
|
||||
type PlanItem = Record<string, any>
|
||||
|
||||
const planList = ref<PlanItem[]>([])
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const loading = ref(false)
|
||||
const noMore = ref(false)
|
||||
|
||||
function resetAndLoad() {
|
||||
planList.value = []
|
||||
page.value = 1
|
||||
noMore.value = false
|
||||
getPlan()
|
||||
}
|
||||
|
||||
async function getPlan() {
|
||||
if (loading.value || noMore.value) return
|
||||
loading.value = true
|
||||
try {
|
||||
const staff = uni.getStorageSync('staff') || {}
|
||||
const hospId = staff.hosp_id || staff.hospId || 0
|
||||
// API 兼容多返回格式
|
||||
const res = await api.getPlanInfo(hospId, page.value, pageSize.value)
|
||||
const list: any[] = (res?.data?.records)
|
||||
|| (res?.data?.list)
|
||||
|| (Array.isArray(res?.data) ? res.data : [])
|
||||
|| []
|
||||
planList.value = planList.value.concat(list)
|
||||
if (list.length < pageSize.value) noMore.value = true
|
||||
} catch (e) {
|
||||
console.error('获取易耗品领用计划失败', e)
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function goToDistribution(id: string | number) {
|
||||
if (!id && id !== 0) {
|
||||
uni.showToast({ title: '缺少计划ID', icon: 'none' })
|
||||
return
|
||||
}
|
||||
uni.navigateTo({ url: `/pages/consumables/distribution?id=${id}` })
|
||||
}
|
||||
|
||||
onLoad(() => {
|
||||
resetAndLoad()
|
||||
})
|
||||
|
||||
onReachBottom(() => {
|
||||
if (!loading.value && !noMore.value) {
|
||||
page.value += 1
|
||||
getPlan()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
padding: 24rpx;
|
||||
background: #f5f7fb;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.empty {
|
||||
color: #9a9aa0;
|
||||
text-align: center;
|
||||
padding: 24rpx 0;
|
||||
}
|
||||
.plan-card {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 20rpx;
|
||||
margin-bottom: 16rpx;
|
||||
box-shadow: 0 6rpx 16rpx rgba(0,0,0,0.06);
|
||||
}
|
||||
.plan-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
.plan-title {
|
||||
font-size: 30rpx;
|
||||
color: #222;
|
||||
font-weight: 600;
|
||||
}
|
||||
.plan-action {
|
||||
font-size: 26rpx;
|
||||
color: #4b7aff;
|
||||
}
|
||||
.plan-info, .plan-time {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 12rpx;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
.info-item { color: #444; }
|
||||
.label { color: #888; }
|
||||
.value { color: #333; }
|
||||
.plan-footer { color: #666; font-size: 24rpx; display: flex; flex-direction: column; gap: 6rpx; }
|
||||
.load-status { text-align: center; color: #9a9aa0; padding: 20rpx 0; }
|
||||
</style>
|
||||
140
src/pages/fixed-assets/detail.vue
Normal file
@ -0,0 +1,140 @@
|
||||
<template>
|
||||
<view class="page-container">
|
||||
<view class="popup-content card">
|
||||
<view class="popup-title">资产详情</view>
|
||||
<view class="title-divider"></view>
|
||||
|
||||
<view class="info-group">
|
||||
<view class="group-label">基础信息</view>
|
||||
<view class="popup-text">资产名称:{{ detailData.assetName }}</view>
|
||||
<view class="popup-text">设备序列号:{{ detailData.assetSn }}</view>
|
||||
<view class="popup-text">规格:{{ detailData.specifications }}</view>
|
||||
<view class="popup-text">型号:{{ detailData.model }}</view>
|
||||
</view>
|
||||
|
||||
<view class="info-group">
|
||||
<view class="group-label">资产属性</view>
|
||||
<view class="popup-text">品牌:{{ detailData.assetBrand }}</view>
|
||||
<view class="popup-text">状态:
|
||||
<text class="status-tag" v-if="detailData.status === 1 || detailData.status === 0">使用中</text>
|
||||
<text class="status-tag" v-else-if="detailData.status === 2">报废</text>
|
||||
<text class="status-tag" v-else-if="detailData.status === 3">闲置</text>
|
||||
<text class="status-tag" v-else-if="detailData.status === 4">维修中</text>
|
||||
<text class="status-tag" v-else-if="detailData.status === 5">外借</text>
|
||||
<text class="status-tag" v-else>未知</text>
|
||||
</view>
|
||||
<view class="popup-text">制造商:{{ detailData.manufacturer }}</view>
|
||||
<view class="popup-text">保存位置:{{ detailData.location }}</view>
|
||||
</view>
|
||||
|
||||
<view class="info-group">
|
||||
<view class="group-label">财务信息</view>
|
||||
<view class="popup-text">采购日期:{{ detailData.purchaseDate }}</view>
|
||||
<view class="popup-text">购买价格:{{ detailData.purchasePrice }}</view>
|
||||
<view class="popup-text">购买数量:{{ detailData.purchaseQuantity }}</view>
|
||||
<view class="popup-text">单位:{{ detailData.unit }}</view>
|
||||
<view class="popup-text">购买人:{{ detailData.purchaseManName }}</view>
|
||||
<view class="popup-text">现有价值:{{ detailData.value }}</view>
|
||||
<view class="popup-text">折旧率:{{ detailData.depreciationRate }}</view>
|
||||
<view class="popup-text">使用期限(单位年):{{ detailData.usefullife }}</view>
|
||||
</view>
|
||||
|
||||
<view class="info-group">
|
||||
<view class="group-label">使用信息</view>
|
||||
<view class="popup-text">使用人:{{ detailData.useManName }}</view>
|
||||
<view class="popup-text">使用部门:{{ detailData.departmentName }}</view>
|
||||
<view class="popup-text">负责人:{{ detailData.headerName }}</view>
|
||||
<view class="popup-text">是否处置:
|
||||
<text class="disposal-tag" v-if="detailData.isDisposal === 0">否</text>
|
||||
<text class="disposal-tag" v-else>是</text>
|
||||
</view>
|
||||
<view class="popup-text">处置日期:{{ detailData.disposaldate || '未处置' }}</view>
|
||||
<view class="popup-text">保修到期时间:{{ detailData.warrantyDate }}</view>
|
||||
<view class="popup-text">说明:{{ detailData.note || '无' }}</view>
|
||||
</view>
|
||||
|
||||
<view class="info-group">
|
||||
<view class="group-label">登记信息</view>
|
||||
<view class="popup-text">登记人:{{ detailData.registrantName }}</view>
|
||||
<view class="popup-text">登记时间:{{ detailData.registrantDate }}</view>
|
||||
</view>
|
||||
|
||||
<view class="image-viewer">
|
||||
<view class="image-label">设备照片</view>
|
||||
<view class="image-container" @tap="previewImage">
|
||||
<image class="image" :src="detailData.assetPic || defaultImage" mode="aspectFit" />
|
||||
<view class="image-hint" v-if="detailData.assetPic">点击预览大图</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
// @ts-ignore JS模块无类型声明
|
||||
import { fetchAssetInfoById } from '@/pages/api/fixedAssets.js'
|
||||
|
||||
const detailData = ref<any>({})
|
||||
const defaultImage = '/static/images/asset-default.png'
|
||||
|
||||
onLoad(async (options?: Record<string, any>) => {
|
||||
const sn = options?.assetSn || options?.sn
|
||||
if (sn) {
|
||||
await fetchBySn(sn)
|
||||
return
|
||||
}
|
||||
// 回退:使用 eventChannel 直接接收完整对象
|
||||
const pages = getCurrentPages()
|
||||
// @ts-ignore 小程序端API类型可能缺失
|
||||
const eventChannel = (pages[pages.length - 1] as any)?.getOpenerEventChannel?.()
|
||||
eventChannel?.on('sendData', (res: any) => { detailData.value = res?.data || {} })
|
||||
})
|
||||
|
||||
async function fetchBySn(sn: string) {
|
||||
try {
|
||||
const data: any = await fetchAssetInfoById(sn)
|
||||
// 兼容多种返回结构
|
||||
const d = data?.data
|
||||
if (Array.isArray(d) && d.length > 0) {
|
||||
detailData.value = d[0]
|
||||
} else if (Array.isArray(d?.list) && d.list.length > 0) {
|
||||
detailData.value = d.list[0]
|
||||
} else if (d && typeof d === 'object') {
|
||||
detailData.value = d
|
||||
} else {
|
||||
detailData.value = {}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取资产详情失败', e)
|
||||
uni.showToast({ title: '获取详情失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
function previewImage() {
|
||||
if (detailData.value.assetPic) {
|
||||
uni.previewImage({ urls: [detailData.value.assetPic], current: detailData.value.assetPic,
|
||||
fail: () => uni.showToast({ title: '预览失败,请稍后重试', icon: 'none' }) })
|
||||
} else {
|
||||
uni.showToast({ title: '暂无设备照片', icon: 'none' })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-container { padding: 24rpx; }
|
||||
.card { background: #fff; border-radius: 20rpx; box-shadow: 0 8rpx 24rpx rgba(0,0,0,0.06); }
|
||||
.popup-content { padding: 20rpx; }
|
||||
.popup-title { font-size: 32rpx; font-weight: 600; color: #222; }
|
||||
.title-divider { height: 2rpx; background: #f0f1f5; margin: 12rpx 0 20rpx; }
|
||||
.info-group { margin-bottom: 16rpx; }
|
||||
.group-label { font-size: 26rpx; color: #666; margin-bottom: 6rpx; }
|
||||
.popup-text { font-size: 24rpx; color: #333; margin-top: 6rpx; }
|
||||
.status-tag, .disposal-tag { color: #3a5ddd; }
|
||||
.image-viewer { margin-top: 20rpx; }
|
||||
.image-label { font-size: 26rpx; color: #666; margin-bottom: 8rpx; }
|
||||
.image-container { background: #fafafa; border: 1rpx solid #eee; border-radius: 12rpx; display: flex; align-items: center; justify-content: center; padding: 12rpx; position: relative; }
|
||||
.image { width: 100%; height: 360rpx; }
|
||||
.image-hint { position: absolute; right: 12rpx; bottom: 12rpx; font-size: 22rpx; color: #9a9aa0; }
|
||||
</style>
|
||||
442
src/pages/fixed-assets/index.vue
Normal file
@ -0,0 +1,442 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<!-- 搜索栏 -->
|
||||
<view class="search-bar card">
|
||||
<input v-model="searchName" placeholder="请输入资产名" class="search-input" />
|
||||
<button class="btn btn-primary" @tap="search">搜索</button>
|
||||
</view>
|
||||
|
||||
<!-- 空状态/加载状态 -->
|
||||
<view class="load-status" v-if="loading">加载中...</view>
|
||||
<view class="load-status" v-else-if="!assetList.length">没有更多数据了</view>
|
||||
|
||||
<!-- 资产列表 -->
|
||||
<view class="list">
|
||||
<view class="asset-item card" v-for="item in assetList" :key="item.assetId || item.assetid">
|
||||
<view class="asset-main" @tap="gotoDetail(item)">
|
||||
<view class="asset-name">{{ item.assetName }}</view>
|
||||
<view class="asset-detail">采购日期:{{ item.purchaseDate }}</view>
|
||||
<view class="asset-detail">采购价格:{{ item.purchasePrice }}</view>
|
||||
<view class="asset-detail">采购数量:{{ item.purchaseQuantity }}</view>
|
||||
<view class="asset-detail">设备状态:
|
||||
<text v-if="item.status === 1 || item.status === 0">使用中</text>
|
||||
<text v-else-if="item.status === 2">报废</text>
|
||||
<text v-else-if="item.status === 3">闲置</text>
|
||||
<text v-else-if="item.status === 4">维修中</text>
|
||||
<text v-else-if="item.status === 5">外借</text>
|
||||
<text v-else>未知</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="asset-actions">
|
||||
<button class="btn btn-primary outline" v-if="item.status === 3"
|
||||
@tap.stop="borrow(item)">借用</button>
|
||||
<button class="btn" @tap="gotoShowMaintenanceInfo(item)">维修信息</button>
|
||||
<button class="btn" @tap="gotoDetail(item)">详细信息</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 分页加载状态 -->
|
||||
<view class="load-status">
|
||||
<text v-if="loading && assetList.length">加载中...</text>
|
||||
<text v-else-if="noMore && assetList.length">没有更多数据了</text>
|
||||
</view>
|
||||
|
||||
<!-- 借用弹窗 -->
|
||||
<view class="modal" v-if="showBorrowPopup">
|
||||
<view class="modal-content">
|
||||
<view class="modal-header">
|
||||
<text class="modal-title">借用设备信息</text>
|
||||
<text class="modal-close" @tap="cancelBorrow">×</text>
|
||||
</view>
|
||||
<view class="modal-body">
|
||||
<view class="row"><text class="label">设备名称:</text><text class="value">{{ selectedAsset?.assetName
|
||||
}}</text></view>
|
||||
<view class="form-item">
|
||||
<text class="label">借用人电话:</text>
|
||||
<input v-model="borrowForm.lenderMobile" placeholder="请输入借用人电话" type="number"
|
||||
@input="searchUserByMobile" />
|
||||
<view class="user-suggestions" v-if="userSuggestions.length">
|
||||
<view class="suggestion-item" v-for="u in userSuggestions" :key="u.userMobile"
|
||||
@tap="selectUser(u)">
|
||||
{{ u.userName }} ({{ u.userMobile }}) (id: {{ u.id }})
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- <view class="form-item">
|
||||
<text class="label">借用人姓名:</text>
|
||||
<input v-model="borrowForm.borrowerName" disabled placeholder="请输入手机号查询" />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="label">借用人ID:</text>
|
||||
<input v-model="borrowForm.borrowerId" disabled placeholder="请输入手机号查询" />
|
||||
</view> -->
|
||||
<view class="form-item">
|
||||
<text class="label">借用日期:</text>
|
||||
<picker mode="date" @change="onBorrowDateChange">
|
||||
<view class="picker-value">{{ borrowForm.borrowDate || '请选择借用日期' }}</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="label">预计归还日期:</text>
|
||||
<picker mode="date" @change="onReturnDateChange">
|
||||
<view class="picker-value">{{ borrowForm.returnDate || '请选择归还日期' }}</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="label">备注:</text>
|
||||
<textarea v-model="borrowForm.note" placeholder="请输入备注" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="modal-footer">
|
||||
<button class="btn" @tap="cancelBorrow">取消</button>
|
||||
<button class="btn btn-primary" @tap="confirmBorrow">确认</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { onLoad, onReachBottom } from '@dcloudio/uni-app'
|
||||
// @ts-ignore JS模块无类型声明
|
||||
import { searchFixedAssets, fetchFixedAssets, fetchMaintenanceInfoById, borrowAssetById, returnAsset, searchUserByMobile as apiSearchUserByMobile } from '@/pages/api/fixedAssets.js'
|
||||
|
||||
interface AssetItem { [k: string]: any }
|
||||
|
||||
const assetList = ref<AssetItem[]>([])
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const loading = ref(false)
|
||||
const noMore = ref(false)
|
||||
const searchName = ref('')
|
||||
|
||||
const showBorrowPopup = ref(false)
|
||||
const selectedAsset = ref<AssetItem | null>(null)
|
||||
const userInfo = uni.getStorageSync('staff') || {}
|
||||
const borrowForm = ref({
|
||||
lenderMobile: '',
|
||||
borrowDate: '',
|
||||
returnDate: '',
|
||||
location: '',
|
||||
borrowDepartmentId: '',
|
||||
approverId: '',
|
||||
approverMobile: '',
|
||||
note: ''
|
||||
})
|
||||
const userSuggestions = ref<any[]>([])
|
||||
let searchTimeout: any = null
|
||||
const isSearching = ref(false)
|
||||
|
||||
onLoad(() => {
|
||||
getAssetList()
|
||||
})
|
||||
|
||||
onReachBottom(() => {
|
||||
if (!loading.value && !noMore.value) {
|
||||
page.value++
|
||||
getAssetList()
|
||||
}
|
||||
})
|
||||
|
||||
async function search() {
|
||||
loading.value = true
|
||||
try {
|
||||
if (!searchName.value) {
|
||||
assetList.value = []
|
||||
page.value = 1
|
||||
noMore.value = false
|
||||
await getAssetList()
|
||||
return
|
||||
}
|
||||
const data: any = await searchFixedAssets(searchName.value)
|
||||
assetList.value = data?.data || []
|
||||
noMore.value = true
|
||||
} catch (e) {
|
||||
console.error('获取数据失败', e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function getAssetList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const data: any = await fetchFixedAssets(page.value, pageSize.value)
|
||||
const list = data?.data?.list || []
|
||||
if (list.length < pageSize.value) noMore.value = true
|
||||
assetList.value = assetList.value.concat(list)
|
||||
} catch (e) {
|
||||
console.error('获取数据失败', e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function gotoDetail(item: AssetItem) {
|
||||
const sn = item.assetSn || item.assetSN || item.asset_sn
|
||||
if (sn) {
|
||||
uni.navigateTo({ url: `/pages/fixed-assets/detail?assetSn=${encodeURIComponent(sn)}` })
|
||||
} else {
|
||||
uni.navigateTo({ url: '/pages/fixed-assets/detail', success(res) { res.eventChannel.emit('sendData', { data: item }) } })
|
||||
}
|
||||
}
|
||||
|
||||
function gotoShowMaintenanceInfo(item: AssetItem) {
|
||||
const id = item.assetId || item.assetid
|
||||
if (id)
|
||||
uni.navigateTo({ url: `/pages/fixed-assets/maintenance-info?assetId=${id}` })
|
||||
else
|
||||
uni.navigateTo({
|
||||
url: '/pages/fixed-assets/maintenance-info',
|
||||
success(res) { res.eventChannel.emit('sendData', { data: item }) }
|
||||
})
|
||||
}
|
||||
|
||||
function borrow(item: AssetItem) {
|
||||
selectedAsset.value = item
|
||||
showBorrowPopup.value = true
|
||||
userSuggestions.value = []
|
||||
borrowForm.value.lenderMobile = ''
|
||||
}
|
||||
|
||||
function cancelBorrow() {
|
||||
showBorrowPopup.value = false
|
||||
userSuggestions.value = []
|
||||
}
|
||||
|
||||
function onBorrowDateChange(e: any) { borrowForm.value.borrowDate = e.detail.value }
|
||||
function onReturnDateChange(e: any) { borrowForm.value.returnDate = e.detail.value }
|
||||
|
||||
async function searchUserByMobile() {
|
||||
const mobile = borrowForm.value.lenderMobile
|
||||
if (searchTimeout) clearTimeout(searchTimeout)
|
||||
if (!mobile || mobile.length < 3) { userSuggestions.value = []; return }
|
||||
isSearching.value = true
|
||||
searchTimeout = setTimeout(async () => {
|
||||
try {
|
||||
const resp: any = await apiSearchUserByMobile(mobile)
|
||||
userSuggestions.value = resp.data ?? []
|
||||
} catch (err) {
|
||||
userSuggestions.value = []
|
||||
uni.showToast({ title: '搜索失败', icon: 'none' })
|
||||
} finally {
|
||||
isSearching.value = false
|
||||
}
|
||||
}, 500)
|
||||
}
|
||||
|
||||
function selectUser(user: any) {
|
||||
borrowForm.value.lenderMobile = user.userMobile
|
||||
}
|
||||
|
||||
async function confirmBorrow() {
|
||||
const s = selectedAsset.value || {}
|
||||
const payload = {
|
||||
assetId: s.assetId || s.assetid,
|
||||
assetName: s.assetName,
|
||||
borrowDate: borrowForm.value.borrowDate,
|
||||
borrowerMobile: borrowForm.value.lenderMobile,
|
||||
lenderMobile: s.headerMobile,
|
||||
note: borrowForm.value.note,
|
||||
registrantDate: new Date().toISOString(),
|
||||
registrantId: s.registrantId,
|
||||
registrantMobile: s.registrantMobile,
|
||||
returnDate: borrowForm.value.returnDate
|
||||
}
|
||||
try {
|
||||
await borrowAssetById(payload)
|
||||
uni.showModal({ content: '借用成功', showCancel: false })
|
||||
showBorrowPopup.value = false
|
||||
// 刷新当前页
|
||||
assetList.value = []
|
||||
page.value = 1
|
||||
noMore.value = false
|
||||
await getAssetList()
|
||||
} catch (err) {
|
||||
uni.showToast({ title: '借用失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
display: flex;
|
||||
gap: 12rpx;
|
||||
padding: 20rpx;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
background: #f5f7fb;
|
||||
border-radius: 12rpx;
|
||||
padding: 12rpx 16rpx;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 0 20rpx;
|
||||
background: #e9efff;
|
||||
color: #3a5ddd;
|
||||
border-radius: 10rpx;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #3a5ddd;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-primary.outline {
|
||||
background: #fff;
|
||||
color: #3a5ddd;
|
||||
border: 2rpx solid #3a5ddd;
|
||||
}
|
||||
|
||||
.list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.asset-item {
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.asset-main {
|
||||
border-bottom: 1rpx solid #f0f1f5;
|
||||
padding-bottom: 16rpx;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.asset-name {
|
||||
font-size: 30rpx;
|
||||
color: #222;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.asset-detail {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
margin-top: 6rpx;
|
||||
}
|
||||
|
||||
.asset-actions {
|
||||
display: flex;
|
||||
gap: 12rpx;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.load-status {
|
||||
text-align: center;
|
||||
color: #9a9aa0;
|
||||
margin: 20rpx 0;
|
||||
}
|
||||
|
||||
/* 弹窗样式与项目统一 */
|
||||
.modal {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
width: 86%;
|
||||
max-height: 76vh;
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 20rpx 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1rpx solid #eee;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 30rpx;
|
||||
color: #222;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
font-size: 40rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 16rpx 24rpx;
|
||||
max-height: 60vh;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
padding: 16rpx 24rpx;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 16rpx;
|
||||
border-top: 1rpx solid #eee;
|
||||
}
|
||||
|
||||
.modal-footer .btn {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: #666;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: #333;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
margin: 16rpx 0;
|
||||
}
|
||||
|
||||
.picker-value {
|
||||
padding: 10rpx 12rpx;
|
||||
background: #f5f7fb;
|
||||
border-radius: 8rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.user-suggestions {
|
||||
background: #fff;
|
||||
border: 1rpx solid #eee;
|
||||
border-radius: 8rpx;
|
||||
margin-top: 8rpx;
|
||||
max-height: 240rpx;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.suggestion-item {
|
||||
padding: 10rpx 12rpx;
|
||||
border-bottom: 1rpx solid #f5f5f5;
|
||||
}
|
||||
</style>
|
||||
64
src/pages/fixed-assets/inventory-end.vue
Normal file
@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<view class="borrow-list">
|
||||
<view v-for="item in detailList" :key="item.assetId" class="borrow-item card">
|
||||
<view class="item-header"><text class="asset-name">{{ item.assetName }}</text></view>
|
||||
<view class="content">
|
||||
<view class="row">📅 盘点日期:<text class="value">{{ item.inventoryDate }}</text></view>
|
||||
<view class="row">👤 盘点人:<text class="value">{{ item.inventoryManName }}</text></view>
|
||||
<view class="row">📝 备注:<text class="value">{{ item.note || '无' }}</text></view>
|
||||
</view>
|
||||
<view class="result-row">
|
||||
<text class="label">盘点结果:</text>
|
||||
<text class="result normal" v-if="item.inventoryResult==0">正常</text>
|
||||
<text class="result damaged" v-else-if="item.inventoryResult==1">损坏</text>
|
||||
<text class="result lost" v-else-if="item.inventoryResult==2">丢失</text>
|
||||
<text class="result unknown" v-else>未知</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="none" v-if="detailList.length===0">没有更多数据了</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
// @ts-ignore JS模块无类型声明
|
||||
import { getInventoryDetailEnd } from '@/pages/api/fixedAssets.js'
|
||||
|
||||
const planId = ref<string | number | null>(null)
|
||||
const detailList = ref<any[]>([])
|
||||
|
||||
onLoad(async (options?: Record<string, any>) => {
|
||||
planId.value = options?.planId || null
|
||||
if (!planId.value) return
|
||||
await loadDetail()
|
||||
})
|
||||
|
||||
async function loadDetail() {
|
||||
try {
|
||||
const res: any = await getInventoryDetailEnd(planId.value)
|
||||
detailList.value = res?.data || []
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page { min-height: 100vh; background: #f5f7fb; padding: 24rpx; }
|
||||
.card { background: #fff; border-radius: 20rpx; box-shadow: 0 8rpx 24rpx rgba(0,0,0,0.06); padding: 20rpx; margin-bottom: 16rpx; }
|
||||
.asset-name { font-size: 30rpx; color: #222; font-weight: 600; }
|
||||
.row { font-size: 24rpx; color: #666; margin-top: 6rpx; }
|
||||
.result-row { margin-top: 8rpx; }
|
||||
.label { font-size: 24rpx; color: #666; margin-right: 8rpx; }
|
||||
.result { font-size: 24rpx; }
|
||||
.result.normal { color: #38b000; }
|
||||
.result.damaged { color: #ff6b6b; }
|
||||
.result.lost { color: #f39c12; }
|
||||
.result.unknown { color: #999; }
|
||||
.none { text-align: center; color: #9a9aa0; margin-top: 20rpx; }
|
||||
.value { color: #333; }
|
||||
</style>
|
||||
99
src/pages/fixed-assets/inventory-ing.vue
Normal file
@ -0,0 +1,99 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<view class="borrow-list">
|
||||
<view v-for="item in detailList" :key="item.assetId" class="borrow-item card">
|
||||
<view class="item-header"><text class="asset-name">设备名称:{{ item.assetName }}</text></view>
|
||||
<view class="detail">使用人:<text class="value">{{ item.useManName }}</text></view>
|
||||
<view class="detail">设备购入日期:<text class="value">{{ item.purchaseDate }}</text></view>
|
||||
<view class="detail">负责人:<text class="value">{{ item.headerName }}</text></view>
|
||||
<view class="detail">备注:<text class="value">{{ item.note || '无' }}</text></view>
|
||||
<view class="detail">盘点状态:<text class="value">{{ item.isInventory }}</text></view>
|
||||
<view class="action-row" v-if="item.isInventory!='已盘点'">
|
||||
<view @tap="setInventory(0,item)" class="inventory-btn inventory-win">正常</view>
|
||||
<view @tap="setInventory(1,item)" class="inventory-btn inventory-lose">损坏</view>
|
||||
<view @tap="setInventory(2,item)" class="inventory-btn inventory-normal">丢失</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="none" v-if="detailList.length===0">没有更多数据了</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
// @ts-ignore JS模块无类型声明
|
||||
import { getInventoryDetailIng, inventory } from '@/pages/api/fixedAssets.js'
|
||||
|
||||
const planId = ref<string | number | null>(null)
|
||||
const userInfo = uni.getStorageSync('staff') || {}
|
||||
const detailList = ref<any[]>([])
|
||||
|
||||
const inventoryInfo = ref({
|
||||
assetId: '',
|
||||
inventoryManId: '',
|
||||
inventoryManMobile: '',
|
||||
inventoryPic: '',
|
||||
inventoryPlanId: '',
|
||||
inventoryResult: '',
|
||||
inventoryDate: ''
|
||||
})
|
||||
|
||||
onLoad(async (options?: Record<string, any>) => {
|
||||
planId.value = options?.planId || null
|
||||
if (!planId.value) return
|
||||
await loadDetail()
|
||||
})
|
||||
|
||||
async function loadDetail() {
|
||||
try {
|
||||
const res: any = await getInventoryDetailIng(planId.value)
|
||||
detailList.value = res?.data || []
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
function setInventory(status: number, item: any) {
|
||||
const statusMap: Record<number, string> = { 0: '正常', 1: '损坏', 2: '丢失' }
|
||||
const statusText = statusMap[status] || '未知'
|
||||
uni.showModal({
|
||||
title: '确认操作',
|
||||
content: `确定要将盘点状态设置为【${statusText}】吗?`,
|
||||
success: async (r) => {
|
||||
if (r.confirm) {
|
||||
const payload = {
|
||||
assetId: item.assetId,
|
||||
inventoryManId: userInfo.id,
|
||||
inventoryManMobile: userInfo.mobile,
|
||||
inventoryPic: '',
|
||||
inventoryPlanId: planId.value,
|
||||
inventoryResult: status
|
||||
}
|
||||
try {
|
||||
await inventory(payload)
|
||||
uni.showToast({ title: '提交成功', icon: 'success' })
|
||||
await loadDetail()
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '提交失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page { min-height: 100vh; background: #f5f7fb; padding: 24rpx; }
|
||||
.card { background: #fff; border-radius: 20rpx; box-shadow: 0 8rpx 24rpx rgba(0,0,0,0.06); padding: 20rpx; margin-bottom: 16rpx; }
|
||||
.asset-name { font-size: 30rpx; color: #222; font-weight: 600; }
|
||||
.detail { font-size: 24rpx; color: #666; margin-top: 6rpx; }
|
||||
.action-row { display: flex; gap: 12rpx; margin-top: 10rpx; }
|
||||
.inventory-btn { padding: 12rpx 20rpx; border-radius: 10rpx; color: #fff; font-size: 24rpx; }
|
||||
.inventory-win { background: #38b000; }
|
||||
.inventory-lose { background: #ff6b6b; }
|
||||
.inventory-normal { background: #f39c12; }
|
||||
.none { text-align: center; color: #9a9aa0; margin-top: 20rpx; }
|
||||
.value { color: #333; }
|
||||
</style>
|
||||
170
src/pages/fixed-assets/inventory-plan.vue
Normal file
@ -0,0 +1,170 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<view class="load-status" v-if="!inventoryPlanList.length && !loading">没有更多数据了</view>
|
||||
<view class="inventory-list">
|
||||
<view class="inventory-item card" v-for="item in inventoryPlanList" :key="item.inventoryPlanId">
|
||||
<view class="left" @tap="gotoPlanDetail(item)">
|
||||
<view class="header"><text class="title">📋 盘点计划</text></view>
|
||||
<view class="content">
|
||||
<view class="row">开始日期:<text class="value">{{ item.inventoryStartDate }}</text></view>
|
||||
<view class="row">结束日期:<text class="value">{{ item.inventoryEndDate }}</text></view>
|
||||
<view class="row">盘点人:<text class="value">{{ item.inventoryManName }}</text></view>
|
||||
<view class="row">备注:<text class="value">{{ item.note || '无' }}</text></view>
|
||||
</view>
|
||||
<view class="status">计划状态:<text class="value">{{ item.status || '无' }}</text></view>
|
||||
</view>
|
||||
<view class="preview" @click="previewImage(item.image)">
|
||||
<image class="preview-img" :src="'data:image/png;base64,' + item.image" mode="aspectFill" />
|
||||
</view>
|
||||
|
||||
</view>
|
||||
</view>
|
||||
<view class="load-status">
|
||||
<text v-if="loading">加载中...</text>
|
||||
<text v-else-if="noMore && inventoryPlanList.length">没有更多数据了</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { onLoad, onReachBottom } from '@dcloudio/uni-app'
|
||||
// @ts-ignore JS模块无类型声明
|
||||
import { getInventoryPlan } from '@/pages/api/fixedAssets.js'
|
||||
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const loading = ref(false)
|
||||
const noMore = ref(false)
|
||||
const inventoryPlanList = ref<any[]>([])
|
||||
|
||||
onLoad(() => { fetchInventoryList() })
|
||||
|
||||
onReachBottom(() => {
|
||||
if (!loading.value && !noMore.value) { page.value++; fetchInventoryList() }
|
||||
})
|
||||
|
||||
async function fetchInventoryList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const data: any = await getInventoryPlan(page.value, pageSize.value)
|
||||
const list = data?.data?.list || []
|
||||
if (list.length < pageSize.value) noMore.value = true
|
||||
console.log(list);
|
||||
|
||||
inventoryPlanList.value = inventoryPlanList.value.concat(list)
|
||||
} catch (e) {
|
||||
console.error('获取数据失败', e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function gotoPlanDetail(item: any) {
|
||||
const id = item.inventoryPlanId || item.inventoryId || item.id
|
||||
if (!id) return
|
||||
if (item.status === '已结束') {
|
||||
uni.navigateTo({ url: `/pages/fixed-assets/inventory-end?planId=${id}` })
|
||||
} else if (item.status === '正在进行') {
|
||||
uni.navigateTo({ url: `/pages/fixed-assets/inventory-ing?planId=${id}` })
|
||||
} else {
|
||||
// 未知状态,默认当进行中处理
|
||||
uni.navigateTo({ url: `/pages/fixed-assets/inventory-ing?planId=${id}` })
|
||||
}
|
||||
}
|
||||
|
||||
function previewImage(imageBase64: string) {
|
||||
if (!imageBase64) {
|
||||
uni.showToast({ title: '无图片可预览', icon: 'none' })
|
||||
return
|
||||
}
|
||||
uni.previewImage({
|
||||
urls: ['data:image/png;base64,' + imageBase64],
|
||||
current: 'data:image/png;base64,' + imageBase64
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #f5f7fb;
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.inventory-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.inventory-item {
|
||||
padding: 20rpx;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
/* 让左右两侧等高,右侧图片可拉满高度 */
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.left {
|
||||
/* 按内容宽度,不挤占右侧空间 */
|
||||
flex: 0 0 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.preview {
|
||||
/* 占据剩余空间 */
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
/* 使内部图片可以 100% 拉伸 */
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 28rpx;
|
||||
color: #222;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.content .row {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
margin-top: 6rpx;
|
||||
}
|
||||
|
||||
.status {
|
||||
font-size: 24rpx;
|
||||
color: #333;
|
||||
margin-top: 10rpx;
|
||||
}
|
||||
|
||||
.load-status {
|
||||
text-align: center;
|
||||
color: #9a9aa0;
|
||||
margin: 20rpx 0;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* 右侧图片自适应填满容器,保持裁剪 */
|
||||
.preview-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
/* H5 有效;小程序端由 mode=aspectFill 生效 */
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
</style>
|
||||
74
src/pages/fixed-assets/maintenance-info.vue
Normal file
@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<view class="page-container">
|
||||
<view class="error" v-if="!maintenanceInfo?.data">
|
||||
{{ maintenanceInfo?.msg || '未查询到维修信息' }}
|
||||
</view>
|
||||
<view v-else>
|
||||
<view class="maintenance-item card" v-for="item in maintenanceInfo.data" :key="item.id || item.maintenanceDate">
|
||||
<view class="popup-text">资产名称:{{ item.assetName }}</view>
|
||||
<view class="popup-text">维修日期:{{ item.maintenanceDate }}</view>
|
||||
<view class="popup-text">维修费用:{{ item.maintenanceCost }}</view>
|
||||
<view class="popup-text">维修单位:{{ item.maintenanceDapartment }}</view>
|
||||
<view class="popup-text">维修人员:{{ item.maintenanceMan }}</view>
|
||||
<view class="popup-text">联系方式:{{ item.maintenancePhone }}</view>
|
||||
<view class="popup-text">登记人员:{{ item.registrantName }}</view>
|
||||
<view class="popup-text">登记日期:{{ item.registrantDate }}</view>
|
||||
<view class="popup-text">备注:{{ item.note }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { onLoad, onShow } from '@dcloudio/uni-app'
|
||||
// @ts-ignore JS模块无类型声明
|
||||
import { fetchMaintenanceInfoById } from '@/pages/api/fixedAssets.js'
|
||||
|
||||
const maintenanceInfo = ref<any>({})
|
||||
const assetId = ref<string | number | null>(null)
|
||||
|
||||
onLoad((options?: Record<string, any>) => {
|
||||
// 1) 优先从路由参数读取 assetId
|
||||
const fromQuery = options?.assetId || options?.assetid
|
||||
if (fromQuery) {
|
||||
assetId.value = fromQuery
|
||||
}
|
||||
|
||||
// 2) 回退:监听 eventChannel(兼容旧的跳转方式)
|
||||
if (!assetId.value) {
|
||||
const pages = getCurrentPages()
|
||||
// @ts-ignore uni-app 小程序端提供,类型定义里可能缺失
|
||||
const eventChannel = (pages[pages.length - 1] as any)?.getOpenerEventChannel?.()
|
||||
eventChannel?.on('sendData', (res: any) => {
|
||||
const data = res?.data || {}
|
||||
assetId.value = data.assetId || data.assetid || null
|
||||
if (assetId.value) fetchById(assetId.value)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
onShow(() => {
|
||||
// 页面可见时,如果已拿到 assetId 且还未加载数据,则发起请求
|
||||
if (assetId.value && !maintenanceInfo.value?.data) {
|
||||
fetchById(assetId.value)
|
||||
}
|
||||
})
|
||||
|
||||
async function fetchById(id: string | number) {
|
||||
try {
|
||||
const data: any = await fetchMaintenanceInfoById(id)
|
||||
maintenanceInfo.value = data
|
||||
} catch (e) {
|
||||
console.error('获取数据失败', e)
|
||||
maintenanceInfo.value = { msg: '获取数据失败' }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-container { padding: 24rpx; }
|
||||
.card { background: #fff; border-radius: 20rpx; box-shadow: 0 8rpx 24rpx rgba(0,0,0,0.06); padding: 20rpx; margin-bottom: 16rpx; }
|
||||
.popup-text { font-size: 24rpx; color: #333; margin-top: 6rpx; }
|
||||
.error { color: #ef5350; background: #fff; border-radius: 12rpx; padding: 20rpx; }
|
||||
</style>
|
||||
133
src/pages/fixed-assets/my-borrows.vue
Normal file
@ -0,0 +1,133 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<view class="borrow-list">
|
||||
<view v-for="item in borrowList.filter(item => !item.isReturn)" :key="item.borrowId || item.id"
|
||||
class="borrow-item card">
|
||||
|
||||
<view class="item-header">
|
||||
<text class="asset-name">{{ item.assetName }}</text>
|
||||
<!-- <button class="btn btn-primary outline" @tap="button_returnAsset(item)">归还</button> -->
|
||||
</view>
|
||||
<view class="detail">借用日期:{{ item.borrowDate }}</view>
|
||||
<view class="detail">预计归还日期:{{ item.returnDate }}</view>
|
||||
<view class="detail">借用人:{{ item.borrowerName }}({{ item.borrowerMobile }})</view>
|
||||
<view class="detail">出借人:{{ item.lenderName }}({{ item.lenderMobile }})</view>
|
||||
<view class="detail">备注:{{ item.note || '无' }}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="empty" v-if="borrowList.length === 0">暂无更多数据</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { onLoad, onShow } from '@dcloudio/uni-app'
|
||||
// @ts-ignore JS模块无类型声明
|
||||
import { borrowInfo, returnAsset } from '@/pages/api/fixedAssets.js'
|
||||
|
||||
const borrowList = ref<any[]>([])
|
||||
const userInfo = uni.getStorageSync('staff') || {}
|
||||
|
||||
onLoad(() => { loadBorrowList() })
|
||||
|
||||
onShow(() => { loadBorrowList() })
|
||||
|
||||
function loadBorrowList() {
|
||||
if (!userInfo?.id) {
|
||||
borrowList.value = []
|
||||
return
|
||||
}
|
||||
borrowInfo(userInfo.id)
|
||||
.then((res: any) => { borrowList.value = res?.data || [] })
|
||||
.catch((err: any) => {
|
||||
console.error(err)
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
})
|
||||
}
|
||||
|
||||
function button_returnAsset(item: any) {
|
||||
uni.showModal({
|
||||
content: '确认归还?',
|
||||
success: async (r) => {
|
||||
if (r.confirm) {
|
||||
try {
|
||||
await returnAsset(item.assetId)
|
||||
uni.showToast({ title: '归还成功', icon: 'success' })
|
||||
loadBorrowList()
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '归还失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #f5f7fb;
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.borrow-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.borrow-item {
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.item-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.asset-name {
|
||||
font-size: 30rpx;
|
||||
color: #222;
|
||||
font-weight: 600;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.detail {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
margin-top: 6rpx;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 0 20rpx;
|
||||
background: #e9efff;
|
||||
color: #3a5ddd;
|
||||
border-radius: 10rpx;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #3a5ddd;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-primary.outline {
|
||||
background: #fff;
|
||||
color: #3a5ddd;
|
||||
border: 2rpx solid #3a5ddd;
|
||||
}
|
||||
|
||||
.empty {
|
||||
text-align: center;
|
||||
color: #9a9aa0;
|
||||
margin-top: 40rpx;
|
||||
}
|
||||
</style>
|
||||
150
src/pages/fixed-assets/scan.vue
Normal file
@ -0,0 +1,150 @@
|
||||
<template>
|
||||
<scroll-view scroll-y class="page">
|
||||
<view class="card">
|
||||
<view class="title">扫码查看信息</view>
|
||||
<view class="desc">请扫描资产二维码。二维码内容为资产编号 assetSn,我们会自动跳转到资产详情页。</view>
|
||||
<button class="scan-btn" @tap="startScan">开始扫码</button>
|
||||
<view v-if="lastContent" class="last">最近一次扫描内容:{{ lastContent }}</view>
|
||||
<view class="tips">如扫码失败,可点击上方按钮重试。</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
const lastContent = ref('')
|
||||
|
||||
function extractAssetSn(raw: string): string | null {
|
||||
if (!raw) return null
|
||||
const text = String(raw).trim()
|
||||
// 优先匹配 URL 中的 assetSn 参数
|
||||
const m = text.match(/[?#&]assetSn=([^&#]+)/i)
|
||||
if (m && m[1]) {
|
||||
try {
|
||||
return decodeURIComponent(m[1])
|
||||
} catch (_) {
|
||||
return m[1]
|
||||
}
|
||||
}
|
||||
// 否则直接当作 assetSn
|
||||
return text || null
|
||||
}
|
||||
|
||||
function toDetail(assetSn: string) {
|
||||
if (!assetSn) {
|
||||
uni.showToast({ title: '二维码无效', icon: 'none' })
|
||||
return
|
||||
}
|
||||
uni.navigateTo({ url: `/pages/fixed-assets/detail?assetSn=${encodeURIComponent(assetSn)}` })
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
import jzH5ScanCode from '@/uni_modules/jz-h5-scanCode/js/index.js'
|
||||
|
||||
function startScan() {
|
||||
// 导入插件
|
||||
// #ifdef WEB
|
||||
jzH5ScanCode.scanCode({
|
||||
success: (res: any) => {
|
||||
console.log('扫码成功:', res.result)
|
||||
const content = res.result
|
||||
lastContent.value = content || ''
|
||||
const sn = extractAssetSn(content)
|
||||
if (sn) {
|
||||
toDetail(sn)
|
||||
} else {
|
||||
uni.showToast({ title: '未识别到资产编号', icon: 'none' })
|
||||
}
|
||||
|
||||
},
|
||||
fail: (res: any) => {
|
||||
console.log('扫码失败:', res.errMsg)
|
||||
uni.showToast({ title: '扫码已取消,' + res.errMsg, icon: 'none' })
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
|
||||
// #ifndef WEB
|
||||
uni.scanCode({
|
||||
scanType: ['qrCode'],
|
||||
autoZoom: false,
|
||||
|
||||
success: (res) => {
|
||||
const content = (res as any)?.result || (res as any)?.path || ''
|
||||
lastContent.value = content || ''
|
||||
const sn = extractAssetSn(content)
|
||||
if (sn) {
|
||||
toDetail(sn)
|
||||
} else {
|
||||
uni.showToast({ title: '未识别到资产编号', icon: 'none' })
|
||||
}
|
||||
},
|
||||
fail: (e) => {
|
||||
uni.showToast({ title: '扫码已取消,' + JSON.stringify(e), icon: 'none' })
|
||||
},
|
||||
complete: () => {
|
||||
console.log(1);
|
||||
|
||||
// 扫码完成
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
}
|
||||
|
||||
/* onMounted(() => {
|
||||
// 进入页面即触发一次扫码,便于快速使用
|
||||
startScan()
|
||||
}) */
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
padding: 24rpx;
|
||||
background: #f5f7fb;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 28rpx;
|
||||
box-shadow: 0 6rpx 16rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.desc {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
margin-bottom: 24rpx;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.scan-btn {
|
||||
background: #4b7aff;
|
||||
color: #fff;
|
||||
border-radius: 12rpx;
|
||||
padding: 0;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.last {
|
||||
margin-top: 16rpx;
|
||||
color: #444;
|
||||
font-size: 24rpx;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.tips {
|
||||
margin-top: 8rpx;
|
||||
color: #9a9aa0;
|
||||
font-size: 22rpx;
|
||||
}
|
||||
</style>
|
||||
@ -19,19 +19,9 @@
|
||||
|
||||
<!-- 顶部滑动轮播图 -->
|
||||
<view class="swiper-card card">
|
||||
<swiper
|
||||
class="swiper"
|
||||
:current="swiperIndex"
|
||||
@change="changeSwiper"
|
||||
previous-margin="50rpx"
|
||||
next-margin="50rpx"
|
||||
:indicator-dots="true"
|
||||
indicator-color="rgba(51,51,51,0.25)"
|
||||
indicator-active-color="#4b7aff"
|
||||
autoplay
|
||||
interval="3000"
|
||||
circular
|
||||
>
|
||||
<swiper class="swiper" :current="swiperIndex" @change="changeSwiper" previous-margin="50rpx" next-margin="50rpx"
|
||||
:indicator-dots="true" indicator-color="rgba(51,51,51,0.25)" indicator-active-color="#4b7aff" autoplay
|
||||
interval="3000" circular>
|
||||
<swiper-item v-for="(img, idx) in swiperImgs" :key="idx">
|
||||
<view class="swiper-item-wrap" :class="{ 'swiper-scale': swiperIndex !== idx }">
|
||||
<image class="swiper-img" :src="img" mode="aspectFill" />
|
||||
@ -100,8 +90,8 @@ const assetOpen = ref(true)
|
||||
const assetItems = [
|
||||
{ key: 'fixed', text: '固定资产', icon: '/static/icons/fixed-assets.png' },
|
||||
{ key: 'mine', text: '我的借用', icon: '/static/icons/my-loans.png' },
|
||||
{ key: 'inventory', text: '资产盘点', icon: '/static/icons/asset-inventory.png' },
|
||||
{ key: 'scan', text: '扫码查看信息', icon: '/static/icons/scan-to-view-info.png' },
|
||||
{ key: 'inventory', text: '资产盘点', icon: '/static/icons/asset-inventory.png' },
|
||||
{ key: 'plan', text: '易耗品领用计划', icon: '/static/icons/consumables-plan.png' },
|
||||
{ key: 'consumable', text: '易耗品盘点', icon: '/static/icons/consumables-inventory.png' }
|
||||
]
|
||||
@ -202,7 +192,34 @@ function toggleAsset() {
|
||||
assetOpen.value = !assetOpen.value
|
||||
}
|
||||
function handleAsset(key: string) {
|
||||
uni.showToast({ title: `打开:${assetItems.find(i => i.key === key)?.text}`, icon: 'none' })
|
||||
const item = assetItems.find(i => i.key === key)
|
||||
if (!item) return
|
||||
if (key === 'fixed') {
|
||||
uni.navigateTo({ url: '/pages/fixed-assets/index' })
|
||||
return
|
||||
}
|
||||
if (key === 'mine') {
|
||||
uni.navigateTo({ url: '/pages/fixed-assets/my-borrows' })
|
||||
return
|
||||
}
|
||||
if (key === 'inventory') {
|
||||
uni.navigateTo({ url: '/pages/fixed-assets/inventory-plan' })
|
||||
return
|
||||
}
|
||||
if (key === 'plan') {
|
||||
uni.navigateTo({ url: '/pages/consumables/plan' })
|
||||
return
|
||||
}
|
||||
if (key === 'consumable') {
|
||||
uni.navigateTo({ url: '/pages/consumables/inventory-plan' })
|
||||
return
|
||||
}
|
||||
if (key === 'scan') {
|
||||
uni.navigateTo({ url: '/pages/fixed-assets/scan' })
|
||||
return
|
||||
}
|
||||
// 其他入口可在此对接具体页面
|
||||
uni.showToast({ title: `打开:${item.text}`, icon: 'none' })
|
||||
}
|
||||
function previewCharge() {
|
||||
uni.previewImage({
|
||||
@ -220,7 +237,7 @@ function previewCharge() {
|
||||
.card {
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
box-shadow: 0 8rpx 24rpx rgba(0,0,0,0.06);
|
||||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.header {
|
||||
@ -230,31 +247,95 @@ function previewCharge() {
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10rpx;
|
||||
}
|
||||
.hello { color: #7a7a7a; font-size: 26rpx; }
|
||||
.app-name { color: #111; font-size: 40rpx; font-weight: 600; }
|
||||
.sub { color: #9a9aa0; font-size: 24rpx; }
|
||||
.logo { width: 120rpx; height: 120rpx; }
|
||||
.header-right { display: flex; align-items: center; gap: 12rpx; }
|
||||
.login-btn, .logout-btn { padding: 10rpx 20rpx; background: #3a5ddd; color: #fff; border-radius: 10rpx; font-size: 24rpx; }
|
||||
.logout-btn { background: #ef5350; }
|
||||
.user-box { display: flex; align-items: center; gap: 12rpx; }
|
||||
.user-name { font-size: 24rpx; color: #333; }
|
||||
|
||||
.hello {
|
||||
color: #7a7a7a;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.app-name {
|
||||
color: #111;
|
||||
font-size: 40rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.sub {
|
||||
color: #9a9aa0;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.login-btn,
|
||||
.logout-btn {
|
||||
padding: 0rpx 20rpx;
|
||||
background: #3a5ddd;
|
||||
color: #fff;
|
||||
border-radius: 10rpx;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
background: #ef5350;
|
||||
}
|
||||
|
||||
.user-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-size: 24rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* 轮播卡片 */
|
||||
.swiper-card { margin: 0 32rpx 24rpx; padding: 12rpx; }
|
||||
.swiper { width: 100%; height: 300rpx; }
|
||||
.swiper-item-wrap {
|
||||
width: 100%; height: 100%; border-radius: 24rpx; overflow: hidden;
|
||||
transition: transform 0.25s ease, box-shadow 0.25s ease;
|
||||
box-shadow: 0 6rpx 18rpx rgba(0,0,0,0.08);
|
||||
.swiper-card {
|
||||
margin: 0 32rpx 24rpx;
|
||||
padding: 12rpx;
|
||||
}
|
||||
|
||||
.swiper {
|
||||
width: 100%;
|
||||
height: 300rpx;
|
||||
}
|
||||
|
||||
.swiper-item-wrap {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 24rpx;
|
||||
overflow: hidden;
|
||||
transition: transform 0.25s ease, box-shadow 0.25s ease;
|
||||
box-shadow: 0 6rpx 18rpx rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.swiper-scale {
|
||||
transform: scale(0.95);
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.swiper-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.swiper-scale { transform: scale(0.95); box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.06); }
|
||||
.swiper-img { width: 100%; height: 100%; display: block; }
|
||||
|
||||
.ops {
|
||||
margin: 0 32rpx 24rpx;
|
||||
@ -262,6 +343,7 @@ function previewCharge() {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.op-btn {
|
||||
background: linear-gradient(180deg, #f9fbff 0%, #ffffff 100%);
|
||||
border-radius: 20rpx;
|
||||
@ -271,23 +353,57 @@ function previewCharge() {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12rpx;
|
||||
box-shadow: 0 6rpx 16rpx rgba(0,0,0,0.04);
|
||||
box-shadow: 0 6rpx 16rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.op-icon {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
}
|
||||
|
||||
.op-text {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.section-card {
|
||||
margin: 0 32rpx 24rpx;
|
||||
}
|
||||
.op-icon { width: 80rpx; height: 80rpx; }
|
||||
.op-text { font-size: 26rpx; color: #333; }
|
||||
|
||||
.section-card { margin: 0 32rpx 24rpx; }
|
||||
.section-header {
|
||||
padding: 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.section-left { display: flex; align-items: center; gap: 16rpx; }
|
||||
.section-icon { width: 48rpx; height: 48rpx; }
|
||||
.section-text { display: flex; flex-direction: column; }
|
||||
.section-title-text { font-size: 30rpx; color: #222; font-weight: 600; }
|
||||
.section-sub { font-size: 22rpx; color: #9a9aa0; }
|
||||
|
||||
.section-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.section-icon {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
}
|
||||
|
||||
.section-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.section-title-text {
|
||||
font-size: 30rpx;
|
||||
color: #222;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.section-sub {
|
||||
font-size: 22rpx;
|
||||
color: #9a9aa0;
|
||||
}
|
||||
|
||||
.chevron {
|
||||
width: 24rpx;
|
||||
height: 24rpx;
|
||||
@ -296,7 +412,10 @@ function previewCharge() {
|
||||
transform: rotate(45deg);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
.chevron.open { transform: rotate(135deg); }
|
||||
|
||||
.chevron.open {
|
||||
transform: rotate(135deg);
|
||||
}
|
||||
|
||||
.asset-grid {
|
||||
padding: 0 24rpx 24rpx;
|
||||
@ -304,6 +423,7 @@ function previewCharge() {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.grid-item {
|
||||
background: #fafbff;
|
||||
border-radius: 16rpx;
|
||||
@ -314,8 +434,18 @@ function previewCharge() {
|
||||
padding: 24rpx 12rpx;
|
||||
gap: 10rpx;
|
||||
}
|
||||
.grid-icon { width: 64rpx; height: 64rpx; }
|
||||
.grid-text { font-size: 24rpx; color: #444; text-align: center; line-height: 1.4; }
|
||||
|
||||
.grid-icon {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
}
|
||||
|
||||
.grid-text {
|
||||
font-size: 24rpx;
|
||||
color: #444;
|
||||
text-align: center;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
padding: 0 32rpx 12rpx;
|
||||
@ -328,6 +458,16 @@ function previewCharge() {
|
||||
padding: 20rpx;
|
||||
text-align: center;
|
||||
}
|
||||
.notice-img { width: 100%; border-radius: 12rpx; }
|
||||
.notice-tip { display: block; color: #9a9aa0; font-size: 22rpx; margin-top: 12rpx; }
|
||||
|
||||
.notice-img {
|
||||
width: 100%;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.notice-tip {
|
||||
display: block;
|
||||
color: #9a9aa0;
|
||||
font-size: 22rpx;
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -88,7 +88,7 @@ async function onLogin() {
|
||||
} else {
|
||||
uni.showToast({ title: res?.data?.msg || '登录失败', icon: 'error', duration: 3000 })
|
||||
}
|
||||
} catch (err:any) {
|
||||
} catch (err: any) {
|
||||
// util.request 已有统一错误提示
|
||||
console.warn('login error', err)
|
||||
}
|
||||
@ -105,24 +105,72 @@ function onCancel() {
|
||||
background: #f5f7fb;
|
||||
padding: 24rpx 0 40rpx;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
margin: 0 32rpx 24rpx;
|
||||
box-shadow: 0 8rpx 24rpx rgba(0,0,0,0.06);
|
||||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
.header { padding: 28rpx 24rpx; }
|
||||
.title { font-size: 36rpx; color: #111; font-weight: 600; }
|
||||
.sub { display: block; margin-top: 8rpx; font-size: 24rpx; color: #888; }
|
||||
|
||||
.form { padding: 20rpx 24rpx; }
|
||||
.field { margin-bottom: 20rpx; }
|
||||
.label { display: block; color: #666; font-size: 26rpx; margin-bottom: 10rpx; }
|
||||
.input {
|
||||
width: 100%; height: 80rpx; padding: 0 20rpx;
|
||||
border: 2rpx solid #e6e8ef; border-radius: 12rpx; background: #fcfdff;
|
||||
.header {
|
||||
padding: 28rpx 24rpx;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 36rpx;
|
||||
color: #111;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.sub {
|
||||
display: block;
|
||||
margin-top: 8rpx;
|
||||
font-size: 24rpx;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.form {
|
||||
padding: 20rpx 24rpx;
|
||||
}
|
||||
|
||||
.field {
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: block;
|
||||
color: #666;
|
||||
font-size: 26rpx;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.input {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 80rpx;
|
||||
padding: 0 20rpx;
|
||||
border: 2rpx solid #e6e8ef;
|
||||
border-radius: 12rpx;
|
||||
background: #fcfdff;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.btn {
|
||||
flex: 1;
|
||||
|
||||
border-radius: 14rpx;
|
||||
background: #eef2ff;
|
||||
color: #3a5ddd;
|
||||
}
|
||||
|
||||
.btn.primary {
|
||||
background: #3a5ddd;
|
||||
color: #fff;
|
||||
}
|
||||
.actions { display: flex; gap: 20rpx; margin-top: 8rpx; }
|
||||
.btn { flex: 1; height: 84rpx; border-radius: 14rpx; background: #eef2ff; color: #3a5ddd; }
|
||||
.btn.primary { background: #3a5ddd; color: #fff; }
|
||||
</style>
|
||||
|
||||
@ -1,75 +1,400 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<view class="profile">
|
||||
<scroll-view scroll-y class="page">
|
||||
<!-- 个人信息卡片 -->
|
||||
<view class="profile card">
|
||||
<image class="avatar" src="/static/logo.png" />
|
||||
<view class="info" v-if="!isLoggedIn">
|
||||
<view class="info not-login-in" v-if="!isLoggedIn" @tap="goLogin">
|
||||
<text class="name">未登录</text>
|
||||
<text class="sub" @tap="goLogin">点击登录/注册</text>
|
||||
<text class="sub">点击登录/注册</text>
|
||||
</view>
|
||||
<view class="info" v-else>
|
||||
<text class="name">{{ staff?.name || '已登录' }}</text>
|
||||
<text class="sub">工号:{{ staff?.job_no || '-' }}</text>
|
||||
<text class="name">{{ staff?.user_name || staff?.name || '已登录' }}</text>
|
||||
<text class="sub">类型:{{ userTypeText }}</text>
|
||||
<text class="sub">角色:{{ staff?.role_name || '-' }}</text>
|
||||
<text class="sub" v-if="staff?.job_no">工号:{{ staff?.job_no }}</text>
|
||||
<view class="actions">
|
||||
<button class="btn" @tap="logout">退出登录</button>
|
||||
<button class="btn btn-danger" @tap="logout">退出登录</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 我的任务标题 -->
|
||||
<view class="section-title">
|
||||
<text>我的任务</text>
|
||||
</view>
|
||||
|
||||
<!-- 任务列表 -->
|
||||
<view class="task-list">
|
||||
<view v-if="loading" class="empty">加载中...</view>
|
||||
<view v-else-if="!taskList.length" class="empty">暂无任务</view>
|
||||
<view v-else class="list-wrap">
|
||||
<view class="task card" v-for="(task, idx) in taskList" :key="task.id || idx">
|
||||
<view class="row">
|
||||
<text class="label">申请时间:</text>
|
||||
<text class="value">{{ fmt(task.decTime) }}</text>
|
||||
</view>
|
||||
<view class="row">
|
||||
<text class="label">申请科室:</text>
|
||||
<text class="value">{{ fmt(task.decDeptName) }}</text>
|
||||
</view>
|
||||
<view class="row two-cols">
|
||||
<view class="col">
|
||||
<text class="label">申请床号:</text>
|
||||
<text class="value">{{ fmt(task.decBedNo) }}</text>
|
||||
</view>
|
||||
<view class="col">
|
||||
<text class="label">运送工具:</text>
|
||||
<text class="value">{{ fmt(task.carryingtoolsName) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="row two-cols">
|
||||
<view class="col">
|
||||
<text class="label">是否预约:</text>
|
||||
<text class="value">{{ task.ordered ? '预约' : '立即' }}</text>
|
||||
</view>
|
||||
<view class="col" v-if="task.ordered">
|
||||
<text class="label">预约时间:</text>
|
||||
<text class="value">{{ fmt(task.orderedDatetime) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="row">
|
||||
<text class="label">运单状态:</text>
|
||||
<text class="value">{{ fmt(task.tasksheetStateName) }}</text>
|
||||
</view>
|
||||
<view class="row">
|
||||
<text class="label">接单时间:</text>
|
||||
<text class="value">{{ fmt(task.recieveTime) }}</text>
|
||||
</view>
|
||||
<view class="row">
|
||||
<text class="label">接单电话:</text>
|
||||
<text class="value">{{ fmt(task.undertakePersonTel) }}</text>
|
||||
</view>
|
||||
<view class="row">
|
||||
<text class="label">启运时间:</text>
|
||||
<text class="value">{{ fmt(task.transportTime) }}</text>
|
||||
</view>
|
||||
<view class="row">
|
||||
<text class="label">结运时间:</text>
|
||||
<text class="value">{{ fmt(task.finishTime) }}</text>
|
||||
</view>
|
||||
<view class="row">
|
||||
<text class="label">服务费用:</text>
|
||||
<text class="value">{{ fmt(task.serviceFee) }}</text>
|
||||
</view>
|
||||
<view class="row" v-if="task.tasksheetState === '06'">
|
||||
<text class="label">评价等级:</text>
|
||||
<text class="value">{{ fmt(task.satisfactionEvaluationName) }}</text>
|
||||
</view>
|
||||
<view class="actions-inline">
|
||||
<button class="btn btn-text red" v-if="task.tasksheetState === '03' && task.undertakePersonTel" @tap="onPhoneCall(idx)">电话</button>
|
||||
<button class="btn btn-text blue" v-if="task.tasksheetState === '05'" @tap="onEvaluate(idx)">评价</button>
|
||||
<button class="btn btn-text yellow" @tap="dispNewDetail(idx)">详情</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 详情弹层 -->
|
||||
<view v-if="isDispDetail" class="modal">
|
||||
<view class="modal-content">
|
||||
<view class="modal-header">
|
||||
<text class="modal-title">当前运单信息</text>
|
||||
<text class="modal-close" @tap="isDispDetail = false">×</text>
|
||||
</view>
|
||||
<scroll-view scroll-y class="modal-body">
|
||||
<view class="row"><text class="label">申请时间:</text><text class="value">{{ fmt(currentTask?.decTime) }}</text></view>
|
||||
<view class="row"><text class="label">申请科室:</text><text class="value">{{ fmt(currentTask?.decDeptName) }}</text></view>
|
||||
<view class="row"><text class="label">申请床号:</text><text class="value">{{ fmt(currentTask?.decBedNo) }}</text></view>
|
||||
<view class="row"><text class="label">申请电话:</text><text class="value">{{ fmt(currentTask?.decTel) }}</text></view>
|
||||
<view class="row"><text class="label">运输工具:</text><text class="value">{{ fmt(currentTask?.carryingtoolsName) }}</text></view>
|
||||
<view class="row"><text class="label">工具数量:</text><text class="value">{{ fmt(currentTask?.carryingtoolsCount) }}</text></view>
|
||||
<view class="row"><text class="label">是否预约:</text><text class="value">{{ currentTask?.ordered ? '预约' : '立即' }}</text></view>
|
||||
<view class="row" v-if="currentTask?.ordered"><text class="label">预约时间:</text><text class="value">{{ fmt(currentTask?.orderedDatetime) }}</text></view>
|
||||
<view class="row"><text class="label">支付方式:</text><text class="value">{{ fmt(currentTask?.paymentMethodName) }}</text></view>
|
||||
<view class="row"><text class="label">代申请部门:</text><text class="value">{{ fmt(currentTask?.needcontactDeptName) }}</text></view>
|
||||
<view class="row"><text class="label">代申请电话:</text><text class="value">{{ fmt(currentTask?.needcontactTel) }}</text></view>
|
||||
<view class="row"><text class="label">运单状态:</text><text class="value">{{ fmt(currentTask?.tasksheetStateName) }}</text></view>
|
||||
<view class="row"><text class="label">接单骑手:</text><text class="value">{{ fmt(currentTask?.undertakePersonName) }}</text></view>
|
||||
<view class="row"><text class="label">骑手电话:</text><text class="value">{{ fmt(currentTask?.undertakePersonTel) }}</text></view>
|
||||
<view class="row"><text class="label">接单时间:</text><text class="value">{{ fmt(currentTask?.recieveTime) }}</text></view>
|
||||
<view class="row"><text class="label">启运时间:</text><text class="value">{{ fmt(currentTask?.transportTime) }}</text></view>
|
||||
<view class="row"><text class="label">结运时间:</text><text class="value">{{ fmt(currentTask?.finishTime) }}</text></view>
|
||||
<view class="row"><text class="label">服务费用:</text><text class="value">{{ fmt(currentTask?.serviceFee) }}</text></view>
|
||||
<view class="row" v-if="currentTask?.tasksheetState === '06'"><text class="label">评价等级:</text><text class="value">{{ fmt(currentTask?.satisfactionEvaluationName) }}</text></view>
|
||||
<view class="row" v-if="currentTask?.tasksheetState === '06'"><text class="label">评价意见:</text><text class="value">{{ fmt(currentTask?.satisfactionContent) }}</text></view>
|
||||
</scroll-view>
|
||||
<view class="modal-footer">
|
||||
<button class="btn" @tap="isDispDetail = false">关闭</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 评价弹层 -->
|
||||
<view v-if="isEvaluate" class="modal">
|
||||
<view class="modal-content">
|
||||
<view class="modal-header">
|
||||
<text class="modal-title">请对担架运送服务进行评价</text>
|
||||
<text class="modal-close" @tap="cancelEvaluate">×</text>
|
||||
</view>
|
||||
<view class="modal-body">
|
||||
<view class="form-row">
|
||||
<text class="label">服务评价:</text>
|
||||
<view class="radio-group">
|
||||
<label v-for="item in satisfactionEvaluationDatas" :key="item.value" class="radio-item">
|
||||
<radio :value="item.value" :checked="evaluateRequest.satisfactionEvaluation === item.value" @tap="evaluateRequest.satisfactionEvaluation = item.value" />
|
||||
<text>{{ item.label }}</text>
|
||||
</label>
|
||||
</view>
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<text class="label">意见:</text>
|
||||
<textarea class="textarea" maxlength="200" placeholder="请输入评价信息" v-model="evaluateRequest.satisfactionContent" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="modal-footer">
|
||||
<button class="btn btn-primary" @tap="submitEvaluate">评价</button>
|
||||
<button class="btn" @tap="cancelEvaluate">取消</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { onShow } from '@dcloudio/uni-app'
|
||||
// @ts-ignore JS模块无类型声明
|
||||
import queryService from '@/service/queryService.js'
|
||||
// @ts-ignore JS模块无类型声明
|
||||
import getService from '@/service/getService.js'
|
||||
// @ts-ignore JS模块无类型声明
|
||||
import config from '@/config.js'
|
||||
|
||||
type Task = Record<string, any>
|
||||
|
||||
const isLoggedIn = ref(false)
|
||||
const staff = ref<any | null>(null)
|
||||
const isWeixin = ref(false)
|
||||
const taskList = ref<Task[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
const isDispDetail = ref(false)
|
||||
const currentTask = ref<Task | null>(null)
|
||||
|
||||
const isEvaluate = ref(false)
|
||||
const evaluateRequest = ref<{ id: string | number | null; satisfactionContent: string | null; satisfactionEvaluation: string }>({
|
||||
id: null,
|
||||
satisfactionContent: null,
|
||||
satisfactionEvaluation: '04'
|
||||
})
|
||||
const satisfactionEvaluationDatas = [
|
||||
{ value: '03', label: '不满意' },
|
||||
{ value: '04', label: '满意' },
|
||||
{ value: '05', label: '非常满意' }
|
||||
]
|
||||
|
||||
const userTypeText = computed(() => {
|
||||
const t = staff.value?.user_type
|
||||
if (!t) return '非注册用户'
|
||||
if (t === 1) return '系统用户'
|
||||
if (t === 2) return '医院职工'
|
||||
if (t === 3) return '物业员工'
|
||||
return '非注册用户'
|
||||
})
|
||||
|
||||
function fmt(v: any) {
|
||||
return v === null || v === undefined || v === '' ? '' : String(v)
|
||||
}
|
||||
|
||||
function goLogin() {
|
||||
uni.navigateTo({ url: '/pages/login/index' })
|
||||
}
|
||||
|
||||
function logout() {
|
||||
uni.setStorageSync('staff', null)
|
||||
uni.setStorageSync('token', null)
|
||||
staff.value = null
|
||||
isLoggedIn.value = false
|
||||
taskList.value = []
|
||||
uni.showToast({ title: '已退出登录', icon: 'none' })
|
||||
}
|
||||
|
||||
async function initEnv() {
|
||||
// 平台标识
|
||||
// @ts-ignore
|
||||
isWeixin.value = process?.env?.VUE_APP_PLATFORM === 'mp-weixin'
|
||||
if (isWeixin.value) {
|
||||
// 确保 openid
|
||||
const openid = uni.getStorageSync('openid')
|
||||
if (!openid) {
|
||||
try { await getService.getOpenId?.() } catch {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTasks() {
|
||||
loading.value = true
|
||||
try {
|
||||
const staffId = staff.value?.id
|
||||
let openid: string | null = null
|
||||
if (!staffId) {
|
||||
openid = uni.getStorageSync('openid') || (config?.defaultOpenId || null)
|
||||
}
|
||||
const data = {
|
||||
decTel: null,
|
||||
needcontactTel: null,
|
||||
openid: staffId ? staffId : openid,
|
||||
decendDatetime: null,
|
||||
decstartDatetime: null,
|
||||
index: 0,
|
||||
page: 0,
|
||||
size: 0,
|
||||
sort: null,
|
||||
tasksheetStates: null
|
||||
}
|
||||
const res = await queryService.queryStretcherTask(data)
|
||||
if (res?.data?.code === 0) {
|
||||
taskList.value = res.data.data || []
|
||||
} else {
|
||||
taskList.value = []
|
||||
}
|
||||
} catch (e) {
|
||||
taskList.value = []
|
||||
console.warn('加载任务失败:', e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function dispNewDetail(index: number) {
|
||||
currentTask.value = taskList.value[index] || null
|
||||
isDispDetail.value = true
|
||||
}
|
||||
|
||||
function onPhoneCall(index: number) {
|
||||
const t = taskList.value[index]
|
||||
const tel = t?.undertakePersonTel
|
||||
if (!tel) {
|
||||
return uni.showToast({ title: '暂无联系电话', icon: 'none' })
|
||||
}
|
||||
uni.makePhoneCall({
|
||||
phoneNumber: tel,
|
||||
success() { console.log('拨打成功') },
|
||||
fail() { uni.showToast({ title: '拨打失败', icon: 'error', duration: 3000 }) }
|
||||
})
|
||||
}
|
||||
|
||||
function onEvaluate(index: number) {
|
||||
currentTask.value = taskList.value[index] || null
|
||||
if (!currentTask.value) return
|
||||
evaluateRequest.value.id = currentTask.value.id || null
|
||||
evaluateRequest.value.satisfactionContent = null
|
||||
evaluateRequest.value.satisfactionEvaluation = '04'
|
||||
isEvaluate.value = true
|
||||
}
|
||||
|
||||
async function submitEvaluate() {
|
||||
if (!currentTask.value || !evaluateRequest.value.id) {
|
||||
return uni.showToast({ title: '无法提交评价', icon: 'none' })
|
||||
}
|
||||
try {
|
||||
const res = await queryService.evaluateStretcherTransport(evaluateRequest.value)
|
||||
if (res?.data?.code === 0) {
|
||||
uni.showToast({ title: '评价提交成功', icon: 'none' })
|
||||
isEvaluate.value = false
|
||||
// 重置
|
||||
evaluateRequest.value = { id: null, satisfactionContent: null, satisfactionEvaluation: '04' }
|
||||
// 刷新列表
|
||||
await loadTasks()
|
||||
} else {
|
||||
uni.showToast({ title: '评价提交失败', icon: 'error', duration: 3000 })
|
||||
}
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '评价提交失败', icon: 'error', duration: 3000 })
|
||||
}
|
||||
}
|
||||
|
||||
function cancelEvaluate() {
|
||||
isEvaluate.value = false
|
||||
evaluateRequest.value = { id: null, satisfactionContent: null, satisfactionEvaluation: '04' }
|
||||
}
|
||||
|
||||
function sync() {
|
||||
staff.value = uni.getStorageSync('staff') || null
|
||||
isLoggedIn.value = !!staff.value
|
||||
}
|
||||
|
||||
function goLogin() {
|
||||
uni.navigateTo({ url: '/pages/login/index' })
|
||||
}
|
||||
function logout() {
|
||||
uni.setStorageSync('staff', null)
|
||||
uni.setStorageSync('token', null)
|
||||
onMounted(async () => {
|
||||
sync()
|
||||
uni.showToast({ title: '已退出登录', icon: 'none' })
|
||||
}
|
||||
await initEnv()
|
||||
await loadTasks()
|
||||
})
|
||||
|
||||
onMounted(sync)
|
||||
onShow(async () => {
|
||||
sync()
|
||||
await loadTasks()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #f7f8fa;
|
||||
background: #f5f7fb;
|
||||
}
|
||||
.card {
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
box-shadow: 0 8rpx 24rpx rgba(0,0,0,0.06);
|
||||
}
|
||||
.profile {
|
||||
margin: 32rpx;
|
||||
padding: 32rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
padding: 40rpx 32rpx;
|
||||
background: #fff;
|
||||
}
|
||||
.avatar {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.name {
|
||||
font-size: 32rpx;
|
||||
color: #333;
|
||||
}
|
||||
.sub {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
.info { display: flex; flex-direction: column; }
|
||||
.name { font-size: 32rpx; color: #333; }
|
||||
.sub { font-size: 24rpx; color: #999; }
|
||||
.actions { margin-top: 16rpx; }
|
||||
.btn { padding: 12rpx 20rpx; background: #ef5350; color: #fff; border-radius: 10rpx; font-size: 24rpx; }
|
||||
.btn { padding: 12rpx 20rpx; background: #3a5ddd; color: #fff; border-radius: 10rpx; font-size: 24rpx; }
|
||||
.btn-danger { background: #ef5350; }
|
||||
.btn-primary { background: #3a5ddd; }
|
||||
.btn-text { background: transparent; color: #3a5ddd; padding: 6rpx 10rpx; }
|
||||
.btn-text.red { color: #ef5350; }
|
||||
.btn-text.blue { color: #3a5ddd; }
|
||||
.btn-text.yellow { color: #f39c12; }
|
||||
|
||||
.section-title { padding: 0 32rpx 12rpx; color: #666; font-size: 26rpx; }
|
||||
.task-list { padding: 0 32rpx 32rpx; }
|
||||
.empty { text-align: center; color: #9a9aa0; padding: 40rpx 0; }
|
||||
.list-wrap { display: flex; flex-direction: column; gap: 20rpx; }
|
||||
.task { padding: 20rpx; }
|
||||
.row { display: flex; align-items: center; padding: 8rpx 0; flex-wrap: wrap; }
|
||||
.two-cols { justify-content: space-between; }
|
||||
.two-cols .col { width: 48%; display: flex; }
|
||||
.label { color: #666; font-size: 24rpx; }
|
||||
.value { color: #333; font-size: 26rpx; }
|
||||
.actions-inline { display: flex; justify-content: flex-end; gap: 16rpx; padding-top: 8rpx; }
|
||||
.actions-inline .btn{ flex: 1;}
|
||||
|
||||
/* 简易弹层样式 */
|
||||
.modal { position: fixed; left: 0; top: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.45); display: flex; align-items: center; justify-content: center; z-index: 999; }
|
||||
.modal-content { width: 86%; max-height: 76vh; background: #fff; border-radius: 16rpx; overflow: hidden; display: flex; flex-direction: column; }
|
||||
.modal-header { padding: 20rpx 24rpx; display: flex; align-items: center; justify-content: space-between; border-bottom: 1rpx solid #eee; }
|
||||
.modal-title { font-size: 30rpx; color: #222; font-weight: 600; }
|
||||
.modal-close { font-size: 40rpx; color: #999; }
|
||||
.modal-body { padding: 16rpx 24rpx; max-height: 60vh; }
|
||||
.modal-footer { padding: 16rpx 24rpx; display: flex; justify-content: flex-end; gap: 16rpx; border-top: 1rpx solid #eee; }
|
||||
.modal-footer .btn { flex: 1; }
|
||||
.form-row { margin-bottom: 20rpx; }
|
||||
.radio-group { display: flex; gap: 24rpx; flex-wrap: wrap; }
|
||||
.radio-item { display: flex; align-items: center; gap: 8rpx; }
|
||||
.textarea { width: 100%; min-height: 160rpx; border: 1rpx solid #e5e6eb; border-radius: 12rpx; padding: 12rpx; font-size: 26rpx; }
|
||||
</style>
|
||||
|
||||
BIN
src/static/icons/home-selected.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
src/static/icons/messages-selected.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
src/static/icons/profile-selected.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
src/static/icons/tabbar/home-selected.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
src/static/icons/tabbar/home.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
src/static/icons/tabbar/messages-selected.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
src/static/icons/tabbar/messages.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
src/static/icons/tabbar/profile-selected.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
src/static/icons/tabbar/profile.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
src/static/icons/tabbar/tasks-selected.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
src/static/icons/tabbar/tasks.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
src/static/icons/tasks-selected.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
244
src/uni_modules/jz-h5-scanCode/CHANGELOG.md
Normal file
@ -0,0 +1,244 @@
|
||||
## 1.0.5(2025-07-12)
|
||||
新增 静态销毁方法
|
||||
// 销毁插件
|
||||
jzH5ScanCode.destroy();
|
||||
|
||||
用于页面左滑返回 监听不了返回的情况
|
||||
使用方法
|
||||
import {
|
||||
onLoad,
|
||||
onBackPress
|
||||
} from "@dcloudio/uni-app";
|
||||
|
||||
onBackPress(() => {
|
||||
jzH5ScanCode.destroy();
|
||||
})
|
||||
|
||||
## 1.0.4(2025-07-09)
|
||||
--修复 在pc 网页中 canvas 画面变形 导致不能识别的问题,采用局部中心canvas 解决
|
||||
--移除大量不必要的打印日志
|
||||
## 1.0.3(2025-07-08)
|
||||
修复部分手机canvas 初始化 尺寸错位问题
|
||||
## 1.0.2(2025-07-07)
|
||||
---优化初始进入 显示局部canvas
|
||||
---修复选择图片/拍照 所用的图片没有二维码,无法失败回调的问题
|
||||
## 1.0.1(2025-06-27)
|
||||
修复用户选择相册-> 拍照后导致卡死的问题
|
||||
优化性能,减少卡死概率
|
||||
# 更新日志
|
||||
|
||||
## 1.1.9(2024-12-19)
|
||||
### 🔧 拍照扫码体验优化 - 完美解决警告弹窗和界面卡死问题
|
||||
|
||||
#### 🐛 关键问题修复
|
||||
- **消除重复弹窗**:修复拍照成功后仍然出现警告弹窗的问题
|
||||
- **自动隐藏界面**:拍照完成后自动隐藏扫描界面,避免界面卡死
|
||||
- **统一错误处理**:重构错误处理逻辑,避免多重弹窗冲突
|
||||
- **处理流程优化**:统一图片处理路径,消除重复处理导致的异常
|
||||
|
||||
#### 🎯 用户体验提升
|
||||
- **无干扰扫码**:成功识别后不再显示多余的错误提示
|
||||
- **智能界面管理**:成功1秒后隐藏界面,失败1.5秒后隐藏
|
||||
- **静默失败处理**:失败时只在控制台记录,不干扰用户
|
||||
- **流畅交互体验**:消除了拍照->识别->弹窗->卡死的不良体验
|
||||
|
||||
#### 🛠️ 技术改进
|
||||
- **处理逻辑重构**:`handleUniImageSelected`现在统一调用`processUniSelectedImage`
|
||||
- **新增统一方法**:`hideScannerUIAfterImageProcess`统一管理界面隐藏
|
||||
- **错误处理优化**:避免`handleScanError`和`showErrorMessage`的重复调用
|
||||
- **资源清理增强**:确保界面隐藏时完整清理所有状态
|
||||
|
||||
#### 📊 修复效果
|
||||
- **警告弹窗消除**:100%解决拍照成功后的警告弹窗问题
|
||||
- **界面卡死修复**:完全避免拍照后的界面卡死问题
|
||||
- **用户流程优化**:拍照->识别->自动关闭的完整体验
|
||||
- **错误提示减少**:非必要错误提示减少90%
|
||||
|
||||
## 1.1.8(2024-12-19)
|
||||
### 🔧 图片加载修复 - 彻底解决选择图片后的错误弹窗
|
||||
|
||||
#### 🐛 关键问题修复
|
||||
- **图片加载失败修复**:修复选择图片后显示"图片加载失败"的问题
|
||||
- **跨域问题解决**:修复uniapp临时文件路径的跨域访问问题
|
||||
- **重复弹窗修复**:解决同一错误导致多个弹窗的问题
|
||||
- **路径兼容性**:智能识别不同类型的图片路径格式
|
||||
|
||||
#### 🛠️ 技术改进
|
||||
- **智能路径处理**:自动检测uniapp临时文件、网络图片等不同路径类型
|
||||
- **错误传播优化**:避免多层错误处理导致的重复提示
|
||||
- **降级机制完善**:Worker处理失败时的更好恢复能力
|
||||
- **异常捕获增强**:更精确的错误分类和处理
|
||||
|
||||
#### 💡 处理策略
|
||||
- **临时文件检测**:识别`tmp_`、`temp`、`wxfile://`等uniapp路径
|
||||
- **网络图片处理**:自动为http/https图片设置跨域模式
|
||||
- **错误去重机制**:避免相同错误的多次弹窗提示
|
||||
- **资源清理优化**:确保处理失败时的完整资源释放
|
||||
|
||||
#### 📊 修复效果
|
||||
- **图片加载成功率**:从60%提升到95%
|
||||
- **错误弹窗减少**:从平均2-3个减少到1个
|
||||
- **用户体验改善**:消除了选择图片后的立即报错问题
|
||||
- **兼容性提升**:支持更多图片路径格式
|
||||
|
||||
## 1.1.7(2024-12-19)
|
||||
### 🔧 编译修复 - Web Worker 优化
|
||||
|
||||
#### 🐛 问题修复
|
||||
- **编译错误修复**:修复WorkerManager导出格式不兼容的问题
|
||||
- **模块导入优化**:统一使用ES6 export default导出格式
|
||||
- **兼容性优化**:暂时禁用OffscreenCanvas依赖,确保浏览器兼容性
|
||||
|
||||
#### ⚡ 性能优化
|
||||
- **主线程优化**:改进图片压缩的主线程处理逻辑
|
||||
- **UI响应性提升**:使用requestAnimationFrame确保UI更新流畅
|
||||
- **进度显示优化**:实时显示压缩进度,避免假死感觉
|
||||
|
||||
#### 🎨 用户体验改进
|
||||
- **进度条动画**:绿色渐变进度条,视觉效果更佳
|
||||
- **成功提示动画**:添加识别成功的动画反馈
|
||||
- **处理状态提示**:实时显示"正在分析图片..."等状态信息
|
||||
- **防立即失败**:添加缓冲时间,避免拍照后立即报错
|
||||
|
||||
#### 🔧 技术改进
|
||||
- **Worker降级策略**:不支持Worker时自动使用主线程处理
|
||||
- **错误处理优化**:更完善的异常捕获和用户友好提示
|
||||
- **资源管理优化**:改进Worker生命周期管理
|
||||
- **模块化重构**:更清晰的模块职责划分
|
||||
|
||||
#### 📊 编译兼容性
|
||||
- ✅ **HBuilder X**:完美支持项目编译打包
|
||||
- ✅ **Webpack**:兼容现代打包工具
|
||||
- ✅ **Vite**:支持快速开发构建
|
||||
- ✅ **uni-app**:完整的uni-app生态兼容
|
||||
|
||||
---
|
||||
|
||||
## 1.1.6(2024-12-28)
|
||||
### 🚀 重大优化 - 彻底解决拍照卡死问题
|
||||
|
||||
#### 🎯 核心问题解决
|
||||
- **极限压缩模式**:针对超大图片(>4MP)启用极限压缩,避免拍照后卡死
|
||||
- **分片处理技术**:大图片分块处理,每200px一个区块,避免一次性处理导致卡死
|
||||
- **智能尺寸控制**:最大尺寸从1920px降低到800px,超大图片压缩到400px正方形
|
||||
|
||||
#### ⚡ 性能突破
|
||||
- **图片处理速度**:分片处理提升90%处理速度
|
||||
- **内存占用优化**:极限压缩减少95%内存使用
|
||||
- **解码效率提升**:简化预处理流程,去除复杂算法
|
||||
- **CPU占用降低**:快速渲染模式,关闭抗锯齿优化
|
||||
|
||||
#### 🔧 技术升级
|
||||
- 新增`extremeCompressForScan()`极限压缩方法
|
||||
- 实现`drawImageInChunks()`分片绘制技术
|
||||
- 添加多重超时机制:图片处理6秒、解码操作8秒
|
||||
- 强制资源清理:超时或错误时立即释放内存
|
||||
|
||||
#### 🎨 用户体验改进
|
||||
- 添加处理进度提示UI:"正在优化图片..."
|
||||
- 智能压缩策略:自动识别图片大小选择压缩模式
|
||||
- 友好的错误提示:超时提示更明确
|
||||
- 无感知处理:后台自动优化,用户无需干预
|
||||
|
||||
#### 📊 性能数据
|
||||
- **图片压缩比**:从2MB→50KB(96%压缩率)
|
||||
- **处理时间**:从15秒→2秒(87%提升)
|
||||
- **内存峰值**:从200MB→10MB(95%降低)
|
||||
- **卡死率**:从80%→0%(完全解决)
|
||||
|
||||
#### 🛠️ 技术细节
|
||||
- 超大图片检测:`originalSize > 4000000`像素
|
||||
- 分片大小:200px区块,10ms间隔处理
|
||||
- 压缩目标:400px正方形,0.6质量
|
||||
- 超时控制:6层超时保护机制
|
||||
|
||||
## 1.0.0(2025-06-17)
|
||||
### 🎉 首次发布
|
||||
|
||||
#### ✨ 核心功能
|
||||
- **完全兼容uni.scanCode**:提供与官方API完全一致的接口
|
||||
- **摄像头实时扫码**:支持二维码、条形码实时识别
|
||||
- **图片扫码**:支持从相册选择图片进行扫码
|
||||
- **多种码制支持**:二维码(QR Code)、条形码(EAN、Code128等)
|
||||
|
||||
#### 🎨 用户界面
|
||||
- **微信风格UI**:仿微信扫一扫界面设计
|
||||
- **扫描动画**:流畅的扫描线动画效果
|
||||
- **响应式布局**:适配不同屏幕尺寸
|
||||
- **简洁操作**:一键扫码,操作简单
|
||||
|
||||
#### ⚡ 性能优化
|
||||
- **高效扫码引擎**:集成jsQR和Quagga.js双引擎
|
||||
- **智能降级**:优先使用原生BarcodeDetector API
|
||||
- **内存优化**:及时清理摄像头和解码资源
|
||||
- **CPU优化**:控制扫码频率,减少CPU占用
|
||||
|
||||
#### 📱 平台兼容性
|
||||
- ✅ **H5浏览器**:Chrome、Safari、Firefox、Edge
|
||||
- ✅ **移动端H5**:iOS Safari、Android Chrome
|
||||
- ✅ **微信浏览器**:支持微信内置浏览器
|
||||
- ✅ **App端**:通过webview组件支持
|
||||
- ❌ **小程序端**:不支持(技术限制)
|
||||
|
||||
#### 🔧 技术特性
|
||||
- **模块化设计**:5个核心模块分工明确
|
||||
- **TypeScript支持**:完整的类型定义
|
||||
- **错误处理**:完善的异常捕获和用户提示
|
||||
- **调试模式**:开发环境下的详细日志
|
||||
|
||||
#### 📦 核心模块
|
||||
- **index.js**:主入口和API管理
|
||||
- **cameraScanner.js**:摄像头访问和管理
|
||||
- **qrCodeDecoder.js**:二维码识别引擎
|
||||
- **uiManager.js**:用户界面管理
|
||||
- **utils.js**:工具函数和调试
|
||||
|
||||
#### 🎯 典型应用场景
|
||||
- 商品条码扫描购物应用
|
||||
- 二维码支付扫码功能
|
||||
- 会议签到扫码应用
|
||||
- 任何需要H5扫码的场景
|
||||
|
||||
#### 🔧 主要API
|
||||
```javascript
|
||||
// 基础扫码
|
||||
uni.scanCode({
|
||||
success: (res) => {
|
||||
console.log('扫码结果:', res.result);
|
||||
}
|
||||
});
|
||||
|
||||
// 高级配置
|
||||
uni.scanCode({
|
||||
scanType: ['qrCode', 'barCode'],
|
||||
autoDecodeCharSet: true,
|
||||
success: (res) => {
|
||||
console.log('码制:', res.scanType);
|
||||
console.log('结果:', res.result);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
#### 🚀 性能提升
|
||||
- **CPU占用降低**:扫码间隔优化,降低约60%CPU占用
|
||||
- **内存优化**:资源清理优化,降低约30%内存占用
|
||||
- **响应速度**:UI简化和异步优化,提升约40%响应速度
|
||||
- **稳定性提升**:修复卡死问题,显著提升稳定性
|
||||
|
||||
---
|
||||
|
||||
### 📈 版本统计
|
||||
- **总代码行数**:约2000行
|
||||
- **核心模块数**:5个专业模块
|
||||
- **支持码制数**:10+种常见码制
|
||||
- **兼容浏览器数**:主流浏览器全覆盖
|
||||
|
||||
### 🚀 后续计划
|
||||
- [ ] 支持更多条码格式
|
||||
- [ ] 添加扫码历史记录
|
||||
- [ ] 优化低光环境识别
|
||||
- [ ] 支持批量扫码模式
|
||||
|
||||
---
|
||||
|
||||
> **注意**:本插件基于MIT协议开源,专为H5环境设计,提供最佳的扫码体验!
|
||||
364
src/uni_modules/jz-h5-scanCode/README.md
Normal file
@ -0,0 +1,364 @@
|
||||
# jz-h5-scanCode
|
||||
|
||||
## 🔍 插件简介
|
||||
|
||||
jz-h5-scanCode 是一个专为 H5 环境设计的二维码扫描插件,提供与 `uni.scanCode` 完全兼容的 API 接口。插件采用模块化架构,支持多种识别引擎,提供微信风格的扫码界面。
|
||||
|
||||
## ✨ 核心特性
|
||||
|
||||
### 🎯 API 兼容性
|
||||
- 与 `uni.scanCode` API 完全兼容
|
||||
- 支持相同的参数配置和回调函数
|
||||
- 平台自动判断,H5 使用自定义实现,其他平台使用原生 API
|
||||
|
||||
### 📱 用户界面
|
||||
- **微信风格设计**:仿微信扫码界面,用户体验一致
|
||||
- **全屏沉浸式**:黑色背景,突出扫码区域
|
||||
- **动态扫描线**:绿色扫描线上下移动,提供视觉反馈
|
||||
- **自定义配置**:支持 8 种扫描框颜色选择
|
||||
|
||||
### 🔧 技术架构
|
||||
- **模块化设计**:核心功能分离,便于维护和扩展
|
||||
- **多重识别引擎**:BarcodeDetector API + jsQR 库双重保障
|
||||
- **智能降级**:摄像头不可用时自动切换到图片选择模式
|
||||
- **图像预处理**:多种算法提高二维码识别成功率
|
||||
|
||||
## 📦 安装使用
|
||||
|
||||
### 1. uni-app 项目中使用
|
||||
|
||||
```javascript
|
||||
// 导入插件
|
||||
import jzH5ScanCode from '@/uni_modules/jz-h5-scanCode/js/index.js'
|
||||
|
||||
// 基础扫码
|
||||
jzH5ScanCode.scanCode({
|
||||
success: (res) => {
|
||||
console.log('扫码成功:', res.result)
|
||||
},
|
||||
fail: (res) => {
|
||||
console.log('扫码失败:', res.errMsg)
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### 2. 普通 H5 项目中使用
|
||||
|
||||
```javascript
|
||||
// ES6 模块导入
|
||||
import jzH5ScanCode from './uni_modules/jz-h5-scanCode/js/index.js'
|
||||
|
||||
// 或者直接引入
|
||||
<script type="module">
|
||||
import jzH5ScanCode from './uni_modules/jz-h5-scanCode/js/index.js'
|
||||
// 使用插件
|
||||
</script>
|
||||
```
|
||||
|
||||
## 🔧 API 文档
|
||||
|
||||
### scanCode(options)
|
||||
|
||||
主要扫码方法,与 uni.scanCode API 完全兼容。
|
||||
|
||||
#### 参数说明
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|
||||
|--------|------|------|--------|------|
|
||||
| success | Function | 否 | - | 扫码成功回调函数 |
|
||||
| fail | Function | 否 | - | 扫码失败回调函数 |
|
||||
| complete | Function | 否 | - | 扫码完成回调函数 |
|
||||
| scanType | Array | 否 | ['qrCode'] | 扫码类型,目前仅支持二维码 |
|
||||
| onlyFromCamera | Boolean | 否 | false | 是否仅允许从相机扫码 |
|
||||
| scanFrameColor | String | 否 | '#00ff00' | 扫描框颜色 |
|
||||
|
||||
#### 回调参数
|
||||
|
||||
**success 回调参数:**
|
||||
|
||||
| 参数名 | 类型 | 说明 |
|
||||
|--------|------|------|
|
||||
| result | String | 扫码内容 |
|
||||
| scanType | String | 扫码类型 |
|
||||
| charSet | String | 字符集 |
|
||||
| imageChannel | String | 图像来源(camera/album) |
|
||||
| errMsg | String | 错误信息 |
|
||||
|
||||
**fail 回调参数:**
|
||||
|
||||
| 参数名 | 类型 | 说明 |
|
||||
|--------|------|------|
|
||||
| errMsg | String | 错误信息 |
|
||||
|
||||
#### 使用示例
|
||||
|
||||
```javascript
|
||||
// 基础扫码
|
||||
jzH5ScanCode.scanCode({
|
||||
success: (res) => {
|
||||
console.log('扫码结果:', res.result)
|
||||
console.log('扫码类型:', res.scanType)
|
||||
console.log('图像来源:', res.imageChannel)
|
||||
},
|
||||
fail: (res) => {
|
||||
console.log('扫码失败:', res.errMsg)
|
||||
},
|
||||
complete: () => {
|
||||
console.log('扫码完成')
|
||||
}
|
||||
})
|
||||
|
||||
// 高级配置
|
||||
jzH5ScanCode.scanCode({
|
||||
scanType: ['qrCode'],
|
||||
onlyFromCamera: true,
|
||||
scanFrameColor: '#007aff',
|
||||
success: (res) => {
|
||||
// 处理扫码结果
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### 静态方法
|
||||
|
||||
#### isCameraSupported()
|
||||
|
||||
检查设备是否支持摄像头。
|
||||
|
||||
```javascript
|
||||
const supported = await jzH5ScanCode.isCameraSupported()
|
||||
console.log('摄像头支持:', supported)
|
||||
```
|
||||
|
||||
#### isBarcodeDetectorSupported()
|
||||
|
||||
检查浏览器是否支持 BarcodeDetector API。
|
||||
|
||||
```javascript
|
||||
const supported = jzH5ScanCode.isBarcodeDetectorSupported()
|
||||
console.log('BarcodeDetector 支持:', supported)
|
||||
```
|
||||
|
||||
#### getSupportedFormats()
|
||||
|
||||
获取支持的条码格式。
|
||||
|
||||
```javascript
|
||||
const formats = await jzH5ScanCode.getSupportedFormats()
|
||||
console.log('支持的格式:', formats)
|
||||
```
|
||||
|
||||
#### getPluginInfo()
|
||||
|
||||
获取插件信息。
|
||||
|
||||
```javascript
|
||||
const info = jzH5ScanCode.getPluginInfo()
|
||||
console.log('插件信息:', info)
|
||||
```
|
||||
|
||||
## 🎨 界面配置
|
||||
|
||||
### 扫描框颜色
|
||||
|
||||
插件支持 8 种预设颜色:
|
||||
|
||||
- `#00ff00` - 绿色(默认)
|
||||
- `#007aff` - 蓝色
|
||||
- `#ff3b30` - 红色
|
||||
- `#ff9500` - 橙色
|
||||
- `#ffcc00` - 黄色
|
||||
- `#34c759` - 青绿色
|
||||
- `#af52de` - 紫色
|
||||
- `#ffffff` - 白色
|
||||
|
||||
```javascript
|
||||
jzH5ScanCode.scanCode({
|
||||
scanFrameColor: '#007aff', // 蓝色扫描框
|
||||
success: (res) => {
|
||||
// 处理结果
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## 🏗️ 技术架构
|
||||
|
||||
### 核心模块
|
||||
|
||||
#### 1. index.js - 主入口
|
||||
- API 兼容性处理
|
||||
- 平台判断逻辑
|
||||
- 事件管理和协调
|
||||
|
||||
#### 2. cameraScanner.js - 摄像头管理
|
||||
- 摄像头权限申请
|
||||
- 视频流获取和管理
|
||||
- 图像数据提取
|
||||
|
||||
#### 3. qrCodeDecoder.js - 二维码解码
|
||||
- BarcodeDetector API 集成
|
||||
- jsQR 库集成
|
||||
- 图像预处理算法
|
||||
- 多重识别策略
|
||||
|
||||
#### 4. uiManager.js - 界面管理
|
||||
- 微信风格 UI 创建
|
||||
- 扫描动画效果
|
||||
- 用户交互处理
|
||||
- 响应式布局
|
||||
|
||||
#### 5. utils.js - 工具函数
|
||||
- 调试日志管理
|
||||
- 错误处理
|
||||
- 通用工具方法
|
||||
|
||||
### 识别流程
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[开始扫码] --> B{检查平台}
|
||||
B -->|H5| C[检查摄像头权限]
|
||||
B -->|其他平台| D[使用 uni.scanCode]
|
||||
C -->|有权限| E[创建扫码界面]
|
||||
C -->|无权限| F[降级到图片选择]
|
||||
E --> G[初始化解码器]
|
||||
F --> G
|
||||
G --> H[开始识别循环]
|
||||
H --> I{识别成功?}
|
||||
I -->|是| J[返回结果]
|
||||
I -->|否| H
|
||||
```
|
||||
|
||||
## 🔍 识别引擎
|
||||
|
||||
### 1. BarcodeDetector API
|
||||
- **优先级**:最高
|
||||
- **支持浏览器**:Chrome 83+, Edge 83+
|
||||
- **特点**:原生实现,性能最佳
|
||||
|
||||
### 2. jsQR 库
|
||||
- **优先级**:备用
|
||||
- **支持浏览器**:所有现代浏览器
|
||||
- **特点**:纯 JavaScript 实现,兼容性好
|
||||
|
||||
### 3. 图像预处理
|
||||
- **灰度转换**:提高识别速度
|
||||
- **对比度增强**:改善图像质量
|
||||
- **二值化处理**:突出二维码特征
|
||||
- **图像缩放**:适配不同尺寸
|
||||
- **图像旋转**:处理倾斜的二维码
|
||||
|
||||
## 📱 浏览器兼容性
|
||||
|
||||
| 功能 | Chrome | Firefox | Safari | Edge | 移动端 |
|
||||
|------|--------|---------|--------|------|--------|
|
||||
| 摄像头扫码 | ✅ 60+ | ✅ 55+ | ✅ 11+ | ✅ 79+ | ✅ |
|
||||
| 图片扫码 | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| BarcodeDetector | ✅ 83+ | ❌ | ❌ | ✅ 83+ | 部分 |
|
||||
| jsQR 识别 | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
|
||||
## 🛠️ 开发指南
|
||||
|
||||
### 本地开发
|
||||
|
||||
1. **环境要求**
|
||||
- 现代浏览器
|
||||
- HTTPS 或 localhost 环境
|
||||
- HTTP 服务器(不能直接打开 HTML 文件)
|
||||
|
||||
2. **调试模式**
|
||||
```javascript
|
||||
// 开启调试日志
|
||||
console.log('调试模式已开启')
|
||||
```
|
||||
|
||||
3. **常见问题**
|
||||
- **摄像头权限**:确保使用 HTTPS 或 localhost
|
||||
- **识别失败**:检查二维码清晰度和光线条件
|
||||
- **界面异常**:检查 CSS 样式冲突
|
||||
|
||||
### 自定义扩展
|
||||
|
||||
```javascript
|
||||
// 扩展识别引擎
|
||||
import { QRCodeDecoder } from './qrCodeDecoder.js'
|
||||
|
||||
class CustomDecoder extends QRCodeDecoder {
|
||||
// 自定义识别逻辑
|
||||
}
|
||||
|
||||
// 扩展 UI 样式
|
||||
import { UIManager } from './uiManager.js'
|
||||
|
||||
class CustomUIManager extends UIManager {
|
||||
// 自定义界面
|
||||
}
|
||||
```
|
||||
|
||||
## 📊 性能优化
|
||||
|
||||
### 识别性能
|
||||
- **多重引擎**:BarcodeDetector + jsQR 双重保障
|
||||
- **图像预处理**:多种算法提高识别率
|
||||
- **智能采样**:减少不必要的计算
|
||||
|
||||
### 内存管理
|
||||
- **及时清理**:自动释放摄像头资源
|
||||
- **事件解绑**:防止内存泄漏
|
||||
- **DOM 清理**:完全移除 UI 元素
|
||||
|
||||
### 用户体验
|
||||
- **快速响应**:扫码成功立即返回
|
||||
- **流畅动画**:60fps 扫描线动画
|
||||
- **错误提示**:友好的错误信息
|
||||
|
||||
## 🔧 故障排除
|
||||
|
||||
### 常见问题
|
||||
|
||||
1. **摄像头无法启动**
|
||||
- 检查是否使用 HTTPS 协议
|
||||
- 确认浏览器摄像头权限
|
||||
- 尝试刷新页面重新授权
|
||||
|
||||
2. **二维码识别失败**
|
||||
- 确保二维码清晰可见
|
||||
- 调整光线条件
|
||||
- 尝试不同角度和距离
|
||||
|
||||
3. **界面显示异常**
|
||||
- 检查 CSS 样式冲突
|
||||
- 确认 z-index 设置
|
||||
- 验证 DOM 结构完整性
|
||||
|
||||
### 调试信息
|
||||
|
||||
```javascript
|
||||
// 获取详细的调试信息
|
||||
const info = jzH5ScanCode.getPluginInfo()
|
||||
console.log('插件版本:', info.version)
|
||||
console.log('浏览器支持:', info.cameraSupported)
|
||||
console.log('识别引擎:', info.barcodeDetectorSupported)
|
||||
```
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
MIT License - 详见 [LICENSE](../../LICENSE) 文件
|
||||
|
||||
## 🤝 贡献
|
||||
|
||||
欢迎提交 Issue 和 Pull Request!
|
||||
|
||||
## 📞 支持
|
||||
|
||||
如有问题,请通过以下方式联系:
|
||||
|
||||
- 提交 Issue
|
||||
- 邮箱:[your-email@example.com]
|
||||
- 文档:查看项目 README
|
||||
|
||||
---
|
||||
|
||||
**版本**: 1.0.0
|
||||
**更新时间**: 2024年
|
||||
**作者**: JZ
|
||||
513
src/uni_modules/jz-h5-scanCode/js/cameraScanner.js
Normal file
@ -0,0 +1,513 @@
|
||||
/**
|
||||
* 摄像头扫描器模块
|
||||
* 负责处理摄像头访问、视频流管理和图像数据获取
|
||||
*/
|
||||
|
||||
class CameraScanner {
|
||||
constructor() {
|
||||
this.video = null;
|
||||
this.canvas = null;
|
||||
this.ctx = null;
|
||||
this.stream = null;
|
||||
this.drawFrameId = null; // 添加绘制帧ID
|
||||
this.lastDrawTime = 0; // 上次绘制时间
|
||||
this.drawInterval = 33; // 绘制间隔约30fps
|
||||
this.config = {
|
||||
width: 640,
|
||||
height: 480,
|
||||
facingMode: 'environment' // 后置摄像头
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测设备类型
|
||||
* @returns {string} 'mobile' | 'tablet' | 'desktop'
|
||||
*/
|
||||
detectDeviceType() {
|
||||
const userAgent = navigator.userAgent;
|
||||
const screenWidth = window.innerWidth;
|
||||
|
||||
// 检测移动设备
|
||||
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent);
|
||||
|
||||
// 更精确的判断
|
||||
if (isMobile) {
|
||||
// iPad或大屏手机视为平板
|
||||
if (/iPad/i.test(userAgent) || screenWidth >= 768) {
|
||||
return 'tablet';
|
||||
}
|
||||
return 'mobile';
|
||||
}
|
||||
|
||||
// PC端
|
||||
return 'desktop';
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为手机设备
|
||||
*/
|
||||
isMobileDevice() {
|
||||
return this.detectDeviceType() === 'mobile';
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化摄像头
|
||||
* @param {HTMLCanvasElement} canvasElement - canvas元素
|
||||
* @param {Object} config - 配置选项
|
||||
* @param {Function} onVideoSizeReady - 视频尺寸确定后的回调函数
|
||||
*/
|
||||
async init(canvasElement, config = {}, onVideoSizeReady = null) {
|
||||
try {
|
||||
this.canvas = canvasElement;
|
||||
this.ctx = this.canvas.getContext('2d');
|
||||
this.config = { ...this.config, ...config };
|
||||
this.onVideoSizeReady = onVideoSizeReady;
|
||||
|
||||
// 获取video元素(由UIManager创建)
|
||||
this.video = document.querySelector('.jz-scanner-video');
|
||||
if (!this.video) {
|
||||
throw new Error('Video元素未找到');
|
||||
}
|
||||
|
||||
// 获取摄像头流
|
||||
await this.startCamera();
|
||||
|
||||
// 摄像头初始化成功
|
||||
} catch (error) {
|
||||
console.error('摄像头初始化失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动摄像头
|
||||
*/
|
||||
async startCamera() {
|
||||
try {
|
||||
const constraints = {
|
||||
video: {
|
||||
width: { ideal: this.config.width },
|
||||
height: { ideal: this.config.height },
|
||||
facingMode: { ideal: this.config.facingMode }
|
||||
},
|
||||
audio: false
|
||||
};
|
||||
|
||||
this.stream = await navigator.mediaDevices.getUserMedia(constraints);
|
||||
this.video.srcObject = this.stream;
|
||||
|
||||
// 等待视频开始播放
|
||||
await new Promise((resolve, reject) => {
|
||||
this.video.onloadedmetadata = () => {
|
||||
this.video.play().then(resolve).catch(reject);
|
||||
};
|
||||
this.video.onerror = reject;
|
||||
});
|
||||
|
||||
// 设置canvas尺寸,根据设备类型采用不同策略
|
||||
const videoWidth = this.video.videoWidth;
|
||||
const videoHeight = this.video.videoHeight;
|
||||
|
||||
if (videoWidth > 0 && videoHeight > 0) {
|
||||
// 计算合适的canvas尺寸
|
||||
const screenWidth = window.innerWidth;
|
||||
const screenHeight = window.innerHeight - 60; // 减去导航栏高度
|
||||
const isMobile = this.isMobileDevice();
|
||||
const videoAspectRatio = videoWidth / videoHeight;
|
||||
|
||||
let canvasWidth, canvasHeight;
|
||||
|
||||
if (isMobile) {
|
||||
// 手机端:全屏显示,保持视频比例填充
|
||||
const screenAspectRatio = screenWidth / screenHeight;
|
||||
|
||||
if (videoAspectRatio > screenAspectRatio) {
|
||||
// 视频更宽,以高度为准
|
||||
canvasHeight = screenHeight;
|
||||
canvasWidth = canvasHeight * videoAspectRatio;
|
||||
} else {
|
||||
// 视频更高,以宽度为准
|
||||
canvasWidth = screenWidth;
|
||||
canvasHeight = canvasWidth / videoAspectRatio;
|
||||
}
|
||||
} else {
|
||||
// PC/平板端:高度铺满,宽度最大600px
|
||||
const maxWidth = 600;
|
||||
|
||||
// 以高度为基准计算宽度
|
||||
canvasHeight = screenHeight;
|
||||
canvasWidth = canvasHeight * videoAspectRatio;
|
||||
|
||||
// 如果宽度超过最大限制,则以宽度为准
|
||||
if (canvasWidth > maxWidth) {
|
||||
canvasWidth = maxWidth;
|
||||
canvasHeight = canvasWidth / videoAspectRatio;
|
||||
}
|
||||
}
|
||||
|
||||
this.canvas.width = Math.round(canvasWidth);
|
||||
this.canvas.height = Math.round(canvasHeight);
|
||||
|
||||
// Canvas尺寸已更新
|
||||
|
||||
// 通知主程序视频尺寸已确定,可以更新UI中的canvas
|
||||
if (this.onVideoSizeReady && typeof this.onVideoSizeReady === 'function') {
|
||||
this.onVideoSizeReady(videoWidth, videoHeight);
|
||||
}
|
||||
} else {
|
||||
console.warn('视频尺寸无效,使用屏幕尺寸初始化canvas');
|
||||
// 如果视频尺寸无效,根据设备类型使用不同策略
|
||||
const screenWidth = window.innerWidth;
|
||||
const screenHeight = window.innerHeight - 60;
|
||||
const isMobile = this.isMobileDevice();
|
||||
|
||||
let canvasWidth, canvasHeight;
|
||||
|
||||
if (isMobile) {
|
||||
// 手机端:全屏
|
||||
canvasWidth = screenWidth;
|
||||
canvasHeight = screenHeight;
|
||||
} else {
|
||||
// PC/平板端:高度铺满,宽度最大600px
|
||||
const maxWidth = 600;
|
||||
canvasWidth = Math.min(screenWidth, maxWidth);
|
||||
canvasHeight = screenHeight;
|
||||
}
|
||||
|
||||
this.canvas.width = Math.round(canvasWidth);
|
||||
this.canvas.height = Math.round(canvasHeight);
|
||||
|
||||
// 即使视频尺寸无效,也要显示canvas让用户能看到界面
|
||||
this.showCanvasDirectly();
|
||||
}
|
||||
|
||||
// 开始绘制视频帧到canvas(用于二维码识别)
|
||||
this.drawVideoFrame();
|
||||
|
||||
// 在视频开始播放后再次检查尺寸,确保正确设置
|
||||
setTimeout(() => {
|
||||
if (this.video && this.canvas && this.video.videoWidth > 0 && this.video.videoHeight > 0) {
|
||||
const newVideoWidth = this.video.videoWidth;
|
||||
const newVideoHeight = this.video.videoHeight;
|
||||
const screenWidth = window.innerWidth;
|
||||
const screenHeight = window.innerHeight - 60;
|
||||
const isMobile = this.isMobileDevice();
|
||||
|
||||
// 计算期望的canvas尺寸(使用最新的设备适配逻辑)
|
||||
const videoAspectRatio = newVideoWidth / newVideoHeight;
|
||||
|
||||
let expectedWidth, expectedHeight;
|
||||
|
||||
if (isMobile) {
|
||||
// 手机端全屏逻辑
|
||||
const screenAspectRatio = screenWidth / screenHeight;
|
||||
|
||||
if (videoAspectRatio > screenAspectRatio) {
|
||||
expectedHeight = screenHeight;
|
||||
expectedWidth = expectedHeight * videoAspectRatio;
|
||||
} else {
|
||||
expectedWidth = screenWidth;
|
||||
expectedHeight = expectedWidth / videoAspectRatio;
|
||||
}
|
||||
} else {
|
||||
// PC/平板端逻辑
|
||||
const maxWidth = 600;
|
||||
expectedHeight = screenHeight;
|
||||
expectedWidth = expectedHeight * videoAspectRatio;
|
||||
|
||||
if (expectedWidth > maxWidth) {
|
||||
expectedWidth = maxWidth;
|
||||
expectedHeight = expectedWidth / videoAspectRatio;
|
||||
}
|
||||
}
|
||||
|
||||
expectedWidth = Math.round(expectedWidth);
|
||||
expectedHeight = Math.round(expectedHeight);
|
||||
|
||||
// 如果尺寸有变化,重新设置canvas
|
||||
if (this.canvas.width !== expectedWidth || this.canvas.height !== expectedHeight) {
|
||||
// 延迟检查发现canvas尺寸需要调整
|
||||
this.updateCanvasSizeInternal(newVideoWidth, newVideoHeight);
|
||||
}
|
||||
}
|
||||
}, 500); // 500ms后再次检查
|
||||
|
||||
// 摄像头启动成功
|
||||
|
||||
// 备用显示机制:如果1秒后canvas仍然隐藏,强制显示
|
||||
setTimeout(() => {
|
||||
if (this.canvas && this.canvas.style.opacity === '0') {
|
||||
// 备用机制:强制显示canvas
|
||||
this.canvas.style.opacity = '1';
|
||||
}
|
||||
}, 1000);
|
||||
} catch (error) {
|
||||
console.error('启动摄像头失败:', error);
|
||||
throw new Error('无法访问摄像头: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制视频帧到canvas(用于二维码识别)- 优化版本
|
||||
*/
|
||||
drawVideoFrame() {
|
||||
// 检查是否需要停止绘制
|
||||
if (!this.video || !this.canvas || !this.ctx || !this.stream) {
|
||||
// 绘制条件不满足,停止绘制循环
|
||||
return;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
// 控制绘制频率,减少CPU占用
|
||||
if (now - this.lastDrawTime >= this.drawInterval) {
|
||||
if (this.video.readyState === this.video.HAVE_ENOUGH_DATA) {
|
||||
try {
|
||||
// 清除canvas以避免残留图像
|
||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
|
||||
// 将video画面绘制到canvas(用于二维码识别)
|
||||
// 确保完整绘制,保持视频比例
|
||||
this.ctx.drawImage(
|
||||
this.video,
|
||||
0, 0, this.video.videoWidth, this.video.videoHeight,
|
||||
0, 0, this.canvas.width, this.canvas.height
|
||||
);
|
||||
this.lastDrawTime = now;
|
||||
} catch (error) {
|
||||
console.warn('绘制视频帧失败:', error);
|
||||
// 绘制失败时也要停止循环
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 只有在所有条件都满足时才继续绘制下一帧
|
||||
if (this.video && this.canvas && this.ctx && this.stream) {
|
||||
this.drawFrameId = requestAnimationFrame(() => {
|
||||
this.drawVideoFrame();
|
||||
});
|
||||
} else {
|
||||
// 绘制资源已释放,停止绘制循环
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前图像数据
|
||||
* @returns {ImageData|null} 图像数据
|
||||
*/
|
||||
getImageData() {
|
||||
if (!this.canvas || !this.ctx) {
|
||||
console.warn('getImageData: canvas或context不存在');
|
||||
return null;
|
||||
}
|
||||
|
||||
// 检查canvas尺寸
|
||||
if (this.canvas.width <= 0 || this.canvas.height <= 0) {
|
||||
console.warn('getImageData: canvas尺寸无效', {
|
||||
width: this.canvas.width,
|
||||
height: this.canvas.height
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const imageData = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height);
|
||||
|
||||
// 验证获取的ImageData
|
||||
if (!imageData || !imageData.data || imageData.data.length === 0) {
|
||||
console.warn('getImageData: 获取的ImageData无效');
|
||||
return null;
|
||||
}
|
||||
|
||||
// 验证数据长度是否与尺寸匹配
|
||||
const expectedLength = this.canvas.width * this.canvas.height * 4;
|
||||
if (imageData.data.length !== expectedLength) {
|
||||
console.warn('getImageData: 数据长度不匹配', {
|
||||
expected: expectedLength,
|
||||
actual: imageData.data.length,
|
||||
width: this.canvas.width,
|
||||
height: this.canvas.height
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
return imageData;
|
||||
} catch (error) {
|
||||
console.error('getImageData: 获取图像数据失败', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止摄像头
|
||||
*/
|
||||
async stop() {
|
||||
try {
|
||||
// 停止绘制循环
|
||||
if (this.drawFrameId) {
|
||||
cancelAnimationFrame(this.drawFrameId);
|
||||
this.drawFrameId = null;
|
||||
}
|
||||
|
||||
if (this.stream) {
|
||||
this.stream.getTracks().forEach(track => track.stop());
|
||||
this.stream = null;
|
||||
}
|
||||
|
||||
if (this.video) {
|
||||
this.video.srcObject = null;
|
||||
// 注意:video元素由UIManager管理,不在这里删除
|
||||
this.video = null;
|
||||
}
|
||||
|
||||
// 重置状态
|
||||
this.lastDrawTime = 0;
|
||||
|
||||
// 摄像头已停止
|
||||
} catch (error) {
|
||||
console.error('停止摄像头失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查摄像头权限
|
||||
*/
|
||||
async checkCameraPermission() {
|
||||
try {
|
||||
// 检查是否为HTTPS环境
|
||||
if (location.protocol !== 'https:' && location.hostname !== 'localhost' && location.hostname !== '127.0.0.1') {
|
||||
throw new Error('摄像头访问需要HTTPS环境,当前为HTTP协议。请使用HTTPS访问或在localhost环境下测试。');
|
||||
}
|
||||
|
||||
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
||||
throw new Error('当前浏览器不支持摄像头访问功能');
|
||||
}
|
||||
|
||||
// 尝试获取权限状态
|
||||
if (navigator.permissions) {
|
||||
const permission = await navigator.permissions.query({ name: 'camera' });
|
||||
if (permission.state === 'denied') {
|
||||
throw new Error('摄像头权限被拒绝,请在浏览器设置中允许摄像头访问');
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试访问摄像头验证权限
|
||||
const stream = await navigator.mediaDevices.getUserMedia({
|
||||
video: { facingMode: 'environment' }
|
||||
});
|
||||
stream.getTracks().forEach(track => track.stop());
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('检查摄像头权限失败:', error);
|
||||
|
||||
// 根据错误类型提供更友好的提示
|
||||
if (error.name === 'NotAllowedError') {
|
||||
throw new Error('摄像头权限被拒绝,请允许访问摄像头权限');
|
||||
} else if (error.name === 'NotFoundError') {
|
||||
throw new Error('未检测到摄像头设备');
|
||||
} else if (error.name === 'NotSupportedError' || error.name === 'OverconstrainedError') {
|
||||
throw new Error('摄像头不支持请求的配置');
|
||||
} else if (error.message.includes('HTTPS')) {
|
||||
throw error; // 直接抛出HTTPS相关错误
|
||||
} else {
|
||||
throw new Error('无法访问摄像头: ' + error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 内部方法:更新canvas尺寸(根据设备类型采用不同策略)
|
||||
*/
|
||||
updateCanvasSizeInternal(videoWidth, videoHeight) {
|
||||
if (!this.canvas || !videoWidth || !videoHeight) {
|
||||
return;
|
||||
}
|
||||
|
||||
const screenWidth = window.innerWidth;
|
||||
const screenHeight = window.innerHeight - 60; // 减去导航栏高度
|
||||
const isMobile = this.isMobileDevice();
|
||||
const videoAspectRatio = videoWidth / videoHeight;
|
||||
|
||||
let canvasWidth, canvasHeight;
|
||||
|
||||
if (isMobile) {
|
||||
// 手机端:全屏显示,保持视频比例填充
|
||||
const screenAspectRatio = screenWidth / screenHeight;
|
||||
|
||||
if (videoAspectRatio > screenAspectRatio) {
|
||||
// 视频更宽,以高度为准
|
||||
canvasHeight = screenHeight;
|
||||
canvasWidth = canvasHeight * videoAspectRatio;
|
||||
} else {
|
||||
// 视频更高,以宽度为准
|
||||
canvasWidth = screenWidth;
|
||||
canvasHeight = canvasWidth / videoAspectRatio;
|
||||
}
|
||||
} else {
|
||||
// PC/平板端:高度铺满,宽度最大600px
|
||||
const maxWidth = 600;
|
||||
|
||||
// 以高度为基准计算宽度
|
||||
canvasHeight = screenHeight;
|
||||
canvasWidth = canvasHeight * videoAspectRatio;
|
||||
|
||||
// 如果宽度超过最大限制,则以宽度为准
|
||||
if (canvasWidth > maxWidth) {
|
||||
canvasWidth = maxWidth;
|
||||
canvasHeight = canvasWidth / videoAspectRatio;
|
||||
}
|
||||
}
|
||||
|
||||
this.canvas.width = Math.round(canvasWidth);
|
||||
this.canvas.height = Math.round(canvasHeight);
|
||||
|
||||
// Canvas尺寸已更新(内部调用)
|
||||
|
||||
// 通知主程序视频尺寸已确定
|
||||
if (this.onVideoSizeReady && typeof this.onVideoSizeReady === 'function') {
|
||||
this.onVideoSizeReady(videoWidth, videoHeight);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 直接显示canvas(在视频尺寸无效时的备用方案)
|
||||
*/
|
||||
showCanvasDirectly() {
|
||||
if (!this.canvas) return;
|
||||
|
||||
// 延迟显示,避免iOS Safari的闪烁问题
|
||||
setTimeout(() => {
|
||||
if (this.canvas) {
|
||||
this.canvas.style.opacity = '1';
|
||||
// Canvas已直接显示(备用方案)
|
||||
}
|
||||
}, 200); // 稍长延迟,确保iOS Safari处理完毕
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否支持摄像头
|
||||
*/
|
||||
static async isSupported() {
|
||||
return !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取可用摄像头列表
|
||||
*/
|
||||
static async getCameras() {
|
||||
try {
|
||||
if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||
return devices.filter(device => device.kind === 'videoinput');
|
||||
} catch (error) {
|
||||
console.error('获取摄像头列表失败:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default CameraScanner;
|
||||
286
src/uni_modules/jz-h5-scanCode/js/imageProcessor.worker.js
Normal file
@ -0,0 +1,286 @@
|
||||
/**
|
||||
* 图片处理Web Worker
|
||||
* 用于异步处理图片压缩,避免阻塞主线程
|
||||
*/
|
||||
|
||||
// 监听主线程消息
|
||||
self.onmessage = function(e) {
|
||||
const { action, data, taskId } = e.data;
|
||||
|
||||
try {
|
||||
switch (action) {
|
||||
case 'compressImage':
|
||||
handleCompressImage(data, taskId);
|
||||
break;
|
||||
case 'extremeCompress':
|
||||
handleExtremeCompress(data, taskId);
|
||||
break;
|
||||
default:
|
||||
postMessage({
|
||||
taskId,
|
||||
success: false,
|
||||
error: '未知的操作类型: ' + action
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
postMessage({
|
||||
taskId,
|
||||
success: false,
|
||||
error: error.message || '处理过程中发生未知错误'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理标准图片压缩
|
||||
*/
|
||||
async function handleCompressImage(data, taskId) {
|
||||
try {
|
||||
const { imageData, maxSize, minSize, quality } = data;
|
||||
|
||||
// 发送进度更新
|
||||
postMessage({
|
||||
taskId,
|
||||
type: 'progress',
|
||||
message: '正在压缩图片...',
|
||||
progress: 20
|
||||
});
|
||||
|
||||
// 创建canvas进行压缩
|
||||
const canvas = new OffscreenCanvas(imageData.width, imageData.height);
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// 将ImageData绘制到canvas
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
|
||||
// 计算压缩尺寸
|
||||
const { width, height } = calculateOptimalSize(
|
||||
imageData.width,
|
||||
imageData.height,
|
||||
maxSize,
|
||||
minSize
|
||||
);
|
||||
|
||||
postMessage({
|
||||
taskId,
|
||||
type: 'progress',
|
||||
message: `压缩尺寸: ${width}x${height}`,
|
||||
progress: 50
|
||||
});
|
||||
|
||||
// 创建压缩后的canvas
|
||||
const compressedCanvas = new OffscreenCanvas(width, height);
|
||||
const compressedCtx = compressedCanvas.getContext('2d');
|
||||
|
||||
// 使用高质量缩放
|
||||
compressedCtx.imageSmoothingEnabled = true;
|
||||
compressedCtx.imageSmoothingQuality = 'high';
|
||||
|
||||
// 绘制压缩后的图片
|
||||
compressedCtx.drawImage(canvas, 0, 0, width, height);
|
||||
|
||||
postMessage({
|
||||
taskId,
|
||||
type: 'progress',
|
||||
message: '生成压缩结果...',
|
||||
progress: 80
|
||||
});
|
||||
|
||||
// 获取压缩后的ImageData
|
||||
const compressedImageData = compressedCtx.getImageData(0, 0, width, height);
|
||||
|
||||
postMessage({
|
||||
taskId,
|
||||
success: true,
|
||||
result: {
|
||||
imageData: compressedImageData,
|
||||
originalSize: imageData.width + 'x' + imageData.height,
|
||||
compressedSize: width + 'x' + height,
|
||||
compressionRatio: ((1 - (width * height) / (imageData.width * imageData.height)) * 100).toFixed(1) + '%'
|
||||
},
|
||||
type: 'complete',
|
||||
progress: 100
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
postMessage({
|
||||
taskId,
|
||||
success: false,
|
||||
error: '图片压缩失败: ' + error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理极限压缩
|
||||
*/
|
||||
async function handleExtremeCompress(data, taskId) {
|
||||
try {
|
||||
const { imageData } = data;
|
||||
|
||||
postMessage({
|
||||
taskId,
|
||||
type: 'progress',
|
||||
message: '正在极限压缩图片...',
|
||||
progress: 10
|
||||
});
|
||||
|
||||
// 创建canvas
|
||||
const canvas = new OffscreenCanvas(imageData.width, imageData.height);
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// 将ImageData绘制到canvas
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
|
||||
postMessage({
|
||||
taskId,
|
||||
type: 'progress',
|
||||
message: '应用极限压缩算法...',
|
||||
progress: 30
|
||||
});
|
||||
|
||||
// 极限压缩:固定400px正方形
|
||||
const targetSize = 400;
|
||||
const compressedCanvas = new OffscreenCanvas(targetSize, targetSize);
|
||||
const compressedCtx = compressedCanvas.getContext('2d');
|
||||
|
||||
// 关闭图片平滑,提高速度
|
||||
compressedCtx.imageSmoothingEnabled = false;
|
||||
|
||||
postMessage({
|
||||
taskId,
|
||||
type: 'progress',
|
||||
message: '正在生成400x400压缩图...',
|
||||
progress: 60
|
||||
});
|
||||
|
||||
// 分块处理,避免一次性处理大图
|
||||
await drawImageInChunks(compressedCtx, canvas, targetSize, targetSize, taskId);
|
||||
|
||||
postMessage({
|
||||
taskId,
|
||||
type: 'progress',
|
||||
message: '压缩完成,生成结果...',
|
||||
progress: 90
|
||||
});
|
||||
|
||||
// 获取压缩后的ImageData
|
||||
const compressedImageData = compressedCtx.getImageData(0, 0, targetSize, targetSize);
|
||||
|
||||
postMessage({
|
||||
taskId,
|
||||
success: true,
|
||||
result: {
|
||||
imageData: compressedImageData,
|
||||
originalSize: imageData.width + 'x' + imageData.height,
|
||||
compressedSize: targetSize + 'x' + targetSize,
|
||||
compressionRatio: ((1 - (targetSize * targetSize) / (imageData.width * imageData.height)) * 100).toFixed(1) + '%'
|
||||
},
|
||||
type: 'complete',
|
||||
progress: 100
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
postMessage({
|
||||
taskId,
|
||||
success: false,
|
||||
error: '极限压缩失败: ' + error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分块绘制图片
|
||||
*/
|
||||
async function drawImageInChunks(ctx, sourceCanvas, targetWidth, targetHeight, taskId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const chunkSize = 100; // 每次处理100px的区块
|
||||
let currentY = 0;
|
||||
|
||||
const drawChunk = () => {
|
||||
if (currentY >= targetHeight) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const chunkHeight = Math.min(chunkSize, targetHeight - currentY);
|
||||
const sourceY = (currentY / targetHeight) * sourceCanvas.height;
|
||||
const sourceHeight = (chunkHeight / targetHeight) * sourceCanvas.height;
|
||||
|
||||
try {
|
||||
// 绘制当前区块
|
||||
ctx.drawImage(
|
||||
sourceCanvas,
|
||||
0, sourceY, sourceCanvas.width, sourceHeight,
|
||||
0, currentY, targetWidth, chunkHeight
|
||||
);
|
||||
|
||||
currentY += chunkHeight;
|
||||
|
||||
// 更新进度
|
||||
const progress = 60 + Math.floor((currentY / targetHeight) * 25);
|
||||
postMessage({
|
||||
taskId,
|
||||
type: 'progress',
|
||||
message: `处理进度: ${Math.floor((currentY / targetHeight) * 100)}%`,
|
||||
progress
|
||||
});
|
||||
|
||||
// 异步处理下一个区块
|
||||
setTimeout(drawChunk, 5);
|
||||
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
};
|
||||
|
||||
drawChunk();
|
||||
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算最优压缩尺寸
|
||||
*/
|
||||
function calculateOptimalSize(originalWidth, originalHeight, maxSize, minSize) {
|
||||
let width = originalWidth;
|
||||
let height = originalHeight;
|
||||
|
||||
// 计算压缩比例
|
||||
const ratio = Math.min(maxSize / width, maxSize / height);
|
||||
|
||||
if (ratio < 1) {
|
||||
width = Math.floor(width * ratio);
|
||||
height = Math.floor(height * ratio);
|
||||
}
|
||||
|
||||
// 确保不小于最小尺寸
|
||||
if (width < minSize && height < minSize) {
|
||||
const minRatio = Math.max(minSize / width, minSize / height);
|
||||
width = Math.floor(width * minRatio);
|
||||
height = Math.floor(height * minRatio);
|
||||
}
|
||||
|
||||
// 针对扫码场景,优先正方形比例
|
||||
const avgSize = Math.floor((width + height) / 2);
|
||||
if (avgSize <= maxSize && avgSize >= minSize) {
|
||||
const squareSize = Math.min(avgSize, 600);
|
||||
return { width: squareSize, height: squareSize };
|
||||
}
|
||||
|
||||
return { width, height };
|
||||
}
|
||||
|
||||
// 错误处理
|
||||
self.onerror = function(error) {
|
||||
postMessage({
|
||||
success: false,
|
||||
error: 'Worker发生错误: ' + error.message
|
||||
});
|
||||
};
|
||||
|
||||
// 图片处理Worker已启动
|
||||
2162
src/uni_modules/jz-h5-scanCode/js/index.js
Normal file
1197
src/uni_modules/jz-h5-scanCode/js/qrCodeDecoder.js
Normal file
1057
src/uni_modules/jz-h5-scanCode/js/uiManager.js
Normal file
332
src/uni_modules/jz-h5-scanCode/js/utils.js
Normal file
@ -0,0 +1,332 @@
|
||||
/**
|
||||
* 工具类模块
|
||||
* 提供调试、错误处理和通用功能
|
||||
*/
|
||||
|
||||
class Utils {
|
||||
/**
|
||||
* 调试模式
|
||||
*/
|
||||
static DEBUG = false;
|
||||
|
||||
/**
|
||||
* 日志输出
|
||||
*/
|
||||
static log(message, ...args) {
|
||||
if (this.DEBUG) {
|
||||
console.log(`[jz-h5-scanCode] ${message}`, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 错误日志
|
||||
*/
|
||||
static error(message, error) {
|
||||
console.error(`[jz-h5-scanCode] ${message}`, error);
|
||||
}
|
||||
|
||||
/**
|
||||
* 警告日志
|
||||
*/
|
||||
static warn(message, ...args) {
|
||||
console.warn(`[jz-h5-scanCode] ${message}`, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为移动设备
|
||||
*/
|
||||
static isMobile() {
|
||||
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为iOS设备
|
||||
*/
|
||||
static isIOS() {
|
||||
return /iPad|iPhone|iPod/.test(navigator.userAgent);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为Android设备
|
||||
*/
|
||||
static isAndroid() {
|
||||
return /Android/i.test(navigator.userAgent);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查浏览器支持情况
|
||||
*/
|
||||
static checkBrowserSupport() {
|
||||
const support = {
|
||||
getUserMedia: !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia),
|
||||
barcodeDetector: 'BarcodeDetector' in window,
|
||||
canvas: !!document.createElement('canvas').getContext,
|
||||
webGL: (() => {
|
||||
try {
|
||||
const canvas = document.createElement('canvas');
|
||||
return !!(canvas.getContext('webgl') || canvas.getContext('experimental-webgl'));
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
})(),
|
||||
requestAnimationFrame: 'requestAnimationFrame' in window,
|
||||
vibrate: 'vibrate' in navigator
|
||||
};
|
||||
|
||||
return support;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建错误对象
|
||||
*/
|
||||
static createError(code, message, details = null) {
|
||||
const error = new Error(message);
|
||||
error.code = code;
|
||||
error.details = details;
|
||||
return error;
|
||||
}
|
||||
|
||||
/**
|
||||
* 错误代码常量
|
||||
*/
|
||||
static ERROR_CODES = {
|
||||
CAMERA_PERMISSION_DENIED: 'CAMERA_PERMISSION_DENIED',
|
||||
CAMERA_NOT_FOUND: 'CAMERA_NOT_FOUND',
|
||||
DECODER_INIT_FAILED: 'DECODER_INIT_FAILED',
|
||||
SCAN_TIMEOUT: 'SCAN_TIMEOUT',
|
||||
BROWSER_NOT_SUPPORTED: 'BROWSER_NOT_SUPPORTED',
|
||||
USER_CANCELLED: 'USER_CANCELLED'
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取友好的错误信息
|
||||
*/
|
||||
static getErrorMessage(error) {
|
||||
const messages = {
|
||||
[this.ERROR_CODES.CAMERA_PERMISSION_DENIED]: '请允许访问摄像头权限',
|
||||
[this.ERROR_CODES.CAMERA_NOT_FOUND]: '未检测到摄像头设备',
|
||||
[this.ERROR_CODES.DECODER_INIT_FAILED]: '扫码器初始化失败',
|
||||
[this.ERROR_CODES.SCAN_TIMEOUT]: '扫码超时',
|
||||
[this.ERROR_CODES.BROWSER_NOT_SUPPORTED]: '当前浏览器不支持扫码功能',
|
||||
[this.ERROR_CODES.USER_CANCELLED]: '用户取消扫码'
|
||||
};
|
||||
|
||||
return messages[error.code] || error.message || '未知错误';
|
||||
}
|
||||
|
||||
/**
|
||||
* 防抖函数
|
||||
*/
|
||||
static debounce(func, wait, immediate = false) {
|
||||
let timeout;
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
timeout = null;
|
||||
if (!immediate) func.apply(this, args);
|
||||
};
|
||||
const callNow = immediate && !timeout;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
if (callNow) func.apply(this, args);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 节流函数
|
||||
*/
|
||||
static throttle(func, limit) {
|
||||
let inThrottle;
|
||||
return function(...args) {
|
||||
if (!inThrottle) {
|
||||
func.apply(this, args);
|
||||
inThrottle = true;
|
||||
setTimeout(() => inThrottle = false, limit);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 深度克隆对象
|
||||
*/
|
||||
static deepClone(obj) {
|
||||
if (obj === null || typeof obj !== 'object') return obj;
|
||||
if (obj instanceof Date) return new Date(obj.getTime());
|
||||
if (obj instanceof Array) return obj.map(item => this.deepClone(item));
|
||||
if (typeof obj === 'object') {
|
||||
const clonedObj = {};
|
||||
for (const key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
clonedObj[key] = this.deepClone(obj[key]);
|
||||
}
|
||||
}
|
||||
return clonedObj;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成唯一ID
|
||||
*/
|
||||
static generateId() {
|
||||
return Math.random().toString(36).substr(2, 9);
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化文件大小
|
||||
*/
|
||||
static formatBytes(bytes, decimals = 2) {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const dm = decimals < 0 ? 0 : decimals;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取设备信息
|
||||
*/
|
||||
static getDeviceInfo() {
|
||||
return {
|
||||
userAgent: navigator.userAgent,
|
||||
platform: navigator.platform,
|
||||
language: navigator.language,
|
||||
cookieEnabled: navigator.cookieEnabled,
|
||||
onLine: navigator.onLine,
|
||||
screenWidth: screen.width,
|
||||
screenHeight: screen.height,
|
||||
windowWidth: window.innerWidth,
|
||||
windowHeight: window.innerHeight,
|
||||
pixelRatio: window.devicePixelRatio || 1,
|
||||
isMobile: this.isMobile(),
|
||||
isIOS: this.isIOS(),
|
||||
isAndroid: this.isAndroid()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 性能监测
|
||||
*/
|
||||
static performanceMonitor() {
|
||||
if (!performance || !performance.mark) {
|
||||
return {
|
||||
start: () => {},
|
||||
end: () => 0
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
start: (name) => {
|
||||
performance.mark(`${name}-start`);
|
||||
},
|
||||
end: (name) => {
|
||||
performance.mark(`${name}-end`);
|
||||
performance.measure(name, `${name}-start`, `${name}-end`);
|
||||
const measure = performance.getEntriesByName(name)[0];
|
||||
return measure ? measure.duration : 0;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全执行函数
|
||||
*/
|
||||
static safeExecute(func, defaultValue = null) {
|
||||
try {
|
||||
return func();
|
||||
} catch (error) {
|
||||
this.error('Safe execute failed:', error);
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步安全执行
|
||||
*/
|
||||
static async safeExecuteAsync(func, defaultValue = null) {
|
||||
try {
|
||||
return await func();
|
||||
} catch (error) {
|
||||
this.error('Safe execute async failed:', error);
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 简单的事件发射器
|
||||
*/
|
||||
static createEventEmitter() {
|
||||
const events = {};
|
||||
|
||||
return {
|
||||
on(event, callback) {
|
||||
if (!events[event]) {
|
||||
events[event] = [];
|
||||
}
|
||||
events[event].push(callback);
|
||||
},
|
||||
off(event, callback) {
|
||||
if (events[event]) {
|
||||
events[event] = events[event].filter(cb => cb !== callback);
|
||||
}
|
||||
},
|
||||
emit(event, ...args) {
|
||||
if (events[event]) {
|
||||
events[event].forEach(callback => {
|
||||
try {
|
||||
callback(...args);
|
||||
} catch (error) {
|
||||
Utils.error(`Event callback error for ${event}:`, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* URL参数解析
|
||||
*/
|
||||
static parseUrlParams(url = window.location.href) {
|
||||
const params = {};
|
||||
const urlObj = new URL(url);
|
||||
urlObj.searchParams.forEach((value, key) => {
|
||||
params[key] = value;
|
||||
});
|
||||
return params;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查网络连接状态
|
||||
*/
|
||||
static checkNetworkStatus() {
|
||||
return {
|
||||
online: navigator.onLine,
|
||||
connection: navigator.connection || navigator.mozConnection || navigator.webkitConnection
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为安全环境(HTTPS或localhost)
|
||||
*/
|
||||
static isSecureContext() {
|
||||
return location.protocol === 'https:' ||
|
||||
location.hostname === 'localhost' ||
|
||||
location.hostname === '127.0.0.1' ||
|
||||
location.hostname === '0.0.0.0';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前协议和主机信息
|
||||
*/
|
||||
static getLocationInfo() {
|
||||
return {
|
||||
protocol: location.protocol,
|
||||
hostname: location.hostname,
|
||||
port: location.port,
|
||||
isSecure: this.isSecureContext(),
|
||||
needsHTTPS: !this.isSecureContext()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default Utils;
|
||||
533
src/uni_modules/jz-h5-scanCode/js/workerManager.js
Normal file
@ -0,0 +1,533 @@
|
||||
/**
|
||||
* Web Worker 管理器
|
||||
* 负责管理图片处理Worker的生命周期和通信
|
||||
*/
|
||||
|
||||
class WorkerManager {
|
||||
constructor() {
|
||||
this.worker = null;
|
||||
this.tasks = new Map(); // 存储任务
|
||||
this.taskIdCounter = 0;
|
||||
this.isSupported = typeof Worker !== 'undefined';
|
||||
this.progressCallback = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查浏览器是否支持Web Worker
|
||||
*/
|
||||
checkWorkerSupport() {
|
||||
try {
|
||||
// 由于OffscreenCanvas兼容性问题,暂时禁用Worker
|
||||
// 将来可以考虑使用ImageBitmap等替代方案
|
||||
return false; // 暂时禁用Worker,使用主线程处理
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化Worker
|
||||
*/
|
||||
async init() {
|
||||
if (!this.isSupported) {
|
||||
console.warn('不支持Web Worker,使用主线程处理');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.worker) return true;
|
||||
|
||||
try {
|
||||
const workerCode = `
|
||||
self.onmessage = function(e) {
|
||||
const { action, data, taskId } = e.data;
|
||||
|
||||
if (action === 'compressImage') {
|
||||
try {
|
||||
const { imageData, maxSize, minSize } = data;
|
||||
|
||||
self.postMessage({
|
||||
taskId,
|
||||
type: 'progress',
|
||||
message: '正在压缩图片...',
|
||||
progress: 20
|
||||
});
|
||||
|
||||
// 在Worker中不能使用OffscreenCanvas时的降级处理
|
||||
self.postMessage({
|
||||
taskId,
|
||||
success: false,
|
||||
error: '当前浏览器不支持OffscreenCanvas,将使用主线程处理'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
self.postMessage({
|
||||
taskId,
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
`;
|
||||
|
||||
const blob = new Blob([workerCode], { type: 'application/javascript' });
|
||||
this.worker = new Worker(URL.createObjectURL(blob));
|
||||
|
||||
this.worker.onmessage = (e) => {
|
||||
this.handleWorkerMessage(e.data);
|
||||
};
|
||||
|
||||
this.worker.onerror = (error) => {
|
||||
console.error('Worker错误:', error);
|
||||
this.handleWorkerError(error);
|
||||
};
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Worker初始化失败:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理Worker消息
|
||||
*/
|
||||
handleWorkerMessage(data) {
|
||||
const { taskId, type, success, result, error, message, progress } = data;
|
||||
const task = this.tasks.get(taskId);
|
||||
|
||||
if (!task) return;
|
||||
|
||||
if (type === 'progress' && task.onProgress) {
|
||||
task.onProgress(message, progress);
|
||||
} else if (success !== undefined) {
|
||||
this.tasks.delete(taskId);
|
||||
if (success) {
|
||||
task.resolve(result);
|
||||
} else {
|
||||
task.reject(new Error(error));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理Worker错误
|
||||
*/
|
||||
handleWorkerError(error) {
|
||||
for (const [taskId, task] of this.tasks) {
|
||||
task.reject(new Error('Worker错误: ' + error.message));
|
||||
}
|
||||
this.tasks.clear();
|
||||
this.worker = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置进度回调
|
||||
*/
|
||||
setProgressCallback(callback) {
|
||||
this.progressCallback = callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* 压缩图片
|
||||
*/
|
||||
async compressImage(imageData, maxSize = 800, minSize = 400, quality = 0.6, onProgress = null) {
|
||||
// 由于浏览器兼容性问题,直接使用主线程处理
|
||||
return this.fallbackCompress(imageData, maxSize, minSize, quality, onProgress);
|
||||
}
|
||||
|
||||
/**
|
||||
* 极限压缩
|
||||
*/
|
||||
async extremeCompress(imageData, onProgress = null) {
|
||||
// 如果不支持Worker,使用主线程处理
|
||||
if (!this.isSupported || !this.worker) {
|
||||
return this.fallbackExtremeCompress(imageData, onProgress);
|
||||
}
|
||||
|
||||
const taskId = ++this.taskIdCounter;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// 存储任务
|
||||
this.tasks.set(taskId, { resolve, reject, onProgress });
|
||||
|
||||
// 发送任务到Worker
|
||||
this.worker.postMessage({
|
||||
action: 'extremeCompress',
|
||||
taskId,
|
||||
data: { imageData }
|
||||
});
|
||||
|
||||
// 设置超时
|
||||
setTimeout(() => {
|
||||
if (this.tasks.has(taskId)) {
|
||||
this.tasks.delete(taskId);
|
||||
reject(new Error('极限压缩超时'));
|
||||
}
|
||||
}, 20000); // 20秒超时
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 主线程降级处理 - 标准压缩
|
||||
*/
|
||||
async fallbackCompress(imageData, maxSize, minSize, quality, onProgress) {
|
||||
try {
|
||||
if (onProgress) onProgress('开始处理图片...', 10);
|
||||
|
||||
// 使用requestAnimationFrame确保UI更新
|
||||
await new Promise(resolve => requestAnimationFrame(resolve));
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
canvas.width = imageData.width;
|
||||
canvas.height = imageData.height;
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
|
||||
if (onProgress) onProgress('计算压缩尺寸...', 30);
|
||||
await new Promise(resolve => requestAnimationFrame(resolve));
|
||||
|
||||
const { width, height } = this.calculateOptimalSize(
|
||||
imageData.width,
|
||||
imageData.height,
|
||||
maxSize,
|
||||
minSize
|
||||
);
|
||||
|
||||
if (onProgress) onProgress(`压缩到 ${width}x${height}`, 50);
|
||||
await new Promise(resolve => requestAnimationFrame(resolve));
|
||||
|
||||
const compressedCanvas = document.createElement('canvas');
|
||||
const compressedCtx = compressedCanvas.getContext('2d');
|
||||
|
||||
compressedCanvas.width = width;
|
||||
compressedCanvas.height = height;
|
||||
|
||||
compressedCtx.imageSmoothingEnabled = true;
|
||||
compressedCtx.imageSmoothingQuality = 'high';
|
||||
|
||||
if (onProgress) onProgress('绘制压缩图片...', 80);
|
||||
await new Promise(resolve => requestAnimationFrame(resolve));
|
||||
|
||||
compressedCtx.drawImage(canvas, 0, 0, width, height);
|
||||
|
||||
if (onProgress) onProgress('生成结果...', 95);
|
||||
await new Promise(resolve => requestAnimationFrame(resolve));
|
||||
|
||||
const compressedImageData = compressedCtx.getImageData(0, 0, width, height);
|
||||
|
||||
if (onProgress) onProgress('压缩完成', 100);
|
||||
|
||||
return {
|
||||
imageData: compressedImageData,
|
||||
originalSize: imageData.width + 'x' + imageData.height,
|
||||
compressedSize: width + 'x' + height,
|
||||
compressionRatio: ((1 - (width * height) / (imageData.width * imageData.height)) * 100).toFixed(1) + '%'
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
throw new Error('图片压缩失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 主线程降级处理 - 极限压缩
|
||||
*/
|
||||
async fallbackExtremeCompress(imageData, onProgress) {
|
||||
try {
|
||||
if (onProgress) onProgress('主线程极限压缩...', 10);
|
||||
await new Promise(resolve => requestAnimationFrame(resolve));
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
canvas.width = imageData.width;
|
||||
canvas.height = imageData.height;
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
|
||||
if (onProgress) onProgress('生成400x400图片...', 50);
|
||||
await new Promise(resolve => requestAnimationFrame(resolve));
|
||||
|
||||
const targetSize = 400;
|
||||
const compressedCanvas = document.createElement('canvas');
|
||||
const compressedCtx = compressedCanvas.getContext('2d');
|
||||
|
||||
compressedCanvas.width = targetSize;
|
||||
compressedCanvas.height = targetSize;
|
||||
compressedCtx.imageSmoothingEnabled = false;
|
||||
|
||||
if (onProgress) onProgress('绘制极限压缩图...', 80);
|
||||
await new Promise(resolve => requestAnimationFrame(resolve));
|
||||
|
||||
compressedCtx.drawImage(canvas, 0, 0, targetSize, targetSize);
|
||||
|
||||
if (onProgress) onProgress('生成结果...', 95);
|
||||
await new Promise(resolve => requestAnimationFrame(resolve));
|
||||
|
||||
const compressedImageData = compressedCtx.getImageData(0, 0, targetSize, targetSize);
|
||||
|
||||
if (onProgress) onProgress('极限压缩完成', 100);
|
||||
|
||||
return {
|
||||
imageData: compressedImageData,
|
||||
originalSize: imageData.width + 'x' + imageData.height,
|
||||
compressedSize: targetSize + 'x' + targetSize,
|
||||
compressionRatio: ((1 - (targetSize * targetSize) / (imageData.width * imageData.height)) * 100).toFixed(1) + '%'
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
throw new Error('主线程极限压缩失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算最优压缩尺寸
|
||||
*/
|
||||
calculateOptimalSize(originalWidth, originalHeight, maxSize, minSize) {
|
||||
let width = originalWidth;
|
||||
let height = originalHeight;
|
||||
|
||||
const ratio = Math.min(maxSize / width, maxSize / height);
|
||||
|
||||
if (ratio < 1) {
|
||||
width = Math.floor(width * ratio);
|
||||
height = Math.floor(height * ratio);
|
||||
}
|
||||
|
||||
if (width < minSize && height < minSize) {
|
||||
const minRatio = Math.max(minSize / width, minSize / height);
|
||||
width = Math.floor(width * minRatio);
|
||||
height = Math.floor(height * minRatio);
|
||||
}
|
||||
|
||||
const avgSize = Math.floor((width + height) / 2);
|
||||
if (avgSize <= maxSize && avgSize >= minSize) {
|
||||
const squareSize = Math.min(avgSize, 600);
|
||||
return { width: squareSize, height: squareSize };
|
||||
}
|
||||
|
||||
return { width, height };
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁Worker
|
||||
*/
|
||||
destroy() {
|
||||
if (this.worker) {
|
||||
for (const [taskId, task] of this.tasks) {
|
||||
task.reject(new Error('Worker被销毁'));
|
||||
}
|
||||
this.tasks.clear();
|
||||
this.worker.terminate();
|
||||
this.worker = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Worker代码
|
||||
*/
|
||||
getWorkerCode() {
|
||||
return `
|
||||
// 图片处理Worker代码
|
||||
self.onmessage = function(e) {
|
||||
const { action, data, taskId } = e.data;
|
||||
|
||||
try {
|
||||
switch (action) {
|
||||
case 'compressImage':
|
||||
handleCompressImage(data, taskId);
|
||||
break;
|
||||
case 'extremeCompress':
|
||||
handleExtremeCompress(data, taskId);
|
||||
break;
|
||||
default:
|
||||
postMessage({
|
||||
taskId,
|
||||
success: false,
|
||||
error: '未知的操作类型: ' + action
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
postMessage({
|
||||
taskId,
|
||||
success: false,
|
||||
error: error.message || '处理过程中发生未知错误'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function handleCompressImage(data, taskId) {
|
||||
try {
|
||||
const { imageData, maxSize, minSize, quality } = data;
|
||||
|
||||
postMessage({
|
||||
taskId,
|
||||
type: 'progress',
|
||||
message: '正在压缩图片...',
|
||||
progress: 20
|
||||
});
|
||||
|
||||
// 检查OffscreenCanvas支持
|
||||
if (typeof OffscreenCanvas === 'undefined') {
|
||||
throw new Error('当前浏览器不支持OffscreenCanvas');
|
||||
}
|
||||
|
||||
// 使用canvas进行压缩
|
||||
const canvas = new OffscreenCanvas(imageData.width, imageData.height);
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
|
||||
const { width, height } = calculateOptimalSize(imageData.width, imageData.height, maxSize, minSize);
|
||||
|
||||
postMessage({
|
||||
taskId,
|
||||
type: 'progress',
|
||||
message: '压缩尺寸: ' + width + 'x' + height,
|
||||
progress: 50
|
||||
});
|
||||
|
||||
const compressedCanvas = new OffscreenCanvas(width, height);
|
||||
const compressedCtx = compressedCanvas.getContext('2d');
|
||||
|
||||
compressedCtx.imageSmoothingEnabled = true;
|
||||
compressedCtx.imageSmoothingQuality = 'high';
|
||||
compressedCtx.drawImage(canvas, 0, 0, width, height);
|
||||
|
||||
postMessage({
|
||||
taskId,
|
||||
type: 'progress',
|
||||
message: '生成压缩结果...',
|
||||
progress: 80
|
||||
});
|
||||
|
||||
const compressedImageData = compressedCtx.getImageData(0, 0, width, height);
|
||||
|
||||
postMessage({
|
||||
taskId,
|
||||
success: true,
|
||||
result: {
|
||||
imageData: compressedImageData,
|
||||
originalSize: imageData.width + 'x' + imageData.height,
|
||||
compressedSize: width + 'x' + height,
|
||||
compressionRatio: ((1 - (width * height) / (imageData.width * imageData.height)) * 100).toFixed(1) + '%'
|
||||
},
|
||||
type: 'complete',
|
||||
progress: 100
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
postMessage({
|
||||
taskId,
|
||||
success: false,
|
||||
error: '图片压缩失败: ' + error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function handleExtremeCompress(data, taskId) {
|
||||
try {
|
||||
const { imageData } = data;
|
||||
|
||||
postMessage({
|
||||
taskId,
|
||||
type: 'progress',
|
||||
message: '正在极限压缩图片...',
|
||||
progress: 10
|
||||
});
|
||||
|
||||
// 检查OffscreenCanvas支持
|
||||
if (typeof OffscreenCanvas === 'undefined') {
|
||||
throw new Error('当前浏览器不支持OffscreenCanvas');
|
||||
}
|
||||
|
||||
const canvas = new OffscreenCanvas(imageData.width, imageData.height);
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
|
||||
const targetSize = 400;
|
||||
const compressedCanvas = new OffscreenCanvas(targetSize, targetSize);
|
||||
const compressedCtx = compressedCanvas.getContext('2d');
|
||||
|
||||
compressedCtx.imageSmoothingEnabled = false;
|
||||
|
||||
postMessage({
|
||||
taskId,
|
||||
type: 'progress',
|
||||
message: '正在生成400x400压缩图...',
|
||||
progress: 60
|
||||
});
|
||||
|
||||
compressedCtx.drawImage(canvas, 0, 0, targetSize, targetSize);
|
||||
|
||||
postMessage({
|
||||
taskId,
|
||||
type: 'progress',
|
||||
message: '压缩完成,生成结果...',
|
||||
progress: 90
|
||||
});
|
||||
|
||||
const compressedImageData = compressedCtx.getImageData(0, 0, targetSize, targetSize);
|
||||
|
||||
postMessage({
|
||||
taskId,
|
||||
success: true,
|
||||
result: {
|
||||
imageData: compressedImageData,
|
||||
originalSize: imageData.width + 'x' + imageData.height,
|
||||
compressedSize: targetSize + 'x' + targetSize,
|
||||
compressionRatio: ((1 - (targetSize * targetSize) / (imageData.width * imageData.height)) * 100).toFixed(1) + '%'
|
||||
},
|
||||
type: 'complete',
|
||||
progress: 100
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
postMessage({
|
||||
taskId,
|
||||
success: false,
|
||||
error: '极限压缩失败: ' + error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function calculateOptimalSize(originalWidth, originalHeight, maxSize, minSize) {
|
||||
let width = originalWidth;
|
||||
let height = originalHeight;
|
||||
|
||||
const ratio = Math.min(maxSize / width, maxSize / height);
|
||||
|
||||
if (ratio < 1) {
|
||||
width = Math.floor(width * ratio);
|
||||
height = Math.floor(height * ratio);
|
||||
}
|
||||
|
||||
if (width < minSize && height < minSize) {
|
||||
const minRatio = Math.max(minSize / width, minSize / height);
|
||||
width = Math.floor(width * minRatio);
|
||||
height = Math.floor(height * minRatio);
|
||||
}
|
||||
|
||||
const avgSize = Math.floor((width + height) / 2);
|
||||
if (avgSize <= maxSize && avgSize >= minSize) {
|
||||
const squareSize = Math.min(avgSize, 600);
|
||||
return { width: squareSize, height: squareSize };
|
||||
}
|
||||
|
||||
return { width, height };
|
||||
}
|
||||
|
||||
self.onerror = function(error) {
|
||||
postMessage({
|
||||
success: false,
|
||||
error: 'Worker发生错误: ' + error.message
|
||||
});
|
||||
};
|
||||
|
||||
// 图片处理Worker已启动
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// 导出管理器
|
||||
export default WorkerManager;
|
||||
106
src/uni_modules/jz-h5-scanCode/package.json
Normal file
@ -0,0 +1,106 @@
|
||||
{
|
||||
"id": "jz-h5-scanCode",
|
||||
"displayName": "jz-h5-scanCode 扫码插件",
|
||||
"name": "jz-h5-scancode",
|
||||
"version": "1.0.5",
|
||||
"description": "H5环境下的二维码扫描插件,提供与 uni.scanCode 完全兼容的 API 接口,支持摄像头实时扫码和图片扫码。v1.0.0优化性能,修复卡死问题",
|
||||
"keywords": [
|
||||
"扫码",
|
||||
"二维码",
|
||||
"h5扫码",
|
||||
"h5扫码插件"
|
||||
],
|
||||
"repository": "https://gitcode.com/Etangzeng/jz-h5-scanCode",
|
||||
"engines": {
|
||||
"HBuilderX": "^4.65",
|
||||
"uni-app": "^4.62",
|
||||
"uni-app-x": ""
|
||||
},
|
||||
"dcloudext": {
|
||||
"type": "sdk-js",
|
||||
"sale": {
|
||||
"regular": {
|
||||
"price": "0.00"
|
||||
},
|
||||
"sourcecode": {
|
||||
"price": "0.00"
|
||||
}
|
||||
},
|
||||
"contact": {
|
||||
"qq": "578031621"
|
||||
},
|
||||
"declaration": {
|
||||
"ads": "无",
|
||||
"data": "插件不采集任何数据",
|
||||
"permissions": "摄像头权限"
|
||||
},
|
||||
"npmurl": "https://www.npmjs.com/package/jz-h5-scancode",
|
||||
"darkmode": "√",
|
||||
"i18n": "x",
|
||||
"widescreen": "√"
|
||||
},
|
||||
"uni_modules": {
|
||||
"encrypt": [],
|
||||
"platforms": {
|
||||
"cloud": {
|
||||
"tcb": "√",
|
||||
"aliyun": "√",
|
||||
"alipay": "x"
|
||||
},
|
||||
"client": {
|
||||
"uni-app": {
|
||||
"vue": {
|
||||
"vue2": "√",
|
||||
"vue3": "√"
|
||||
},
|
||||
"web": {
|
||||
"safari": "√",
|
||||
"chrome": "√"
|
||||
},
|
||||
"app": {
|
||||
"vue": "√",
|
||||
"nvue": "-",
|
||||
"android": "-",
|
||||
"ios": "-",
|
||||
"harmony": "-"
|
||||
},
|
||||
"mp": {
|
||||
"weixin": "x",
|
||||
"alipay": "x",
|
||||
"toutiao": "x",
|
||||
"baidu": "x",
|
||||
"kuaishou": "x",
|
||||
"jd": "x",
|
||||
"harmony": "x",
|
||||
"qq": "x",
|
||||
"lark": "x"
|
||||
},
|
||||
"quickapp": {
|
||||
"huawei": "x",
|
||||
"union": "x"
|
||||
}
|
||||
},
|
||||
"uni-app-x": {
|
||||
"web": {
|
||||
"safari": "-",
|
||||
"chrome": "-"
|
||||
},
|
||||
"app": {
|
||||
"android": "-",
|
||||
"ios": "-",
|
||||
"harmony": "-"
|
||||
},
|
||||
"mp": {
|
||||
"weixin": "-"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"main": "js/index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "JZ",
|
||||
"license": "MIT"
|
||||
}
|
||||