docs: 鏇存柊闇€姹傛枃妗h嚦v0.5骞跺綊妗e紑鍙戣繘搴︼紱淇澶ф枃浠朵笂浼犱笌浜岀淮鐮佹捣鎶?
- 闇€姹傛枃妗e崌绾у埌 v0.5锛氳ˉ鍏呬簩缁寸爜娴锋姤淇濆瓨鏂囦欢鍚嶉粯璁ゆ牸寮忋€佸ぇ鏂囦欢涓婁紶涓夊眰闄愬埗锛圛IS+ASP.NET+鍓嶇锛夐儴缃茬害鏉熴€佹柊澧炵11绔犲紑鍙戣繘搴︾姸鎬佸綊妗o紙宸蹭笂绾垮姛鑳?宸蹭慨澶嶇己闄?宸茬煡闄愬埗/鍚庣画浼樺寲锛?- 鍚庣 FileController.Upload 澧炲姞 [RequestFormLimits(MultipartBodyLengthLimit=220_200_960)]锛屼慨澶?128MB multipart 涓婁紶琚嫆 - 鍓嶇 UploadView 浜岀淮鐮佹捣鎶ワ細淇 drawImage 缂╂斁閿欎綅銆侀《閮╨ogo/鏍囬鍨傜洿灞呬腑銆佸垎鍖烘爣棰橀棿璺濄€佸ぇ灏忎笌鏈夋晥鏈熷垎琛屾樉绀猴紱淇濆瓨鏂囦欢鍚嶆敼涓?鏂囦紶鏄撳彇浠剁爜-鍙栦欢鐮?鍘熸枃浠跺悕)-娴佹按鍙?png - 鏂板閮ㄧ讲鑴氭湰锛歛pply_backend.ps1 / force_dll.ps1锛坅pp_offline 瑙i攣DLL锛? ftp_fe.ps1 / ftp_diag.ps1 / ftp_apply.ps1 / _diag/check_dll.ps1 (閮ㄧ讲鎵嬪唽 IIS閮ㄧ讲涓嶧TP鍙戝竷.md 浠呭惈缂栫爜/BOM 宸紓锛屾湭绾冲叆鏈鎻愪氦)
This commit is contained in:
@@ -18,6 +18,20 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 微信内置浏览器提示 -->
|
||||
<div
|
||||
v-if="isWeChat && showWeChatTip"
|
||||
class="mb-4 px-4 py-3 flex items-start gap-2 text-[13px] rounded-2xl border-2 border-[#FFD591] bg-[#FFF7E8]"
|
||||
>
|
||||
<div class="flex-1 text-[#8A6213] leading-relaxed">
|
||||
微信内上传与下载会受限(无法从聊天选择文件、下载需跳转外部浏览器)。建议点击右上角「···」→「在浏览器打开」。
|
||||
<button class="text-[#2D6CFF] font-medium underline underline-offset-2" @click="copySiteLink">复制链接</button>
|
||||
</div>
|
||||
<button class="text-[#C0A06B] shrink-0" @click="showWeChatTip = false">
|
||||
<t-icon name="close" :size="16" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 上传大卡片 -->
|
||||
<div
|
||||
v-if="!result"
|
||||
@@ -48,12 +62,17 @@
|
||||
|
||||
<!-- 文件模式 -->
|
||||
<div v-if="!textMode">
|
||||
<input ref="fileInput" type="file" class="hidden" @change="onFileChange" />
|
||||
<div
|
||||
v-if="!selectedFile"
|
||||
class="py-7 flex flex-col items-center justify-center gap-2.5 cursor-pointer text-center"
|
||||
@click="openPicker"
|
||||
class="relative py-7 flex flex-col items-center justify-center gap-2.5 cursor-pointer text-center"
|
||||
>
|
||||
<!-- 原生 file input 透明铺满上传区:不显示实体按钮,用户点击上传区时直接点到 input,由系统弹出选择器 -->
|
||||
<input
|
||||
ref="fileInput"
|
||||
type="file"
|
||||
class="absolute inset-0 z-10 w-full h-full opacity-0 cursor-pointer"
|
||||
@change="onFileChange"
|
||||
/>
|
||||
<div class="w-14 h-14 rounded-2xl bg-gradient-to-br from-[#2D6CFF] to-[#00B4FF]/80 grid place-items-center text-white shadow-card">
|
||||
<t-icon name="cloud-upload" :size="26" />
|
||||
</div>
|
||||
@@ -287,6 +306,35 @@ const expires = [
|
||||
|
||||
const textMode = ref(false)
|
||||
const fileInput = ref<HTMLInputElement | null>(null)
|
||||
const isWeChat = ref(false)
|
||||
const showWeChatTip = ref(true)
|
||||
|
||||
// 微信内置浏览器上传/下载受限,提供复制链接引导用户在外部浏览器打开
|
||||
function copySiteLink() {
|
||||
const url = location.href
|
||||
const ok = () => MessagePlugin.success('链接已复制,请到浏览器中粘贴打开')
|
||||
if (navigator.clipboard?.writeText) {
|
||||
navigator.clipboard.writeText(url).then(ok).catch(() => fallbackCopy(url))
|
||||
} else {
|
||||
fallbackCopy(url)
|
||||
}
|
||||
}
|
||||
function fallbackCopy(text: string) {
|
||||
const ta = document.createElement('textarea')
|
||||
ta.value = text
|
||||
ta.style.position = 'fixed'
|
||||
ta.style.opacity = '0'
|
||||
ta.style.left = '-9999px'
|
||||
document.body.appendChild(ta)
|
||||
ta.select()
|
||||
try {
|
||||
document.execCommand('copy')
|
||||
MessagePlugin.success('链接已复制,请到浏览器中粘贴打开')
|
||||
} catch {
|
||||
MessagePlugin.error('复制失败,请长按地址栏手动复制')
|
||||
}
|
||||
document.body.removeChild(ta)
|
||||
}
|
||||
const selectedFile = ref<File | null>(null)
|
||||
const pastedText = ref('')
|
||||
const expireHours = ref(24)
|
||||
@@ -322,10 +370,6 @@ const canUpload = computed(() => {
|
||||
return !!selectedFile.value && !passwordError.value && !tagError.value
|
||||
})
|
||||
|
||||
function openPicker() {
|
||||
fileInput.value?.click()
|
||||
}
|
||||
|
||||
function onFileChange(e: Event) {
|
||||
const input = e.target as HTMLInputElement
|
||||
if (input.files?.length) handleFile(input.files[0])
|
||||
@@ -524,14 +568,16 @@ async function buildPoster(d: UploadResult): Promise<string> {
|
||||
roundRect(ctx, 48, 24, 52, 52, 14)
|
||||
ctx.fill()
|
||||
ctx.fillStyle = '#FFFFFF'
|
||||
ctx.textBaseline = 'middle'
|
||||
ctx.textAlign = 'center'
|
||||
ctx.font = `bold 26px ${FONT}`
|
||||
ctx.fillText('传', 74, 37)
|
||||
ctx.fillText('传', 74, 50) // logo 圆块 24-76 范围的中线
|
||||
ctx.textAlign = 'left'
|
||||
ctx.fillText('文传易', 118, 26)
|
||||
ctx.fillText('文传易', 118, 36) // 与 logo 中线略上对齐(视觉与"免登录"副标题更平衡)
|
||||
ctx.font = `14px ${FONT}`
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.9)'
|
||||
ctx.fillText('免登录,传文件,真容易', 118, 60)
|
||||
ctx.fillText('免登录,传文件,真容易', 118, 64)
|
||||
ctx.textBaseline = 'top' // 恢复默认给后续 sectionTitle 等使用
|
||||
|
||||
// 成功提示
|
||||
ctx.fillStyle = '#00B578'
|
||||
@@ -542,12 +588,12 @@ async function buildPoster(d: UploadResult): Promise<string> {
|
||||
// 分区小标题
|
||||
const sectionTitle = (text: string, y: number) => {
|
||||
ctx.fillStyle = '#2D6CFF'
|
||||
roundRect(ctx, 44, y + 2, 4, 15, 2)
|
||||
roundRect(ctx, 38, y + 2, 4, 15, 2)
|
||||
ctx.fill()
|
||||
ctx.fillStyle = '#1F2329'
|
||||
ctx.textAlign = 'left'
|
||||
ctx.font = `600 15px ${FONT}`
|
||||
ctx.fillText(text, 60, y)
|
||||
ctx.fillText(text, 66, y)
|
||||
}
|
||||
|
||||
let y = 164
|
||||
@@ -598,8 +644,8 @@ async function buildPoster(d: UploadResult): Promise<string> {
|
||||
sectionTitle('文件信息', y)
|
||||
y += 36
|
||||
|
||||
// 文件信息卡
|
||||
const cardH = 20 + 32 + nameLines.length * 24 + 14
|
||||
// 文件信息卡(精确高度:上下各 16 padding + 文件名 N*24 + 大小行 24 + 有效期行 24)
|
||||
const cardH = 16 + nameLines.length * 24 + 24 + 24 + 16
|
||||
ctx.fillStyle = '#F4F8FF'
|
||||
roundRect(ctx, 44, y, W - 88, cardH, 16)
|
||||
ctx.fill()
|
||||
@@ -614,14 +660,23 @@ async function buildPoster(d: UploadResult): Promise<string> {
|
||||
cy += 24
|
||||
ctx.fillText(nameLines[i], 130, cy)
|
||||
}
|
||||
cy += 30
|
||||
// 大小(单独一行)
|
||||
cy += 28
|
||||
ctx.fillStyle = '#8A9099'
|
||||
ctx.font = `14px ${FONT}`
|
||||
ctx.fillText('大小 / 有效期', 62, cy)
|
||||
ctx.fillText('大小', 62, cy)
|
||||
ctx.fillStyle = '#1F2329'
|
||||
ctx.font = `15px ${FONT}`
|
||||
ctx.fillText(formatSize(d.size), 130, cy)
|
||||
// 有效期(单独一行)
|
||||
cy += 24
|
||||
ctx.fillStyle = '#8A9099'
|
||||
ctx.font = `14px ${FONT}`
|
||||
ctx.fillText('有效期', 62, cy)
|
||||
ctx.fillStyle = '#1F2329'
|
||||
ctx.font = `15px ${FONT}`
|
||||
const expireText = d.isPermanent ? '永久有效' : `至 ${formatDateTime(d.expiresAt)}`
|
||||
ctx.fillText(`${formatSize(d.size)} · ${expireText}`, 150, cy)
|
||||
ctx.fillText(expireText, 130, cy)
|
||||
|
||||
// 取件码标签(与大码拉开间距避免重叠)
|
||||
let codeY = y + cardH + 28
|
||||
@@ -650,14 +705,15 @@ async function buildPoster(d: UploadResult): Promise<string> {
|
||||
|
||||
// 动态画布高度(按实际内容,消除底部大留白)
|
||||
// 注意:重设 canvas.height 会清空画布内容,必须先备份再画回
|
||||
// 关键:drawImage 必须显式指定目标尺寸=backup 尺寸,否则浏览器会按新 canvas.height 缩放整张图,导致文字比例全乱
|
||||
const newH = qrY + qrSize + 14 + 40
|
||||
if (canvas.height !== newH) {
|
||||
if (Math.abs(canvas.height - newH) > 1) {
|
||||
const backup = document.createElement('canvas')
|
||||
backup.width = canvas.width
|
||||
backup.height = canvas.height
|
||||
backup.getContext('2d')!.drawImage(canvas, 0, 0)
|
||||
canvas.height = newH
|
||||
canvas.getContext('2d')!.drawImage(backup, 0, 0)
|
||||
canvas.getContext('2d')!.drawImage(backup, 0, 0, backup.width, backup.height)
|
||||
}
|
||||
|
||||
return canvas.toDataURL('image/png')
|
||||
@@ -697,9 +753,14 @@ async function copyText(text: string) {
|
||||
async function downloadQr() {
|
||||
if (!result.value) return
|
||||
posterDataUrl.value = await buildPoster(result.value)
|
||||
// 保存文件名:文传易取件码-取件码(原文件名)-流水号.png(清洗 Windows 非法字符)
|
||||
const safeName = (result.value.originalName || '')
|
||||
.replace(/[\\/:*?"<>|\u0000-\u001f]/g, '_')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim() || '未命名'
|
||||
const a = document.createElement('a')
|
||||
a.href = posterDataUrl.value
|
||||
a.download = `文传易取件凭证_${result.value.pickCode}.png`
|
||||
a.download = `文传易取件码-${result.value.pickCode}(${safeName})-${result.value.id}.png`
|
||||
a.click()
|
||||
}
|
||||
|
||||
@@ -715,6 +776,9 @@ function formatDateTime(s: string | null) {
|
||||
return s.replace('T', ' ').slice(0, 16)
|
||||
}
|
||||
|
||||
onMounted(() => window.addEventListener('paste', onPaste))
|
||||
onMounted(() => {
|
||||
isWeChat.value = /MicroMessenger/i.test(navigator.userAgent)
|
||||
window.addEventListener('paste', onPaste)
|
||||
})
|
||||
onUnmounted(() => window.removeEventListener('paste', onPaste))
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user