feat:优化了门店部门财务功能,新增开票功能
This commit is contained in:
@@ -115,6 +115,23 @@ export function generateObjectName(file: File): string {
|
||||
return objectName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 File 推断上传用 Content-Type(浏览器可能给空 type)
|
||||
*/
|
||||
function resolveUploadContentType(file: File): string {
|
||||
if (file.type && file.type !== 'application/octet-stream') {
|
||||
return file.type;
|
||||
}
|
||||
const name = (file.name || '').toLowerCase();
|
||||
if (name.endsWith('.pdf')) return 'application/pdf';
|
||||
if (name.endsWith('.png')) return 'image/png';
|
||||
if (name.endsWith('.jpg') || name.endsWith('.jpeg')) return 'image/jpeg';
|
||||
if (name.endsWith('.gif')) return 'image/gif';
|
||||
if (name.endsWith('.webp')) return 'image/webp';
|
||||
if (name.endsWith('.bmp')) return 'image/bmp';
|
||||
return file.type || 'application/octet-stream';
|
||||
}
|
||||
|
||||
/**
|
||||
* 从后端获取OSS上传签名信息
|
||||
*
|
||||
@@ -252,6 +269,12 @@ export async function uploadToOss(options: UploadOptions): Promise<UploadResult>
|
||||
// 这与后端上传保持一致(后端上传后会调用putObjectAcl设置公共读)
|
||||
// 注意:此字段必须与后端Policy中的条件一致,否则会上传失败
|
||||
formData.append('x-oss-object-acl', 'public-read');
|
||||
|
||||
// Content-Type:按文件 MIME 写入,避免 PDF 被当成 octet-stream 导致浏览器直接下载
|
||||
const contentType = resolveUploadContentType(file);
|
||||
formData.append('Content-Type', contentType);
|
||||
// Content-Disposition: inline —— 浏览器倾向预览而非附件下载
|
||||
formData.append('Content-Disposition', 'inline');
|
||||
|
||||
// file: 要上传的文件
|
||||
// 必须是最后一个字段,OSS要求file字段在FormData的最后
|
||||
|
||||
160
apps/web-antd/src/utils/use-paste-upload-listen.ts
Normal file
160
apps/web-antd/src/utils/use-paste-upload-listen.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
import { onUnmounted, ref, type Ref } from 'vue';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
export interface UsePasteUploadListenOptions {
|
||||
/** accept 字符串,如 `.jpg,.png,image/*,application/pdf`;支持 getter */
|
||||
accept?: string | (() => string);
|
||||
/** 单文件最大体积(字节);支持 getter */
|
||||
maxSize?: number | (() => number);
|
||||
/** 过滤后的文件回调 */
|
||||
onFiles: (files: File[]) => void | Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 accept 规则解析为便于匹配的片段列表
|
||||
*/
|
||||
function parseAccept(accept: string): string[] {
|
||||
return accept
|
||||
.split(',')
|
||||
.map((s) => s.trim().toLowerCase())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断文件是否匹配 accept(扩展名 / mime / image/* 等)
|
||||
*/
|
||||
export function matchAccept(file: File, accept: string): boolean {
|
||||
const rules = parseAccept(accept);
|
||||
if (!rules.length) return true;
|
||||
const name = (file.name || '').toLowerCase();
|
||||
const type = (file.type || '').toLowerCase();
|
||||
const ext = name.includes('.') ? `.${name.split('.').pop()}` : '';
|
||||
return rules.some((rule) => {
|
||||
if (rule === '*/*') return true;
|
||||
if (rule.endsWith('/*')) {
|
||||
const prefix = rule.slice(0, -1); // image/
|
||||
return type.startsWith(prefix);
|
||||
}
|
||||
if (rule.startsWith('.')) {
|
||||
return ext === rule;
|
||||
}
|
||||
return type === rule;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 从粘贴事件中收集 File(files + items 截图等)
|
||||
*/
|
||||
export function collectPasteFiles(event: ClipboardEvent): File[] {
|
||||
const result: File[] = [];
|
||||
const seen = new Set<string>();
|
||||
const push = (file: File | null | undefined) => {
|
||||
if (!file) return;
|
||||
const key = `${file.name}|${file.size}|${file.type}|${file.lastModified}`;
|
||||
if (seen.has(key)) return;
|
||||
seen.add(key);
|
||||
result.push(file);
|
||||
};
|
||||
|
||||
const files = event.clipboardData?.files;
|
||||
if (files?.length) {
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
push(files.item(i));
|
||||
}
|
||||
}
|
||||
|
||||
const items = event.clipboardData?.items;
|
||||
if (items?.length) {
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i];
|
||||
if (item?.kind === 'file') {
|
||||
push(item.getAsFile());
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始/停止监听窗口粘贴,用于上传组件在拖拽区外主动开启粘贴上传
|
||||
*/
|
||||
export function usePasteUploadListen(options: UsePasteUploadListenOptions) {
|
||||
const listening: Ref<boolean> = ref(false);
|
||||
let handler: ((e: ClipboardEvent) => void) | null = null;
|
||||
|
||||
function stop() {
|
||||
if (handler) {
|
||||
window.removeEventListener('paste', handler);
|
||||
handler = null;
|
||||
}
|
||||
listening.value = false;
|
||||
}
|
||||
|
||||
function start() {
|
||||
stop();
|
||||
handler = (event: ClipboardEvent) => {
|
||||
const acceptRaw = options.accept;
|
||||
const accept =
|
||||
typeof acceptRaw === 'function' ? acceptRaw() : acceptRaw || '';
|
||||
const maxSizeRaw = options.maxSize;
|
||||
const maxSize =
|
||||
typeof maxSizeRaw === 'function'
|
||||
? maxSizeRaw()
|
||||
: (maxSizeRaw ?? 20 * 1024 * 1024);
|
||||
const raw = collectPasteFiles(event);
|
||||
if (!raw.length) {
|
||||
message.warning('剪贴板中没有可上传的文件');
|
||||
return;
|
||||
}
|
||||
const filtered: File[] = [];
|
||||
for (const file of raw) {
|
||||
if (accept && !matchAccept(file, accept)) {
|
||||
continue;
|
||||
}
|
||||
if (file.size > maxSize) {
|
||||
message.warning(`${file.name || '文件'} 超过大小限制`);
|
||||
continue;
|
||||
}
|
||||
// 截图常无名为空,补一个默认名便于上传
|
||||
if (!file.name) {
|
||||
const ext =
|
||||
file.type === 'image/png'
|
||||
? 'png'
|
||||
: file.type === 'image/jpeg'
|
||||
? 'jpg'
|
||||
: file.type === 'application/pdf'
|
||||
? 'pdf'
|
||||
: 'bin';
|
||||
filtered.push(new File([file], `paste-${Date.now()}.${ext}`, { type: file.type }));
|
||||
} else {
|
||||
filtered.push(file);
|
||||
}
|
||||
}
|
||||
if (!filtered.length) {
|
||||
message.warning('剪贴板文件类型不符合要求');
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
void options.onFiles(filtered);
|
||||
};
|
||||
window.addEventListener('paste', handler);
|
||||
listening.value = true;
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
if (listening.value) stop();
|
||||
else start();
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
stop();
|
||||
});
|
||||
|
||||
return {
|
||||
listening,
|
||||
start,
|
||||
stop,
|
||||
toggle,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user