This commit is contained in:
oneao committed 2026-03-28 17:22:49 +08:00
1 parent ce466130c0
commit 9c026f00ad
36 files changed
+2230 -1036

No files matched your search

-1
View File
@@ -1,5 +1,4 @@
{
"editor.cursorSmoothCaretAnimation": "on",
// Disable the default formatter, use eslint instead
"prettier.enable": false,
"editor.formatOnSave": false,
+1 -1
View File
@@ -1,3 +1,3 @@
VITE_APP_PORT = 8081
VITE_APP_PORT = 8080
VITE_ALIYUN_BUCKET = 'g3hd'
+2 -1
View File
@@ -1,6 +1,7 @@
<script setup lang="ts">
import { onHide, onLaunch, onShow } from '@dcloudio/uni-app'
import { useUserStore } from './store'
import PLATFORM from './utils/platform'
const userStore = useUserStore()
@@ -14,7 +15,7 @@ onLaunch(() => {
})
})
onLoad(async (options) => {
onLoad(async (options) => {
})
@@ -1,8 +1,9 @@
<script lang="ts" setup>
import type { UploadFile, UploadFileItem } from 'sard-uniapp'
import { dialog, isImageUrl, isVideoUrl, toast } from 'sard-uniapp'
import { aliyunpluploadApi, deleteAliyunFileApi, deleteFileApi, getUniqueIdApi, recognizeOcrBoxApi, saveObjectApi } from '@/request/api'
import { downloadFile, getFileExtension, imageCompress, isImage, uploadFileToAliyun } from '@/utils/file'
import { aliyunpluploadApi, deleteAliyunFileApi, deleteFileApi, getUniqueIdApi, recognizeOcrBoxApi, saveObjectApi, uploadTempFileApi } from '@/request/api'
import { downloadFile, getFileExtension, imageCompress, isImage } from '@/utils/file'
import { uploadToOss } from '@/utils/oss'
const boxImage = ref(null)
@@ -24,11 +25,17 @@ const subFileList = ref([])
const tipContainerNo = ref('')
onLoad(async () => {
toast.loading('正在初始化中...', {
overlay: true,
})
await getUniqueIdApi().then((res) => {
if (res.success) {
form.value.b_id = res.data
}
})
toast.hide()
})
function handleChooseBox() {
@@ -46,14 +53,11 @@ function handleChooseBox() {
})
imageCompress(tempFile.path).then((compressRes) => {
uploadFileToAliyun(compressRes.path, tempFile.name, 'image').then((res) => {
if (res.url) {
form.value.containerImageUrl = res.url
ocrFinish.value = false
console.log(compressRes)
uploadTempFileApi(compressRes.path, tempFile.name).then((res) => {
if (res.success) {
recognizeOcrBoxApi({
fileUrl: form.value.containerImageUrl,
fileUrl: res.data.tempUrl,
}).then((ocrRes) => {
if (ocrRes.success) {
toast.success('识别成功,请检查箱号是否正确')
@@ -132,31 +136,6 @@ function beforeRead(file: UploadFile): boolean | Promise<UploadFile> {
})
}
async function afterRead(fileItem: UploadFileItem) {
// 设置上传中状态
fileItem.status = 'uploading'
fileItem.message = '正在上传'
fileItem.isUpload = false
// 上传到阿里云
const res = await uploadFileToAliyun(
fileItem.file.path,
fileItem.file.name,
fileItem.file.type,
)
if (!res.url)
throw new Error('上传失败:未获取到文件 URL')
// 上传完成
fileItem.status = 'done'
fileItem.message = '上传成功'
fileItem.url = res.url
subFileList.value = [...subFileList.value]
}
function beforeRemove(index: number, fileItem: UploadFileItem): boolean | Promise<void> {
console.log(fileItem)
@@ -172,15 +151,13 @@ function beforeRemove(index: number, fileItem: UploadFileItem): boolean | Promis
}
function onRemove(index: number, item: UploadFileItem) {
if (!item.isUpload) {
return
if (item.status === 'done') {
deleteAliyunFileApi(item.url).then((res) => {
if (res.success) {
toast.success('删除成功')
}
})
}
deleteAliyunFileApi(item.url).then((res) => {
if (res.success) {
toast.success('删除成功')
}
})
}
async function handleConfirm() {
@@ -204,74 +181,76 @@ async function handleConfirm() {
transparent: false,
})
// 图片已经上传成功,保存主表即可
const mainTable = {
b_id: form.value.b_id,
b_container_no: form.value.containerNo,
b_container_img: form.value.containerImageUrl,
b_ocr_no: form.value.ocrNo,
b_ocr_score: form.value.ocrScore || 0.00,
}
//
const fileInserts = []
// // 图片已经上传成功,保存主表即可
// const mainTable = {
// b_id: form.value.b_id,
// b_container_no: form.value.containerNo,
// b_container_img: form.value.containerImageUrl,
// b_ocr_no: form.value.ocrNo,
// b_ocr_score: form.value.ocrScore || 0.00,
// }
const containerNo = form.value.containerNo
// const fileInserts = []
subFileList.value.forEach(async (fileItem, index) => {
const uniqueRes = await getUniqueIdApi()
const indexNumber = index + 2
// const containerNo = form.value.containerNo
fileInserts.push({
subid: uniqueRes.data,
father: form.value.b_id,
mx_xh: indexNumber,
mx_filename: buildFileName(containerNo, indexNumber, fileItem.file.name),
mx_filesize: fileItem.file.size,
mx_fileext: getFileExtension(fileItem.file.name),
mx_mapfilename: fileItem.url,
mx_moduleid: 'b_container',
mx_cate_id: '1',
})
})
// subFileList.value.forEach(async (fileItem, index) => {
// const uniqueRes = await getUniqueIdApi()
// const indexNumber = index + 2
const uniqueRes = await getUniqueIdApi()
// fileInserts.push({
// subid: uniqueRes.data,
// father: form.value.b_id,
// mx_xh: indexNumber,
// mx_filename: buildFileName(containerNo, indexNumber, fileItem.file.name),
// mx_filesize: fileItem.file.size,
// mx_fileext: getFileExtension(fileItem.file.name),
// mx_mapfilename: fileItem.url,
// mx_moduleid: 'b_container',
// mx_cate_id: '1',
// })
// })
fileInserts.push({
subid: uniqueRes.data,
father: form.value.b_id,
mx_xh: 0,
mx_filename: buildFileName(containerNo, 1, boxImage.value.name),
mx_filesize: boxImage.value.size,
mx_fileext: getFileExtension(boxImage.value.name),
mx_mapfilename: form.value.containerImageUrl,
mx_moduleid: 'b_container',
mx_cate_id: '0',
})
// const uniqueRes = await getUniqueIdApi()
const reqData = [
{
tableName: 'b_container',
keyField: 'b_id',
inserts: [mainTable],
updates: [],
deletes: [],
},
{
tableName: 'bf_file',
keyField: 'subid',
inserts: fileInserts,
updates: [],
deletes: [],
},
]
// fileInserts.push({
// subid: uniqueRes.data,
// father: form.value.b_id,
// mx_xh: 0,
// mx_filename: buildFileName(containerNo, 1, boxImage.value.name),
// mx_filesize: boxImage.value.size,
// mx_fileext: getFileExtension(boxImage.value.name),
// mx_mapfilename: form.value.containerImageUrl,
// mx_moduleid: 'b_container',
// mx_cate_id: '0',
// })
saveObjectApi(reqData).then((res) => {
if (res.success) {
toast.success('保存成功')
tipContainerNo.value = form.value.containerNo
showResult.value = true
}
})
// const reqData = [
// {
// tableName: 'b_container',
// keyField: 'b_id',
// inserts: [mainTable],
// updates: [],
// deletes: [],
// },
// {
// tableName: 'bf_file',
// keyField: 'subid',
// inserts: fileInserts,
// updates: [],
// deletes: [],
// },
// ]
// saveObjectApi(reqData).then((res) => {
// if (res.success) {
// toast.success('保存成功')
// tipContainerNo.value = form.value.containerNo
// showResult.value = true
// }
// })
}
function buildFileName(containerNo, index, fileName) {
@@ -317,107 +296,18 @@ async function continueAdd() {
<template>
<view class="g3-container">
<template v-if="!showResult">
<wd-cell-group custom-class="group" title="集装箱号(OCR识别)" border use-slot>
<template #title>
<sar-icon name="close" size="32rpx" style="margin-right: 16rpx;"/>
<text>
集装箱号(OCR识别)
</text>
</template>
<view class="step-content box">
<view v-if="ocrFinish" class="header">
<sar-alert
v-if="!isError"
type="success"
show-icon
closable
:root-style="{
width: '100%',
}"
>
识别成功,请检查箱号是否正确!
<view>
准确率:{{ computedOcrScore }}
</view>
</sar-alert>
<sar-alert
v-else
type="danger"
show-icon
closable
:root-style="{
width: '100%',
}"
>
识别失败,请检查手动填写集装箱号
</sar-alert>
</view>
<view class="box-main">
<view v-if="!boxImage" class="box-placeholder" @click="handleChooseBox">
<sar-icon name="plus" size="156rpx" color="#999" />
<view class="g3-content">
<wd-cell-group custom-class="group" title="集装箱号(OCR识别)" border use-slot>
<template #title>
<view style="display:flex;align-items: center;">
<sar-icon family="iconfont" name="iconyuanbanben_OCR" size="38rpx" color="var(--sar-primary)" style="margin-right: 16rpx;" />
<text>
集装箱号(OCR识别)
</text>
</view>
<view v-else class="box-image">
<wd-img
:src="boxImage.path"
width="100%"
height="100%"
:enable-preview="true"
/>
<view class="delete-btn" @click="handleDeleteBoxImage()">
<sar-icon name="close" size="20rpx" color="#fff" />
</view>
</view>
</view>
<view class="box-input">
<view class="header">
<view class="label-container">
<view>集装箱号</view>
<view class="required-asterisk">
*
</view>
</view>
<view class="format">
格式: 4字母 + 7数字
</view>
</view>
<sar-input
v-model="form.containerNo"
placeholder="请输入箱号"
/>
</view>
</view>
</wd-cell-group>
<wd-cell-group custom-class="group" title="其他图片" border style="margin-top: 32rpx;">
<view class="step-content">
<sar-upload
v-model="subFileList"
multiple
:after-read="afterRead"
:before-read="beforeRead"
:before-remove="beforeRemove"
@item-click="onItemClick"
@remove="onRemove"
/>
</view>
</wd-cell-group>
<!-- <view class="g3-content">
<view class="step">
<view class="step-header">
<sar-tag theme="primary" size="large">
1
</sar-tag>
<view>集装箱号识别(OCR自动识别)</view>
</view>
</template>
<view class="step-content box">
<view v-if="ocrFinish" class="header ">
<view v-if="ocrFinish" class="header">
<sar-alert
v-if="!isError"
type="success"
@@ -447,10 +337,7 @@ async function continueAdd() {
<view class="box-main">
<view v-if="!boxImage" class="box-placeholder" @click="handleChooseBox">
<sar-icon name="image" size="56rpx" color="#999" />
<text class="placeholder-text">
上传箱号图片,自动识别箱号
</text>
<sar-icon name="plus" size="156rpx" color="#999" />
</view>
<view v-else class="box-image">
@@ -485,35 +372,36 @@ async function continueAdd() {
/>
</view>
</view>
</view>
</wd-cell-group>
<view class="step">
<view class="step-header">
<sar-tag theme="primary" mark="right" size="large">
2、其它照片录入
</sar-tag>
</view>
<wd-cell-group custom-class="group" border use-slot style="margin-top: 32rpx;">
<template #title>
<view style="display:flex;align-items: center;">
<sar-icon family="iconfont" name="tupian" size="38rpx" color="var(--sar-primary)" style="margin-right: 16rpx;" />
<text>
其他图片
</text>
</view>
</template>
<view class="step-content">
<sar-upload
v-model="subFileList"
multiple
:after-read="afterRead"
:before-read="beforeRead"
:before-remove="beforeRemove"
@item-click="onItemClick"
@remove="onRemove"
/>
</view>
</view>
</wd-cell-group>
</view>
<view class="g3-footer">
<view class="footer-main">
<view class="submit-btn" @click="handleConfirm">
确认上传
</view>
<sar-button round root-style="width:100%" icon-family="iconfont" icon="fasongtijiao" @click="handleConfirm">
上传
</sar-button>
</view>
</view> -->
</view>
</template>
<view v-else class="g3-result">
@@ -526,7 +414,7 @@ async function continueAdd() {
</text>
</view>
</template>
<sar-button long style="width: 360rpx;font-size: 38rpx;" @click="continueAdd">
<sar-button long style="font-size: 38rpx;" @click="continueAdd">
继续录入
</sar-button>
</sar-result>
@@ -0,0 +1,627 @@
<script lang="ts" setup>
import type { UploadFile, UploadFileItem } from 'sard-uniapp'
import { dialog, isImageUrl, isVideoUrl, toast } from 'sard-uniapp'
import { aliyunpluploadApi, deleteAliyunFileApi, deleteFileApi, getUniqueIdApi, recognizeOcrBoxApi, saveObjectApi, uploadTempFileApi } from '@/request/api'
import { downloadFile, getFileExtension, imageCompress, isImage } from '@/utils/file'
import { uploadToOss } from '@/utils/oss'
const boxImage = ref(null)
const ocrFinish = ref(false)
const isError = ref(false)
const showResult = ref(false)
const form = ref({
b_id: 0,
containerNo: '',
containerImageUrl: '',
ocrScore: 0.00,
ocrNo: '',
})
const subFileList = ref([])
const tipContainerNo = ref('')
onLoad(async () => {
toast.loading('正在初始化中...', {
overlay: true,
})
await getUniqueIdApi().then((res) => {
if (res.success) {
form.value.b_id = res.data
}
})
toast.hide()
})
function handleChooseBox() {
uni.chooseImage({
count: 1,
success: (res) => {
if (Array.isArray(res.tempFiles) && res.tempFiles.length > 0) {
const tempFile: any = res.tempFiles[0]
boxImage.value = tempFile
toast.loading('OCR识别中,请稍等 . . .', {
overlay: true,
transparent: false,
})
imageCompress(tempFile.path).then((compressRes) => {
console.log(compressRes)
// uploadTempFileApi(compressRes.path, tempFile.name).then((res) => {
// console.log(res)
// })
uploadToOss(compressRes.path, tempFile.name, 'image').then((res) => {
if (res.url) {
form.value.containerImageUrl = res.url
ocrFinish.value = false
recognizeOcrBoxApi({
fileUrl: form.value.containerImageUrl,
}).then((ocrRes) => {
if (ocrRes.success) {
toast.success('识别成功,请检查箱号是否正确')
const score = Number(ocrRes?.data?.score ?? 0)
form.value.ocrScore = Math.floor(score * 100) / 100
form.value.containerNo = ocrRes.data.boxNumber
form.value.ocrNo = ocrRes.data.boxNumber
isError.value = false
}
}).catch(() => {
isError.value = true
}).finally(() => {
ocrFinish.value = true
})
}
})
})
}
},
})
}
const computedOcrScore = computed(() => {
return `${(Math.floor(form.value.ocrScore * 100)).toFixed(0)}%`
})
function resetForm() {
boxImage.value = null
form.value.containerImageUrl = ''
form.value.containerNo = ''
form.value.ocrNo = ''
form.value.ocrScore = 0.00
}
function handleDeleteBoxImage() {
if (!form.value.containerImageUrl) {
resetForm()
return
}
toast.loading('正在删除中 . . .', {
overlay: true,
transparent: false,
})
deleteAliyunFileApi(form.value.containerImageUrl).then((res) => {
if (res.success) {
resetForm()
toast.success('删除成功')
}
})
}
function onItemClick(fileItem: UploadFileItem) {
console.log(fileItem)
if (!isImageUrl(fileItem.mx_mapfilename) && !isVideoUrl(fileItem.mx_mapfilename)) {
downloadFile(fileItem.mx_mapfilename, fileItem.name)
}
}
function beforeRead(file: UploadFile): boolean | Promise<UploadFile> {
// 非图片直接允许上传
if (!isImage(file.name))
return true
// 图片异步压缩,返回修改后的 UploadFile 对象
return imageCompress(file.path).then((res) => {
file.path = res.path
file.size = res.size
return file
}).catch((err) => {
console.error('图片压缩失败:', err)
return file
})
}
async function afterRead(fileItem: UploadFileItem) {
// 设置上传中状态
fileItem.status = 'uploading'
fileItem.message = '正在上传'
fileItem.isUpload = false
try {
// 上传到阿里云
const res = await uploadToOss(
fileItem.file.path,
fileItem.file.name,
fileItem.file.type,
)
if (!res.url)
throw new Error('上传失败:未获取到文件 URL')
// 上传完成
fileItem.status = 'done'
fileItem.message = '上传成功'
fileItem.url = res.url
subFileList.value = [...subFileList.value]
}
catch (e) {
// 上传完成
fileItem.status = 'failed'
fileItem.message = '上传失败'
subFileList.value = [...subFileList.value]
}
}
function beforeRemove(index: number, fileItem: UploadFileItem): boolean | Promise<void> {
console.log(fileItem)
return new Promise((resolve, reject) => {
dialog.confirm({
title: '提示',
message: `确定删除该张图片吗?`,
onConfirm: () => resolve(),
onCancel: () => reject(),
onClose: () => reject(),
})
})
}
function onRemove(index: number, item: UploadFileItem) {
if (item.status === 'done') {
deleteAliyunFileApi(item.url).then((res) => {
if (res.success) {
toast.success('删除成功')
}
})
}
}
async function handleConfirm() {
if (form.value.containerImageUrl === '') {
toast.fail('集装箱号图片不能为空')
return
}
if (form.value.containerNo === '') {
toast.fail('集装箱号不能为空')
return
}
if (!validateContainerNo(form.value.containerNo)) {
toast.fail('集装箱号不符合规则,请检查')
return
}
toast.loading('正在保存中 . . .', {
overlay: true,
transparent: false,
})
// 图片已经上传成功,保存主表即可
const mainTable = {
b_id: form.value.b_id,
b_container_no: form.value.containerNo,
b_container_img: form.value.containerImageUrl,
b_ocr_no: form.value.ocrNo,
b_ocr_score: form.value.ocrScore || 0.00,
}
const fileInserts = []
const containerNo = form.value.containerNo
subFileList.value.forEach(async (fileItem, index) => {
const uniqueRes = await getUniqueIdApi()
const indexNumber = index + 2
fileInserts.push({
subid: uniqueRes.data,
father: form.value.b_id,
mx_xh: indexNumber,
mx_filename: buildFileName(containerNo, indexNumber, fileItem.file.name),
mx_filesize: fileItem.file.size,
mx_fileext: getFileExtension(fileItem.file.name),
mx_mapfilename: fileItem.url,
mx_moduleid: 'b_container',
mx_cate_id: '1',
})
})
const uniqueRes = await getUniqueIdApi()
fileInserts.push({
subid: uniqueRes.data,
father: form.value.b_id,
mx_xh: 0,
mx_filename: buildFileName(containerNo, 1, boxImage.value.name),
mx_filesize: boxImage.value.size,
mx_fileext: getFileExtension(boxImage.value.name),
mx_mapfilename: form.value.containerImageUrl,
mx_moduleid: 'b_container',
mx_cate_id: '0',
})
const reqData = [
{
tableName: 'b_container',
keyField: 'b_id',
inserts: [mainTable],
updates: [],
deletes: [],
},
{
tableName: 'bf_file',
keyField: 'subid',
inserts: fileInserts,
updates: [],
deletes: [],
},
]
saveObjectApi(reqData).then((res) => {
if (res.success) {
toast.success('保存成功')
tipContainerNo.value = form.value.containerNo
showResult.value = true
}
})
}
function buildFileName(containerNo, index, fileName) {
const formattedIndex = String(index).padStart(2, '0')
return `${containerNo}_${formattedIndex}.${fileName.split('.').pop()}`
}
// 集装箱号校验
function validateContainerNo(containerNo) {
// 正则表达式: 4个字母 + 7个数字
const pattern = /^[A-Z]{4}\d{7}$/i
// 使用正则进行匹配
if (pattern.test(containerNo)) {
return true
}
else {
return false
}
}
async function continueAdd() {
const idRes = await getUniqueIdApi()
if (idRes.success) {
boxImage.value = null
ocrFinish.value = false
isError.value = false
resetForm()
form.value.b_id = idRes.data
subFileList.value = []
showResult.value = false
tipContainerNo.value = ''
}
}
</script>
<template>
<view class="g3-container">
<template v-if="!showResult">
<view class="g3-content">
<wd-cell-group custom-class="group" title="集装箱号(OCR识别)" border use-slot>
<template #title>
<view style="display:flex;align-items: center;">
<sar-icon family="iconfont" name="iconyuanbanben_OCR" size="38rpx" color="var(--sar-primary)" style="margin-right: 16rpx;" />
<text>
集装箱号(OCR识别)
</text>
</view>
</template>
<view class="step-content box">
<view v-if="ocrFinish" class="header">
<sar-alert
v-if="!isError"
type="success"
show-icon
closable
:root-style="{
width: '100%',
}"
>
识别成功,请检查箱号是否正确!
<view>
准确率:{{ computedOcrScore }}
</view>
</sar-alert>
<sar-alert
v-else
type="danger"
show-icon
closable
:root-style="{
width: '100%',
}"
>
识别失败,请检查手动填写集装箱号
</sar-alert>
</view>
<view class="box-main">
<view v-if="!boxImage" class="box-placeholder" @click="handleChooseBox">
<sar-icon name="plus" size="156rpx" color="#999" />
</view>
<view v-else class="box-image">
<wd-img
:src="boxImage.path"
width="100%"
height="100%"
:enable-preview="true"
/>
<view class="delete-btn" @click="handleDeleteBoxImage()">
<sar-icon name="close" size="20rpx" color="#fff" />
</view>
</view>
</view>
<view class="box-input">
<view class="header">
<view class="label-container">
<view>集装箱号</view>
<view class="required-asterisk">
*
</view>
</view>
<view class="format">
格式: 4字母 + 7数字
</view>
</view>
<sar-input
v-model="form.containerNo"
placeholder="请输入箱号"
/>
</view>
</view>
</wd-cell-group>
<wd-cell-group custom-class="group" border use-slot style="margin-top: 32rpx;">
<template #title>
<view style="display:flex;align-items: center;">
<sar-icon family="iconfont" name="tupian" size="38rpx" color="var(--sar-primary)" style="margin-right: 16rpx;" />
<text>
其他图片
</text>
</view>
</template>
<view class="step-content">
<sar-upload
v-model="subFileList"
multiple
:after-read="afterRead"
:before-read="beforeRead"
:before-remove="beforeRemove"
@item-click="onItemClick"
@remove="onRemove"
/>
</view>
</wd-cell-group>
</view>
<view class="g3-footer">
<view class="footer-main">
<sar-button round root-style="width:100%" icon-family="iconfont" icon="fasongtijiao" @click="handleConfirm">
上传
</sar-button>
</view>
</view>
</template>
<view v-else class="g3-result">
<sar-result status="success" title="操作成功" description="已成功录入箱号 为 xxx 的记录">
<template #description>
<view class="result-tip">
已成功录入箱号
<text class="container-no">
"{{ tipContainerNo }}"
</text>
</view>
</template>
<sar-button long style="font-size: 38rpx;" @click="continueAdd">
继续录入
</sar-button>
</sar-result>
</view>
</view>
</template>
<style lang="scss" scoped>
page {
background-color: #f8f9fa;
}
.g3-result {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
.result-tip {
font-size: 36rpx;
}
.container-no {
font-weight: 600;
color: #4caf50;
}
}
.g3-container {
display: flex;
flex-direction: column;
height: 100%;
.g3-content {
flex: 1;
overflow-y: auto;
padding: 16rpx 0rpx;
}
.g3-footer {
width: 100%;
.footer-main {
padding: 16rpx;
}
}
}
.step {
margin-bottom: 32rpx;
.step-header {
display: flex;
align-items: center;
margin-bottom: 24rpx;
}
}
.step-content {
padding: 32rpx;
}
.box {
display: flex;
flex-direction: column;
.box-main {
height: 326rpx;
}
.box-placeholder {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
background: var(--sar-upload-select-bg);
border-radius: var(--sar-upload-select-border-radius);
.placeholder-text {
color: #999;
font-size: 28rpx;
margin-top: 8rpx;
}
}
.box-image {
width: 100%;
height: 100%;
position: relative;
.delete-btn {
position: absolute;
top: 12rpx;
right: 12rpx;
width: 44rpx;
height: 44rpx;
border-radius: 50%;
background: rgba(0, 0, 0, 0.5);
color: #fff;
font-size: 28rpx;
font-weight: bold;
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
}
}
.box-input {
margin-top: 16rpx;
display: flex;
flex-direction: column;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 28rpx;
margin-bottom: 12rpx;
width: 100%;
.label-container {
position: relative;
}
.required-asterisk {
position: absolute;
top: -5rpx;
right: -12rpx;
color: red;
font-size: 24rpx;
}
.format {
color: #999;
font-size: 24rpx;
}
.tip {
color: #999;
}
}
}
.submit-btn {
width: 100%;
height: 72rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 999px;
font-size: 30rpx;
font-weight: 600;
color: #fff;
background: linear-gradient(135deg, #3a6ee8, #5c8df6);
box-shadow: 0 6rpx 12rpx rgba(58, 110, 232, 0.18);
transition: all 0.15s;
}
.submit-btn:active {
transform: scale(0.98);
box-shadow: 0 3rpx 6rpx rgba(58, 110, 232, 0.15);
}
</style>
+1 -1
View File
@@ -60,7 +60,7 @@
"layout": "default",
"style": {
"navigationBarTitleText": "个人中心",
"navigationStyle": "default"
"navigationStyle": "custom"
}
}
],
@@ -1,152 +1,269 @@
<script setup lang="ts">
import { deleteFileApi } from '@/request/api'
import { toast } from 'sard-uniapp'
import { ref } from 'vue'
import { loadPageDataApi } from '@/request/api'
import { useUserStore } from '@/store'
import { deepEqual, groupFieldsBySpan } from '@/utils/common'
import { getCurrentDate } from '@/utils/time'
const userStore = useUserStore()
onShow(() => {
userStore.requireLogin()
const pagingRef = ref<any>(null)
const dataList = ref<any[]>([])
const itemFields = [
{ label: '录入时间', value: item => item.b_inputdatetime, span: 12 },
{ label: '文件数量', value: item => item.v_file_count, span: 12 },
]
const groupedFields = groupFieldsBySpan(itemFields)
// 搜索关键字
const searchValue = ref('')
// onShow(() => {
// pagingRef.value?.reload()
// })
onMounted(() => {
pagingRef.value?.reload()
})
const swiperCurrent = ref(0)
const swiperList = [
{},
{},
{},
]
function handleToLogin() {
uni.reLaunch({
url: '/pages-sub/login/index',
})
// 搜索
function handleSearch() {
pagingRef.value.reload()
}
function toLinkUrl(url) {
if (!url) {
return
// 模拟分页请求
function queryList(pageNo: number, pageSize: number) {
toast.loading('正在加载中')
const userid = userStore.userInfo.userid
let searchCondition = `2 > 1 AND b_inputuser_id = '${userid}'`
if (searchValue.value) {
searchCondition += `b_container_no LIKE '%${searchValue.value}%' `
}
uni.navigateTo({
url,
loadPageDataApi({
viewName: 'v_container',
searchCondition,
page: pageNo,
size: pageSize,
orderField: 'b_inputdatetime desc',
})
.then((res) => {
const rows = res.data.rows || []
console.log(rows)
if (rows) {
pagingRef.value.complete(rows)
}
})
.finally(() => {
toast.hide()
})
}
function deleteAliyunFiles() {
const subids = [
787154320330821,
787154320388165,
]
subids.forEach((subid) => {
deleteFileApi(subid)
function handleToFile(itemData) {
const searchids = [itemData.b_id, itemData.father]
.filter(Boolean)
.join(',')
uni.navigateTo({
url: `/pages-sub/file/index?moduleid=xc_conyard_in&father=${itemData.father}&searchids=${encodeURIComponent(searchids)}&cateid=016&edit=1`,
})
}
</script>
<template>
<view>
<view class="swiper-container">
<swiper
:current="swiperCurrent"
autoplay
:interval="3000"
class="swiper"
@change="swiperCurrent = $event.detail.current"
<z-paging
ref="pagingRef" v-model="dataList"
auto-show-back-to-top
@query="queryList"
>
<template #top>
<view style="border-bottom: 1px solid #ddd;">
<sar-search
v-model="searchValue"
placeholder="请输入箱号搜索"
@search="handleSearch"
/>
</view>
</template>
<!-- 列表内容 -->
<view
v-for="(item, index) in dataList"
:key="index"
class="list-item"
>
<swiper-item>
<image
src="@/static/images/bg.jpg"
style="width: 100%;height:100%"
/>
</swiper-item>
<swiper-item>
<image
src="@/static/images/bg1.jpg"
style="width: 100%;height:100%"
/>
</swiper-item>
<swiper-item>
<image
src="@/static/images/bg2.jpg"
style="width: 100%;height:100%"
/>
</swiper-item>
</swiper>
<sar-swiper-dot
:current="swiperCurrent"
:list="swiperList"
/>
</view>
<view v-if="userStore.isLogined" class="grid-container">
<sar-grid :columns="1" gap="24rpx">
<sar-grid-item
@click="toLinkUrl('/pages-sub/upload/index')"
>
<view class="grid-item">
<image src="/static/images/home/waybill.png" />
<text>集装箱装货采集</text>
<view class="list-item__header">
<view class="list-item__main">
<sar-image
:src="item.b_container_img"
width="160rpx"
height="160rpx"
/>
</view>
</sar-grid-item>
</sar-grid>
</view>
<sar-result v-else status="warning" title="尚未登陆" style="margin-top: 32rpx;">
<sar-button
@click="handleToLogin"
>
前去登录
</sar-button>
</sar-result>
</view>
<view class="list-item__body">
<sar-row>
<sar-col :span="12">
<view class="list-item__info">
<view class="list-item__label">
集装箱号:
</view>
<view class="list-item__value" style="font-weight: bold;color:var(--sar-primary)">
{{ item.b_container_no }}
</view>
</view>
</sar-col>
</sar-row>
<sar-row>
<sar-col :span="12">
<view class="list-item__info">
<view class="list-item__label">
录入时间:
</view>
<view class="list-item__value">
{{ item.b_inputdatetime }}
</view>
</view>
</sar-col>
</sar-row>
<sar-row>
<sar-col :span="12">
<view class="list-item__info">
<view class="list-item__label">
照片数量:
</view>
<view class="list-item__value">
{{ item.v_file_count }}
</view>
</view>
</sar-col>
</sar-row>
</view>
<view class="list-item__footer">
<!-- <sar-button inline size="small" square @click="handleToFile(item)">
拍照
</sar-button> -->
</view>
<view class="list-item__line" />
</view>
</z-paging>
</view>
</template>
<style lang="scss" scoped>
<style scoped lang="scss">
@import '@/style/mixins.scss';
page {
background-color: #f1f5f9;
}
.dialog-loading {
display: flex;
justify-content: center;
align-items: center;
height: 200rpx;
}
.swiper-container {
position: relative;
.swiper {
height: 400rpx;
width: 100%;
/* tab 栏 */
.tabs {
display: flex;
border-bottom: 2rpx solid #eee;
background-color: #fff;
.tabs__item {
flex: 1;
text-align: center;
padding: 16rpx 0;
font-size: 30rpx;
color: #888;
}
.tabs__item--active {
color: var(--sar-primary);
font-weight: bold;
border-bottom: 4rpx solid var(--sar-primary);
}
}
.grid-container {
padding-top: 32rpx;
/* 列表项 */
.list-item {
position: relative;
padding: 16rpx;
background-color: #fff;
&.list-item--desktop {
.list-item__header {
max-width: 1000rpx;
margin: 0 auto;
}
:deep(.sar-grid__content) {
background-color: #fff;
}
:deep(.sar-grid) {
row-gap: 0 !important;
}
:deep(.sar-grid__item) {
margin-bottom: 24rpx;
.list-item__body {
max-width: 1000rpx;
margin: 0 auto;
}
.list-item__footer {
max-width: 1000rpx;
margin: 0 auto;
}
}
:deep(.sar-grid__wrapper) {
box-shadow: 0 4rpx 8rpx rgba(0, 0, 0, 0.1);
}
:deep(.sar-badge) {
width: 42rpx;
height: 42rpx;
}
.grid-item {
.list-item__header {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: 36rpx;
image {
width: 128rpx;
height: 128rpx;
}
.list-item__body {
padding: 24rpx 0rpx;
display: flex;
flex-direction: column;
@include flexGap(16rpx, 'column');
.list-item__info {
display: flex;
align-items: start;
width: 100%;
font-size: 32rpx;
line-height: 1;
.list-item__label {
display: block;
min-width: 192rpx;
color: #4b5563;
text-align: right;
white-space: nowrap;
}
.list-item__value {
flex: 1;
color: #111827;
word-break: break-all;
}
}
text {
margin-top: 16rpx;
font-weight: 600;
color: #000;
}
.list-item__footer {
display: flex;
justify-content: flex-end;
@include flexGap(36rpx, '');
:deep(.sar-button) {
padding: 0rpx 48rpx;
font-size: 32rpx;
}
}
.list-item__line {
position: absolute;
bottom: 0rpx;
left: 0rpx;
height: 1px;
width: 100%;
background-color: #dcdcdc;
}
}
</style>
@@ -1,4 +1,4 @@
import { httpPost } from './index'
import { httpPost, httpUpload } from './index'
const aliyunBucket = import.meta.env.VITE_ALIYUN_BUCKET
@@ -16,7 +16,7 @@ interface GetaliyunkeyParams {
bucketName: string
}
export function getaliyunkeyApi(params: GetaliyunkeyParams) {
export function getAliyunkeyApi(params: GetaliyunkeyParams) {
return httpPost('/file/getAliyunKey', params)
}
@@ -92,3 +92,23 @@ interface UpdatePasswordParams {
export function updatePasswordApi(params: UpdatePasswordParams) {
return httpPost(`/auth/updatePassword`, params)
}
interface PageParams {
page: number
size: number
viewName: string
searchCondition?: string
orderField?: string
searchColumns?: string[]
}
export function loadPageDataApi(params: PageParams) {
return httpPost(
'/data/page',
params,
)
}
export function uploadTempFileApi(filePath: string, fileName: string) {
return httpUpload('/file/uploadTemp', filePath, fileName)
}
+252 -205
View File
@@ -5,174 +5,260 @@ import { useUserStore } from '@/store'
import { joinUrl } from '@/utils/common'
import { isH5 } from '@/utils/platform'
function request<T = any>(option: UniApp.RequestOptions): Promise<IResData<T>> & { abort: () => void } {
/**
* ======================
* 读取机构配置
* ======================
*/
function readDbJson(): Promise<Record<string, string>> {
return new Promise((resolve, reject) => {
if (isH5) {
const timestamp = Date.now()
uni.request({
url: `/g3ocr-app/static/db.json?t=${timestamp}`,
method: 'GET',
success: r => resolve(r.data as Record<string, string>),
fail: err => reject(err),
})
}
else {
resolve(dbJson as Record<string, string>)
}
})
}
/**
* ======================
* 获取 baseUrl + headers
* ======================
*/
async function getBaseConfig(option: UniApp.RequestOptions, isLogin: boolean) {
const dbRes = await readDbJson()
const userStore = useUserStore()
const { token, userid, orgid } = userStore.userInfo || {}
if (!dbRes || Object.keys(dbRes).length === 0) {
throw new Error('机构配置获取失败')
}
let reqUrl = ''
if (isLogin) {
const dataObj: any = typeof option.data === 'string'
? parseFormUrlencoded(option.data)
: option.data || {}
reqUrl = dbRes[dataObj.orgid || '']
}
else {
reqUrl = dbRes[orgid || '']
}
if (!reqUrl) {
handleInvalidOrg()
throw new Error('机构码错误')
}
const headers: Record<string, any> = { ...(option.header || {}) }
if (!isLogin) {
headers.Authorization = `Bearer ${token}`
headers.userid = encodeURIComponent(userid)
headers.orgid = orgid
}
return {
baseUrl: reqUrl,
headers,
}
}
/**
* ======================
* 统一错误处理
* ======================
*/
function handleInvalidOrg() {
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
const currentRoute = `/${currentPage.route}`
if (currentRoute !== '/pages-sub/login/index') {
uni.showToast({ title: '当前登录已过期,请重新登录', icon: 'none' })
uni.reLaunch({ url: '/pages-sub/login/index' })
}
else {
uni.showToast({ title: '机构码错误', icon: 'none' })
}
}
/**
* ======================
* 统一响应处理
* ======================
*/
function handleResponse<T>(
res: any,
resolve: (data: IResData<T>) => void,
reject: (err: any) => void,
aborted: boolean,
) {
if (aborted)
return reject(new Error('请求已取消'))
if (res.statusCode !== 200) {
toast.fail(`请求失败:${res.statusCode}`)
return reject(new Error(`HTTP错误: ${res.statusCode}`))
}
let data: IResData<T>
try {
data = typeof res.data === 'string'
? JSON.parse(res.data)
: res.data
if (data.data && typeof data.data === 'string') {
try {
data.data = JSON.parse(data.data)
}
catch {}
}
}
catch {
toast.fail('响应解析失败')
return reject(new Error('响应解析失败'))
}
// 401 优先处理(避免被后面拦截)
if (data.code === 401) {
uni.showToast({ title: '请重新登录', icon: 'none' })
handleInvalidOrg()
return reject(new Error('未授权'))
}
if (data.code === 200) {
data.success = true
resolve(data)
}
else {
toast.fail(data.message || '请求失败')
reject(new Error(data.message || '请求失败'))
}
}
/**
* ======================
* 主请求方法
* ======================
*/
function request<T = any>(
option: UniApp.RequestOptions,
): Promise<IResData<T>> & { abort: () => void } {
let task: UniApp.RequestTask | undefined
let aborted = false
const promise = new Promise<IResData<T>>((resolve, reject) => {
// 判断是否是登录请求
const promise = (async () => {
const isLogin = option.url?.includes('login')
const readDbJson = new Promise<Record<string, string>>((res, rej) => {
if (isH5) {
// H5:用 request 请求,带时间戳保证最新
const timestamp = Date.now()
uni.request({
url: `/g3ocr-app/static/db.json?t=${timestamp}`,
method: 'GET',
success: r => res(r.data as Record<string, string>),
fail: err => rej(err),
})
}
else {
// 非 H5 直接返回本地导入的 JSON
res(dbJson)
}
const { baseUrl, headers } = await getBaseConfig(option, isLogin)
option.url = joinUrl(baseUrl, option.url || '')
option.header = headers
return new Promise<IResData<T>>((resolve, reject) => {
task = uni.request({
...option,
success: res =>
handleResponse<T>(res, resolve, reject, aborted),
fail: (err) => {
if (aborted)
return reject(new Error('请求已取消'))
toast.fail('网络错误')
reject(err)
},
})
})
})()
readDbJson
.then((dbRes) => {
if (aborted)
return reject(new Error('请求已取消'))
const finalPromise = promise as Promise<IResData<T>> & { abort: () => void }
// 组织请求 URL
let reqUrl = ''
const userStore = useUserStore()
const { token, userid, orgid } = userStore.userInfo || {}
if (!dbRes || Object.keys(dbRes).length === 0) {
toast.fail('机构配置获取失败,请联系管理人员!')
return reject(new Error('机构配置获取失败,请联系管理人员!'))
}
if (isLogin) {
const dataObj: any = typeof option.data === 'string'
? parseFormUrlencoded(option.data)
: option.data || {}
const orgidFromData = dataObj.orgid || ''
reqUrl = dbRes[orgidFromData]
}
else {
reqUrl = dbRes[orgid || '']
}
if (!reqUrl) {
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
const currentRoute = `/${currentPage.route}`
if (currentRoute !== '/pages-sub/login/index') {
uni.showToast({ title: '当前登录已过期,请重新登录', icon: 'error' })
uni.reLaunch({
url: '/pages-sub/login/index',
})
}
else {
uni.showToast({ title: '机构码错误', icon: 'error' })
}
return reject(new Error('机构码错误'))
}
// 设置 headers
const headers: Record<string, any> = { ...(option.header || {}) }
if (!isLogin) {
headers.Authorization = `Bearer ${token}`
headers.userid = encodeURIComponent(userid)
headers.orgid = orgid
}
// 拼接完整 URL
option.url = joinUrl(reqUrl, option.url || '')
option.header = headers
// 发请求
task = uni.request({
...option,
url: option.url,
success: (res) => {
if (aborted)
return reject(new Error('请求已取消'))
if (res.statusCode !== 200) {
console.error('HTTP错误', res.statusCode, res.data)
toast.fail(`请求失败:${res.statusCode}`)
return reject(new Error(`HTTP错误: ${res.statusCode}`))
}
let data: IResData<T>
try {
// 解析最外层响应
data = typeof res.data === 'string' ? JSON.parse(res.data) : res.data
// 如果 data.data 是字符串,则解析成对象
if (data.data && typeof data.data === 'string') {
try {
data.data = JSON.parse(data.data)
}
catch {
// 如果解析失败,保持原始字符串即可
// console.warn('data.data 不是合法 JSON 字符串', data.data)
}
}
}
catch (err) {
console.log(`解析响应JSON失败:${err}`)
toast.fail('响应数据解析失败')
return reject(new Error('响应数据解析失败'))
}
if (data.code === 200) {
data.success = true
resolve(data)
}
else {
toast.fail(data.message || '请求错误')
reject(new Error(data.message || '请求错误'))
}
// 登录失效处理
if (data.code === 401) {
uni.showToast({ title: '请重新登录', icon: 'none' })
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
const currentRoute = `/${currentPage.route}`
if (currentRoute !== '/pages-sub/login/index') {
uni.reLaunch({
url: '/pages-sub/login/index',
})
}
}
},
fail: (err) => {
if (aborted)
return reject(new Error('请求已取消'))
toast.fail('网络请求失败,请联系管理人员')
reject(new Error(err.errMsg || '网络请求失败'))
},
})
})
.catch((err) => {
toast.fail('机构配置获取失败,请联系管理人员!')
console.error(err)
reject(new Error('机构配置获取失败,请联系管理人员!'))
})
}) as Promise<IResData<T>> & { abort: () => void }
promise.abort = () => {
finalPromise.abort = () => {
aborted = true
task?.abort()
}
return promise
return finalPromise
}
/**
* ======================
* 上传文件
* ======================
*/
export function httpUpload<T = any>(
url: string,
filePath: string,
fileName: string,
formData?: Record<string, any>,
option?: UniApp.UploadFileOption,
) {
let task: UniApp.UploadTask | undefined
let aborted = false
const promise = (async () => {
const isLogin = url.includes('login')
const { baseUrl, headers } = await getBaseConfig(
{ url, data: formData } as any,
isLogin,
)
const fullUrl = joinUrl(baseUrl, url)
const mergedFormData = {
...formData,
fileName,
}
return new Promise<IResData<T>>((resolve, reject) => {
task = uni.uploadFile({
url: fullUrl,
filePath,
name: 'file',
formData: mergedFormData,
header: {
...headers,
...(option?.header || {}),
},
success: res =>
handleResponse<T>(res, resolve, reject, aborted),
fail: (err) => {
if (aborted)
return reject(new Error('请求已取消'))
toast.fail('网络错误')
reject(err)
},
...option,
})
})
})()
const finalPromise = promise as Promise<IResData<T>> & { abort: () => void }
finalPromise.abort = () => {
aborted = true
task?.abort()
}
return finalPromise
}
/**
* ======================
* 工具函数
* ======================
*/
function parseFormUrlencoded(str: string) {
return str.split('&').reduce((acc, pair) => {
const [key, value] = pair.split('=')
@@ -181,39 +267,16 @@ function parseFormUrlencoded(str: string) {
}, {} as Record<string, string>)
}
// GET
request.get = <T = any>(url: string, data?: any, option?: UniApp.RequestOptions) => {
return request<T>({
method: 'GET',
url,
data,
...option,
})
}
/**
* ======================
* 方法封装
* ======================
*/
request.get = <T = any>(url: string, data?: any, option?: UniApp.RequestOptions) =>
request<T>({ method: 'GET', url, data, ...option })
// PUT
request.put = <T = any>(url: string, data?: any, option?: UniApp.RequestOptions) => {
return request<T>({
method: 'PUT',
url,
data,
...option,
})
}
// DELETE
request.delete = <T = any>(url: string, data?: any, option?: UniApp.RequestOptions) => {
return request<T>({
method: 'DELETE',
url,
data,
...option,
})
}
// POST JSON
request.postJson = <T = any>(url: string, data?: any, option?: UniApp.RequestOptions) => {
return request<T>({
request.postJson = <T = any>(url: string, data?: any, option?: UniApp.RequestOptions) =>
request<T>({
method: 'POST',
url,
data,
@@ -223,17 +286,10 @@ request.postJson = <T = any>(url: string, data?: any, option?: UniApp.RequestOpt
},
...option,
})
}
// POST 表单
request.postForm = <T = any>(url: string, data?: any, option?: UniApp.RequestOptions) => {
let payload: string
if (data) {
// 使用 qs.stringify 或 URLSearchParams
payload = qs.stringify(data, { arrayFormat: 'repeat' })
// 生成 tables=val1&tables=val2 格式
}
const payload = data ? qs.stringify(data, { arrayFormat: 'repeat' }) : undefined
return request<T>({
method: 'POST',
url,
@@ -247,16 +303,7 @@ request.postForm = <T = any>(url: string, data?: any, option?: UniApp.RequestOpt
}
export function httpPost<T = any>(url: string, data?: any, option?: UniApp.RequestOptions) {
return request<T>({
method: 'POST',
url,
data,
header: {
'Content-Type': 'application/json',
...(option?.header || {}),
},
...option,
})
return request.postJson<T>(url, data, option)
}
export default request
@@ -1,3 +1,3 @@
{
"g3hd": "http://192.168.5.6:80/api"
"g3hd": "http://192.168.1.137:80/api"
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

@@ -1,9 +1,9 @@
@font-face {
font-family: 'iconfont'; /* Project id 5142758 */
src:
url('data:application/x-font-woff2;charset=utf-8;base64,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')
url('data:application/x-font-woff2;charset=utf-8;base64,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')
format('woff2'),
url('//at.alicdn.com/t/c/font_5142758_pgr9uja0qt.ttf?t=1773887288142') format('truetype');
url('//at.alicdn.com/t/c/font_5142758_32yu5am87gx.ttf?t=1774661340978') format('truetype');
}
.iconfont {
@@ -14,18 +14,18 @@
-moz-osx-font-smoothing: grayscale;
}
.iconfont-tupian:before {
content: '\e865';
}
.iconfont-iconyuanbanben_OCR:before {
content: '\e6b6';
}
.iconfont-fasongtijiao:before {
content: '\e693';
}
.iconfont-tupian:before {
content: '\e8ba';
}
.iconfont-OCRshibie:before {
content: '\e658';
}
.iconfont-save:before {
content: '\e8c7';
}
@@ -5,7 +5,7 @@ page,
.sar-portal {
--sar-input-border-color: #ddd;
// --sar-primary: #34495e;
--sar-primary: #4d80f0;
--sar-primary: #0052d9;
--sar-rounded: 8rpx;
--sar-dialog-title-font-size: 42rpx;
--sar-dialog-message-font-size: 36rpx;
@@ -1,5 +1,5 @@
import { toast } from 'sard-uniapp'
import { getaliyunkeyApi } from '@/request/api'
import { getOssKey } from './oss'
import { isApp, isH5, isMpWeixin } from './platform'
const aliyunBucket = import.meta.env.VITE_ALIYUN_BUCKET
@@ -115,70 +115,12 @@ export function formatFileSize(size: number): string {
else return `${(size / (1024 * 1024 * 1024)).toFixed(2)} GB`
}
export interface AliyunUploadResult {
url: string // 上传后的完整 URL
}
/**
* 上传文件到阿里云 OSS
* @param filePath 本地文件路径
* @param fileName 文件名
* @param fileType 文件类型,可选:image/video/file
* @returns Promise<AliyunUploadResult>
* 上传文件到阿里云 OSS(优化版)
*/
export async function uploadFileToAliyun(
filePath: string,
fileName: string,
fileType: any,
): Promise<AliyunUploadResult> {
// 获取 OSS 临时上传凭证
const keyRes = await getaliyunkeyApi({
bucketName: aliyunBucket,
})
const { accessid, callback, dir, host, policy, signature } = keyRes.data
// 构建文件在 OSS 的存储路径
const fileExtension = fileName.split('.').pop()?.toLowerCase() || 'tmp'
const timestamp = Date.now()
const random = Math.floor(Math.random() * 1000)
const key = `${dir}${timestamp + random}.${fileExtension}`
const formData: Record<string, any> = {
key,
OSSAccessKeyId: accessid,
policy,
signature,
success_action_status: '200',
name: fileName,
}
if (callback)
formData.callback = callback
// 使用 Promise 包裹 uni.uploadFile
return new Promise<AliyunUploadResult>((resolve, reject) => {
uni.uploadFile({
url: host,
filePath,
name: 'file',
fileName,
fileType,
formData,
success: (res) => {
if (res.statusCode === 200) {
resolve({
url: `${host}/${key}`,
})
}
else {
reject(new Error(`上传失败,状态码: ${res.statusCode}`))
}
},
fail: (err) => {
reject(err)
},
})
})
}
/** =================== 工具函数 =================== */
/** 获取文件名:从 URL 提取最后一段 */
@@ -0,0 +1,116 @@
// utils/ossManager.ts
import { getAliyunkeyApi } from '@/request/api'
const aliyunBucket = import.meta.env.VITE_ALIYUN_BUCKET
interface OssKey {
accessid: string
callback?: string
dir: string
host: string
policy: string
signature: string
expire: number
}
interface OssUpdateResult {
url: string // 上传后的完整 URL
}
// ✅ 模块级缓存(天然单例)
let ossKeyCache: OssKey | null = null
let ossKeyPromise: Promise<OssKey> | null = null
/**
* 获取 OSS Key(缓存 + 并发锁)
*/
export async function getOssKey(): Promise<OssKey> {
const now = Math.floor(Date.now() / 1000)
const SAFE_GAP = 10 // 提前10秒过期
// ✅ 命中缓存
if (ossKeyCache && ossKeyCache.expire - SAFE_GAP > now) {
console.log('===命中缓存===')
return ossKeyCache
}
// ✅ 并发锁
if (ossKeyPromise) {
return ossKeyPromise
}
// ✅ 请求
ossKeyPromise = getAliyunkeyApi({
bucketName: aliyunBucket,
})
.then((res) => {
console.log('===重新请求===')
const data = res.data
ossKeyCache = {
accessid: data.accessid,
callback: data.callback,
dir: data.dir,
host: data.host,
policy: data.policy,
signature: data.signature,
expire: Number(data.expire),
}
return ossKeyCache
})
.finally(() => {
ossKeyPromise = null
})
return ossKeyPromise
}
export async function uploadToOss(
filePath: string,
fileName: string,
fileType?: 'image' | 'video' | 'file',
): Promise<OssUpdateResult> {
// ✅ 使用缓存后的 key
const { accessid, callback, dir, host, policy, signature } = await getOssKey()
// 生成文件路径
const ext = fileName.split('.').pop()?.toLowerCase() || 'tmp'
const key = `${dir}${Date.now()}_${Math.floor(Math.random() * 1000)}.${ext}`
const formData: Record<string, any> = {
key,
OSSAccessKeyId: accessid,
policy,
signature,
success_action_status: '200',
name: fileName,
}
if (callback) {
formData.callback = callback
}
return new Promise<OssUpdateResult>((resolve, reject) => {
uni.uploadFile({
url: host,
filePath,
name: 'file',
fileName,
fileType: 'image',
formData,
success: (res) => {
if (res.statusCode === 200) {
resolve({
url: `${host}/${key}`,
})
}
else {
reject(new Error(`上传失败,状态码: ${res.statusCode}`))
}
},
fail: reject,
})
})
}
@@ -1,9 +1,3 @@
/*
* @Author: 菲鸽
* @Date: 2024-03-28 19:13:55
* @Last Modified by: 菲鸽
* @Last Modified time: 2024-03-28 19:24:55
*/
export const platform = __UNI_PLATFORM__
export const isH5 = __UNI_PLATFORM__ === 'h5'
export const isApp = __UNI_PLATFORM__ === 'app'
@@ -13,6 +7,11 @@ export const isMpAplipay = __UNI_PLATFORM__.startsWith('mp-alipay')
export const isMpToutiao = __UNI_PLATFORM__.startsWith('mp-toutiao')
export const isHarmony = __UNI_PLATFORM__.startsWith('app-harmony')
function isInWeChat() {
const ua = navigator.userAgent.toLowerCase()
return ua.includes('micromessenger')
}
const PLATFORM = {
platform,
isH5,
@@ -22,5 +21,6 @@ const PLATFORM = {
isMpAplipay,
isMpToutiao,
isHarmony,
isInWeChat
}
export default PLATFORM