This commit is contained in:
parent
a32354f941
commit
a6c639dd43
|
|
@ -138,6 +138,7 @@
|
|||
import { ref, reactive, getCurrentInstance, nextTick } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { addFault } from '@/services/trunk'
|
||||
import { getPresignUrls, uploadToCos } from '@/services/cos'
|
||||
import { addWatermark } from '@/utils/watermark'
|
||||
|
||||
const statusBarHeight = uni.getSystemInfoSync().statusBarHeight || 0
|
||||
|
|
@ -311,10 +312,10 @@ async function handleSubmit() {
|
|||
|
||||
if (submitting.value) return
|
||||
submitting.value = true
|
||||
uni.showLoading({ title: '提交中...', mask: true })
|
||||
uni.showLoading({ title: '处理图片中...', mask: true })
|
||||
|
||||
try {
|
||||
// 水印处理
|
||||
// 1. 水印处理
|
||||
const watermarkLines = [
|
||||
`${form.faultTime} ${form.personnel}`,
|
||||
`故障原因:${form.faultReason || ''}`,
|
||||
|
|
@ -332,35 +333,39 @@ async function handleSubmit() {
|
|||
}
|
||||
})
|
||||
watermarkedPhotos.push(result)
|
||||
// 每张图处理完后等一下,让 canvas 状态重置
|
||||
await nextTick()
|
||||
} catch (err) {
|
||||
watermarkedPhotos.push(photo)
|
||||
}
|
||||
}
|
||||
|
||||
// 构建上传数据
|
||||
const files = watermarkedPhotos.map((path, index) => ({
|
||||
name: 'images',
|
||||
uri: path
|
||||
}))
|
||||
// 2. 获取 COS 预签名 URL
|
||||
uni.showLoading({ title: '准备上传...', mask: true })
|
||||
const presignList = await getPresignUrls(watermarkedPhotos.length, '.jpg')
|
||||
|
||||
const formData = {
|
||||
files,
|
||||
data: {
|
||||
cableId: cableId.value,
|
||||
faultTime: form.faultTime,
|
||||
personnel: form.personnel,
|
||||
faultReason: form.faultReason,
|
||||
mileage: form.mileage,
|
||||
mileageCorrection: form.mileageCorrection,
|
||||
latitude: String(form.latitude),
|
||||
longitude: String(form.longitude),
|
||||
remark: form.remark
|
||||
}
|
||||
// 3. 逐张直传到 COS
|
||||
const imageUrls = []
|
||||
for (let i = 0; i < watermarkedPhotos.length; i++) {
|
||||
uni.showLoading({ title: `上传图片 ${i + 1}/${watermarkedPhotos.length}`, mask: true })
|
||||
await uploadToCos(presignList[i].presignUrl, watermarkedPhotos[i])
|
||||
imageUrls.push(presignList[i].accessUrl)
|
||||
}
|
||||
|
||||
const res = await addFault(formData)
|
||||
// 4. 提交故障表单(JSON)
|
||||
uni.showLoading({ title: '提交中...', mask: true })
|
||||
const res = await addFault({
|
||||
cableId: Number(cableId.value),
|
||||
faultTime: form.faultTime,
|
||||
personnel: form.personnel,
|
||||
faultReason: form.faultReason,
|
||||
mileage: form.mileage,
|
||||
mileageCorrection: form.mileageCorrection,
|
||||
latitude: Number(form.latitude),
|
||||
longitude: Number(form.longitude),
|
||||
remark: form.remark,
|
||||
imageUrls
|
||||
})
|
||||
|
||||
if (res.code === 200) {
|
||||
uni.showToast({ title: '提交成功', icon: 'success' })
|
||||
setTimeout(() => {
|
||||
|
|
@ -370,7 +375,8 @@ async function handleSubmit() {
|
|||
uni.showToast({ title: res.msg || '提交失败', icon: 'none' })
|
||||
}
|
||||
} catch (err) {
|
||||
uni.showToast({ title: '网络异常,请重试', icon: 'none' })
|
||||
console.error('[fault-add] 提交失败:', err)
|
||||
uni.showToast({ title: err.message || '网络异常,请重试', icon: 'none' })
|
||||
} finally {
|
||||
uni.hideLoading()
|
||||
submitting.value = false
|
||||
|
|
|
|||
93
odf-uniapp/services/cos.js
Normal file
93
odf-uniapp/services/cos.js
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
import { post } from './api'
|
||||
|
||||
/**
|
||||
* 从后端获取 COS 预签名上传 URL
|
||||
* @param {number} count - 文件数量
|
||||
* @param {string} ext - 文件扩展名
|
||||
* @returns {Promise<Array<{cosKey, presignUrl, accessUrl}>>}
|
||||
*/
|
||||
export async function getPresignUrls(count, ext = '.jpg') {
|
||||
const res = await post('/business/CosUpload/presignUrl', { count, ext })
|
||||
if (res.code !== 200) {
|
||||
throw new Error(res.msg || '获取上传地址失败')
|
||||
}
|
||||
return res.data
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过预签名 URL 直传文件到 COS
|
||||
* @param {string} presignUrl - PUT 预签名 URL
|
||||
* @param {string} filePath - 本地文件路径
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export function uploadToCos(presignUrl, filePath) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// #ifdef H5
|
||||
_uploadH5(presignUrl, filePath).then(resolve).catch(reject)
|
||||
// #endif
|
||||
// #ifdef APP-PLUS
|
||||
_uploadApp(presignUrl, filePath).then(resolve).catch(reject)
|
||||
// #endif
|
||||
})
|
||||
}
|
||||
|
||||
// #ifdef H5
|
||||
async function _uploadH5(presignUrl, filePath) {
|
||||
// H5 端 filePath 可能是 base64 或 blob URL
|
||||
let blob
|
||||
if (filePath.startsWith('data:')) {
|
||||
const resp = await fetch(filePath)
|
||||
blob = await resp.blob()
|
||||
} else {
|
||||
const resp = await fetch(filePath)
|
||||
blob = await resp.blob()
|
||||
}
|
||||
const res = await fetch(presignUrl, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'image/jpeg' },
|
||||
body: blob
|
||||
})
|
||||
if (!res.ok) {
|
||||
throw new Error(`COS上传失败: ${res.status}`)
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
|
||||
// #ifdef APP-PLUS
|
||||
function _uploadApp(presignUrl, filePath) {
|
||||
return new Promise((resolve, reject) => {
|
||||
plus.io.resolveLocalFileSystemURL(filePath, (entry) => {
|
||||
entry.file((file) => {
|
||||
const reader = new plus.io.FileReader()
|
||||
reader.onloadend = (e) => {
|
||||
const base64 = e.target.result
|
||||
// 将 base64 转为 ArrayBuffer
|
||||
const binary = atob(base64.split(',')[1])
|
||||
const len = binary.length
|
||||
const bytes = new Uint8Array(len)
|
||||
for (let i = 0; i < len; i++) {
|
||||
bytes[i] = binary.charCodeAt(i)
|
||||
}
|
||||
// 使用 XMLHttpRequest 发送 PUT
|
||||
const xhr = new XMLHttpRequest()
|
||||
xhr.open('PUT', presignUrl, true)
|
||||
xhr.setRequestHeader('Content-Type', 'image/jpeg')
|
||||
xhr.onload = () => {
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
resolve()
|
||||
} else {
|
||||
reject(new Error(`COS上传失败: ${xhr.status}`))
|
||||
}
|
||||
}
|
||||
xhr.onerror = () => reject(new Error('COS上传网络错误'))
|
||||
xhr.send(bytes.buffer)
|
||||
}
|
||||
reader.onerror = () => reject(new Error('读取文件失败'))
|
||||
reader.readAsDataURL(file)
|
||||
})
|
||||
}, (err) => {
|
||||
reject(new Error('解析文件路径失败: ' + JSON.stringify(err)))
|
||||
})
|
||||
})
|
||||
}
|
||||
// #endif
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
import { get, post, BASE_URL } from './api'
|
||||
import store from '@/store'
|
||||
import { get, post } from './api'
|
||||
|
||||
export const getCableList = (deptId) =>
|
||||
get('/business/OdfCables/list', { deptId })
|
||||
|
|
@ -11,36 +10,11 @@ export const getFaultDetail = (id) =>
|
|||
get(`/business/OdfCableFaults/${id}`)
|
||||
|
||||
/**
|
||||
* 新增故障(multipart/form-data,含图片上传)
|
||||
* @param {FormData|object} formData - 包含故障信息和图片的 FormData
|
||||
* 新增故障(JSON 提交,图片已上传至 COS)
|
||||
* @param {object} data - 故障信息,含 imageUrls 数组
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function addFault(formData) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const header = {
|
||||
'Authorization': `Bearer ${store.token}`,
|
||||
'Userid': store.userId,
|
||||
'Username': store.userName
|
||||
}
|
||||
uni.uploadFile({
|
||||
url: BASE_URL + '/business/OdfCableFaults/add',
|
||||
files: formData.files || [],
|
||||
formData: formData.data || {},
|
||||
header,
|
||||
success(res) {
|
||||
try {
|
||||
const result = JSON.parse(res.data)
|
||||
resolve({ code: result.code, msg: result.msg, data: result.data })
|
||||
} catch (e) {
|
||||
reject({ code: -1, msg: '解析响应失败' })
|
||||
}
|
||||
},
|
||||
fail(err) {
|
||||
reject({ code: -1, msg: err.errMsg || '网络异常' })
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
export const addFault = (data) => post('/business/OdfCableFaults/add', data)
|
||||
|
||||
export const incrementFaultCount = (id) =>
|
||||
post(`/business/OdfCableFaults/incrementFaultCount/${id}`)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,90 @@
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using COSXML;
|
||||
using COSXML.Auth;
|
||||
using COSXML.Model.Tag;
|
||||
|
||||
namespace ZR.Admin.WebApi.Controllers.Business
|
||||
{
|
||||
/// <summary>
|
||||
/// COS 预签名上传
|
||||
/// </summary>
|
||||
[Route("business/CosUpload")]
|
||||
public class CosUploadController : BaseController
|
||||
{
|
||||
private readonly IConfiguration _config;
|
||||
|
||||
public CosUploadController(IConfiguration config)
|
||||
{
|
||||
_config = config;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取 COS PUT 预签名 URL(前端直传)
|
||||
/// </summary>
|
||||
[HttpPost("presignUrl")]
|
||||
public IActionResult GetPresignUrl([FromBody] CosPresignRequestDto dto)
|
||||
{
|
||||
var section = _config.GetSection("TencentCos");
|
||||
var appId = section["AppId"];
|
||||
var bucket = section["Bucket"];
|
||||
var region = section["Region"];
|
||||
var secretId = section["SecretId"];
|
||||
var secretKey = section["SecretKey"];
|
||||
var domainUrl = section["DomainUrl"];
|
||||
var expireMinutes = int.Parse(section["PresignExpireMinutes"] ?? "10");
|
||||
|
||||
int count = Math.Clamp(dto.Count, 1, 9);
|
||||
string ext = string.IsNullOrWhiteSpace(dto.Ext) ? ".jpg" : dto.Ext;
|
||||
|
||||
var cosConfig = new CosXmlConfig.Builder()
|
||||
.SetRegion(region)
|
||||
.Build();
|
||||
|
||||
var credentialProvider = new DefaultQCloudCredentialProvider(
|
||||
secretId, secretKey, (long)expireMinutes * 60);
|
||||
var cosXml = new CosXmlServer(cosConfig, credentialProvider);
|
||||
|
||||
var results = new List<object>();
|
||||
var dateDir = DateTime.Now.ToString("yyyyMMdd");
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var cosKey = $"fault-images/{dateDir}/{Guid.NewGuid():N}{ext}";
|
||||
|
||||
var preSignatureStruct = new PreSignatureStruct
|
||||
{
|
||||
appid = appId,
|
||||
bucket = bucket,
|
||||
region = region,
|
||||
key = cosKey,
|
||||
httpMethod = "PUT",
|
||||
isHttps = true,
|
||||
signDurationSecond = expireMinutes * 60,
|
||||
headers = null,
|
||||
queryParameters = null
|
||||
};
|
||||
|
||||
var presignUrl = cosXml.GenerateSignURL(preSignatureStruct);
|
||||
var accessUrl = $"{domainUrl.TrimEnd('/')}/{cosKey}";
|
||||
|
||||
results.Add(new { cosKey, presignUrl, accessUrl });
|
||||
}
|
||||
|
||||
return SUCCESS(results);
|
||||
}
|
||||
}
|
||||
|
||||
public class CosPresignRequestDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 需要上传的文件数量
|
||||
/// </summary>
|
||||
public int Count { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// 文件扩展名,如 .jpg
|
||||
/// </summary>
|
||||
public string Ext { get; set; } = ".jpg";
|
||||
}
|
||||
}
|
||||
|
|
@ -49,13 +49,13 @@ namespace ZR.Admin.WebApi.Controllers.Business
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// 新增故障(含图片上传,APP端调用)
|
||||
/// 新增故障(图片已上传至COS,提交COS URL)
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpPost("add")]
|
||||
[ActionPermissionFilter(Permission = "odfcablefaults:list")]
|
||||
[Log(Title = "干线故障", BusinessType = BusinessType.INSERT)]
|
||||
public async Task<IActionResult> Add([FromForm] OdfCableFaultAddDto dto)
|
||||
public async Task<IActionResult> Add([FromBody] OdfCableFaultAddDto dto)
|
||||
{
|
||||
dto.UserId = HttpContext.GetUId();
|
||||
var response = await _OdfCableFaultsService.AddFault(dto);
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@
|
|||
<PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="7.0.12" />
|
||||
<PackageReference Include="NLog.Web.AspNetCore" Version="5.4.0" />
|
||||
<PackageReference Include="Mapster" Version="7.4.0" />
|
||||
<PackageReference Include="Tencent.QCloud.Cos.Sdk" Version="5.4.44" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -54,6 +54,16 @@
|
|||
"notAllowedExt": [ ".bat", ".exe", ".jar", ".js" ],
|
||||
"requestLimitSize": 50 //请求body大小限制
|
||||
},
|
||||
// 腾讯云COS配置
|
||||
"TencentCos": {
|
||||
"AppId": "1308826010",
|
||||
"Bucket": "youdas-1308826010",
|
||||
"Region": "ap-shanghai",
|
||||
"SecretId": "AKIDNdjgTFyZ3UmvsdDbpsiNp690e6MPFrHV",
|
||||
"SecretKey": "5xc6PVWM0SggYEguxyxkS5bvgNr8B0c2",
|
||||
"DomainUrl": "https://youdas-1308826010.cos.ap-shanghai.myqcloud.com",
|
||||
"PresignExpireMinutes": 10
|
||||
},
|
||||
//阿里云存储配置
|
||||
"ALIYUN_OSS": {
|
||||
"REGIONID": "https://oss-cn-shanghai.aliyuncs.com", //eg:cn-hangzhou
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
using Microsoft.AspNetCore.Http;
|
||||
using MiniExcelLibs.Attributes;
|
||||
|
||||
namespace ZR.Model.Business.Dto
|
||||
|
|
@ -50,7 +49,10 @@ namespace ZR.Model.Business.Dto
|
|||
|
||||
public long? UserId { get; set; }
|
||||
|
||||
public IFormFile[] Images { get; set; }
|
||||
/// <summary>
|
||||
/// COS 图片访问 URL 列表(前端直传 COS 后传入)
|
||||
/// </summary>
|
||||
public string[] ImageUrls { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
using Infrastructure;
|
||||
using Infrastructure.Attribute;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using ZR.Model;
|
||||
using ZR.Model.Business;
|
||||
using ZR.Model.Business.Dto;
|
||||
|
|
@ -156,7 +154,7 @@ namespace ZR.Service.Business
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// 新增故障(含图片上传)
|
||||
/// 新增故障(图片已上传至 COS,仅保存 URL)
|
||||
/// </summary>
|
||||
public async Task<int> AddFault(OdfCableFaultAddDto dto)
|
||||
{
|
||||
|
|
@ -170,7 +168,7 @@ namespace ZR.Service.Business
|
|||
}
|
||||
|
||||
// 校验至少 1 张图片
|
||||
if (dto.Images == null || dto.Images.Length == 0)
|
||||
if (dto.ImageUrls == null || dto.ImageUrls.Length == 0)
|
||||
{
|
||||
throw new CustomException("请至少上传一张图片");
|
||||
}
|
||||
|
|
@ -196,36 +194,10 @@ namespace ZR.Service.Business
|
|||
var faultEntity = Insertable(model).ExecuteReturnEntity();
|
||||
int faultId = faultEntity.Id;
|
||||
|
||||
// 保存图片文件并插入图片记录
|
||||
IWebHostEnvironment webHostEnvironment = App.WebHostEnvironment;
|
||||
string webRootPath = webHostEnvironment.WebRootPath;
|
||||
string uploadDir = Path.Combine("uploads", "fault");
|
||||
string fullDir = Path.Combine(webRootPath, uploadDir);
|
||||
|
||||
if (!Directory.Exists(fullDir))
|
||||
// 插入图片记录(COS URL)
|
||||
foreach (var imageUrl in dto.ImageUrls)
|
||||
{
|
||||
Directory.CreateDirectory(fullDir);
|
||||
}
|
||||
|
||||
foreach (var image in dto.Images)
|
||||
{
|
||||
string fileExt = Path.GetExtension(image.FileName);
|
||||
string fileName = $"{DateTime.Now:yyyyMMdd}_{Guid.NewGuid():N}{fileExt}";
|
||||
string filePath = Path.Combine(fullDir, fileName);
|
||||
|
||||
using (var stream = new FileStream(filePath, FileMode.Create))
|
||||
{
|
||||
await image.CopyToAsync(stream);
|
||||
}
|
||||
|
||||
string imageUrl = $"/{uploadDir}/{fileName}".Replace("\\", "/");
|
||||
|
||||
// 拼接完整URL,前端可直接使用
|
||||
var request = App.HttpContext?.Request;
|
||||
if (request != null)
|
||||
{
|
||||
imageUrl = $"{request.Scheme}://{request.Host}{imageUrl}";
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(imageUrl)) continue;
|
||||
|
||||
var imageRecord = new OdfCableFaultImages
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user