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:
2026-08-24 01:59:27 +08:00
parent 2e3f68472e
commit 01c9cc2330
9 changed files with 389 additions and 28 deletions
+85 -21
View File
@@ -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>