diff --git a/.workbuddy/memory/CHANGELOG.md b/.workbuddy/memory/CHANGELOG.md index edb2846..31f916e 100644 --- a/.workbuddy/memory/CHANGELOG.md +++ b/.workbuddy/memory/CHANGELOG.md @@ -1,6 +1,8 @@ # tool.xpcool.com 变更记录 > 倒序:最新在上。格式:YYYY-MM-DD | 类型 | 摘要 +2026-09-01 | CHG | 全站接入图片预览(TDesign ImageViewer):新增共享 AppImageViewer 组件 + useImageViewer 组合式函数;接入 9 处——UploadDrop 缩略图、Base64 还原图、图片裁剪结果、图片转 ICO 预览(多尺寸成组)、OCR 待识别图、二维码结果、图片压缩(原直连 t-image-viewer 重构为共享组件)、代码转图片画布、占位图画布;全局样式新增 .img-previewable + 2026-09-01 | CHG | 图片转 ICO 结果区增强:逐尺寸单独 ICO 下载(buildIco 新增 singles 字段,复用条目数据零额外编码)+ 多尺寸一键打包 ZIP 下载(jszip);另修复 pnpm wrapper 中 node 版本路径(22.22.2 → 22.22.2-2) 2026-08-31 | CHG | 新增「图片转 ICO」工具:src/utils/ico.ts(纯前端自实现 PNG/BMP DIB/ICO 编码 + 中位切分量化 + 自适应滤波 + 体积上限自动降级),src/views/image-to-ico/ImageToIcoView.vue(尺寸组合/填充/编码方式/量化/体积限制/预览下载/历史),路由注册于 src/router/tools.ts;Pillow 独立解码验证通过 diff --git a/src/assets/styles/index.scss b/src/assets/styles/index.scss index 7a4fbc9..2a791e7 100644 --- a/src/assets/styles/index.scss +++ b/src/assets/styles/index.scss @@ -93,3 +93,11 @@ html[theme-mode='dark'] ::-webkit-scrollbar-thumb { font-size: 14px; color: var(--td-text-color-secondary); } + +/* 可点击放大的图片/画布(配合 AppImageViewer 全站共用):手型光标 + hover 高亮 */ +.img-previewable { + cursor: zoom-in; +} +.img-previewable:hover { + box-shadow: 0 0 0 2px var(--td-brand-color); +} diff --git a/src/components/common/AppImageViewer.vue b/src/components/common/AppImageViewer.vue new file mode 100644 index 0000000..2e150a1 --- /dev/null +++ b/src/components/common/AppImageViewer.vue @@ -0,0 +1,36 @@ + + + diff --git a/src/components/common/UploadDrop.vue b/src/components/common/UploadDrop.vue index 5c8b17a..411e30b 100644 --- a/src/components/common/UploadDrop.vue +++ b/src/components/common/UploadDrop.vue @@ -33,7 +33,14 @@
- + {{ f.name }} @@ -45,6 +52,9 @@

点击可继续添加文件

+ + + @@ -52,6 +62,8 @@ import { onBeforeUnmount, ref, watch } from 'vue' import { DeleteIcon, FileIcon, UploadIcon } from 'tdesign-icons-vue-next' import { formatBytes } from '@/utils' +import AppImageViewer from '@/components/common/AppImageViewer.vue' +import { useImageViewer } from '@/composables/useImageViewer' /** * 通用拖拽上传组件(替代 t-upload 文件模式): @@ -85,6 +97,14 @@ const dragging = ref(false) /** 图片缩略图的对象 URL(与 files 一一对应,非图片为空串) */ const previews = ref([]) +/** 点击缩略图放大预览:图片组取所有非空缩略图,定位到当前点击项 */ +const { visible, index, images, open } = useImageViewer() +function openPreview(i: number) { + // 计算「当前缩略图在所有图片缩略图中的序号」,保证预览定位正确 + const imgIdx = previews.value.slice(0, i + 1).filter(Boolean).length - 1 + open(previews.value.filter(Boolean), imgIdx) +} + // 深度计数:避免拖过子元素时 dragleave 导致高亮闪烁 let dragDepth = 0 @@ -252,6 +272,14 @@ function removeAt(i: number) { color: var(--td-text-color-placeholder); background: var(--td-bg-color-secondarycontainer); } +/* 可点击预览的缩略图 */ +.thumb-clickable { + cursor: zoom-in; + transition: transform 0.15s; +} +.thumb-clickable:hover { + transform: scale(1.06); +} .meta { flex: 1; min-width: 0; diff --git a/src/composables/useImageViewer.ts b/src/composables/useImageViewer.ts new file mode 100644 index 0000000..de99a84 --- /dev/null +++ b/src/composables/useImageViewer.ts @@ -0,0 +1,36 @@ +import { ref } from 'vue' + +/** 可预览的图片:URL 字符串或 File 对象(TDesign ImageViewer 原生支持两种) */ +export type PreviewImage = string | File + +/** + * 图片预览组合式函数:全站各工具页共用。 + * 典型用法: + * const { visible, index, images, open } = useImageViewer() + * (模板中 ref 自动解包) + * 点击图片时 open(urls, i) 打开预览。 + */ +export function useImageViewer() { + /** 是否显示预览 */ + const visible = ref(false) + /** 当前预览的下标 */ + const index = ref(0) + /** 预览的图片组(支持左右切换) */ + const images = ref([]) + + /** 打开预览:传入图片组与起始下标 */ + function open(imgs: PreviewImage[], startIndex = 0) { + const list = imgs.filter(Boolean) + if (!list.length) return + images.value = list + index.value = Math.min(Math.max(startIndex, 0), list.length - 1) + visible.value = true + } + + /** 关闭预览(一般由组件内部关闭,无需手动调用) */ + function close() { + visible.value = false + } + + return { visible, index, images, open, close } +} diff --git a/src/views/base64/Base64View.vue b/src/views/base64/Base64View.vue index 08ce323..0da18f6 100644 --- a/src/views/base64/Base64View.vue +++ b/src/views/base64/Base64View.vue @@ -61,11 +61,20 @@ - Base64 还原的图片 + Base64 还原的图片
+ + + @@ -86,6 +95,8 @@ import CopyButton from '@/components/common/CopyButton.vue' import DownloadButton from '@/components/common/DownloadButton.vue' import HistoryPanel from '@/components/common/HistoryPanel.vue' import { useHistory } from '@/composables/useHistory' +import { useImageViewer } from '@/composables/useImageViewer' +import AppImageViewer from '@/components/common/AppImageViewer.vue' import { dataUrlToBlob, fileToDataUrl } from '@/utils' const tab = ref('text') @@ -97,6 +108,9 @@ const error = ref('') const history = useHistory('base64') +/** 结果图放大预览(TDesign ImageViewer) */ +const { visible: viewerVisible, index: viewerIndex, images: viewerImages, open: openViewer } = useImageViewer() + /** 编码:UTF-8 安全的 base64(先转字节再 btoa,避免中文报错) */ function encodeText() { error.value = '' diff --git a/src/views/code-to-image/CodeToImageView.vue b/src/views/code-to-image/CodeToImageView.vue index b91846c..d2f33a3 100644 --- a/src/views/code-to-image/CodeToImageView.vue +++ b/src/views/code-to-image/CodeToImageView.vue @@ -35,7 +35,7 @@
- +
@@ -44,6 +44,9 @@
+ + + @@ -51,7 +54,9 @@ import { ref } from 'vue' import { DownloadIcon } from 'tdesign-icons-vue-next' import ErrorAlert from '@/components/common/ErrorAlert.vue' +import AppImageViewer from '@/components/common/AppImageViewer.vue' import { downloadDataUrl } from '@/composables/useDownload' +import { useImageViewer } from '@/composables/useImageViewer' const LANGS = ['javascript', 'typescript', 'python', 'go', 'html', 'css', 'json', 'sql'].map((l) => ({ label: l, @@ -65,6 +70,13 @@ const width = ref(800) const filename = ref('demo.js') const error = ref('') const rendered = ref(false) + +/** 点击画布放大预览(把当前画布导出为 PNG 后交给 ImageViewer) */ +const { visible: viewerVisible, index: viewerIndex, images: viewerImages, open: openViewer } = useImageViewer() +function previewCanvas() { + const canvas = canvasRef.value + if (canvas) openViewer([canvas.toDataURL('image/png')]) +} const canvasRef = ref(null) const SAMPLE = `// 示例代码 diff --git a/src/views/image-compress/ImageCompressView.vue b/src/views/image-compress/ImageCompressView.vue index dbfd84e..07bd418 100644 --- a/src/views/image-compress/ImageCompressView.vue +++ b/src/views/image-compress/ImageCompressView.vue @@ -108,14 +108,8 @@ - - + + ([]) -/** 放大预览状态:展示「原图 + 压缩图」,从点击的那张开始 */ -const viewerVisible = ref(false) -const viewerImages = ref([]) -const viewerIndex = ref(0) +/** 放大预览:统一走共享 useImageViewer,展示「原图 + 压缩图」,从点击的那张开始 */ +const { visible: viewerVisible, index: viewerIndex, images: viewerImages, open: openViewer } = useImageViewer() /** 历史记录:保存「张数 | 压缩方式 | 格式 | 尺寸」摘要,点击可恢复压缩参数 */ const history = useHistory('image-compress') @@ -341,9 +335,7 @@ function downloadOne(r: CompressResult) { /** 放大预览:优先展示「原图 + 压缩图」两张并可左右切换,失败时只有原图 */ function openPreview(r: CompressResult, index: number) { - viewerImages.value = r.outUrl ? [r.sourceUrl, r.outUrl] : [r.sourceUrl] - viewerIndex.value = r.outUrl ? index : 0 - viewerVisible.value = true + openViewer(r.outUrl ? [r.sourceUrl, r.outUrl] : [r.sourceUrl], r.outUrl ? index : 0) } /** 全部结果打包为 ZIP 下载(jszip,纯本地生成) */ diff --git a/src/views/image-cropper/ImageCropperView.vue b/src/views/image-cropper/ImageCropperView.vue index 7af8df9..aa97609 100644 --- a/src/views/image-cropper/ImageCropperView.vue +++ b/src/views/image-cropper/ImageCropperView.vue @@ -26,12 +26,21 @@ - 裁剪结果预览 + 裁剪结果预览
+ + + genFilename('cropped', outType.value.split('/')[1])) function ratioValue(v: string): number { diff --git a/src/views/image-to-ico/ImageToIcoView.vue b/src/views/image-to-ico/ImageToIcoView.vue index 755a356..06eaaed 100644 --- a/src/views/image-to-ico/ImageToIcoView.vue +++ b/src/views/image-to-ico/ImageToIcoView.vue @@ -92,7 +92,14 @@
- ICO 预览 + ICO 预览

@@ -120,11 +127,18 @@ -

各尺寸预览(点击右下角可单独下载 PNG / ICO)

+

各尺寸预览(点击图片放大,右下角可单独下载 PNG / ICO)

-
+
- +
{{ p.size }}×{{ p.size }} @@ -138,6 +152,9 @@ + + +
@@ -147,7 +164,9 @@ import JSZip from 'jszip' import UploadDrop from '@/components/common/UploadDrop.vue' import ErrorAlert from '@/components/common/ErrorAlert.vue' import HistoryPanel from '@/components/common/HistoryPanel.vue' +import AppImageViewer from '@/components/common/AppImageViewer.vue' import { useHistory } from '@/composables/useHistory' +import { useImageViewer } from '@/composables/useImageViewer' import { downloadBlob } from '@/composables/useDownload' import { dataUrlToBlob, formatBytes } from '@/utils' import { @@ -198,6 +217,10 @@ const sourceInfo = ref<{ name: string; width: number; height: number; size: numb /** 历史记录:存「文件名 | 参数摘要」,点击可恢复转换参数 */ const history = useHistory('image-to-ico') +/** 预览图放大查看(各尺寸 PNG DataURL 成组) */ +const { visible: viewerVisible, index: viewerIndex, images: viewerImages, open: openViewer } = useImageViewer() +const previewUrls = computed(() => result.value?.previews.map((p) => p.url) ?? []) + /** 编码方式对应的说明文案 */ const modeHint = computed(() => { if (mode.value === 'bmp') return 'BMP 兼容性最好,但 128/256 尺寸体积会明显偏大' diff --git a/src/views/ocr/OcrView.vue b/src/views/ocr/OcrView.vue index fdb9868..aa22d53 100644 --- a/src/views/ocr/OcrView.vue +++ b/src/views/ocr/OcrView.vue @@ -7,7 +7,14 @@ - 待识别图片 + 待识别图片
开始识别 @@ -41,6 +48,9 @@ @remove="history.remove" @clear="history.clear" /> + + +
@@ -56,6 +66,8 @@ import ErrorAlert from '@/components/common/ErrorAlert.vue' import CopyButton from '@/components/common/CopyButton.vue' import HistoryPanel from '@/components/common/HistoryPanel.vue' import { useHistory } from '@/composables/useHistory' +import { useImageViewer } from '@/composables/useImageViewer' +import AppImageViewer from '@/components/common/AppImageViewer.vue' import { downloadText } from '@/composables/useDownload' import { genFilename } from '@/utils' @@ -72,6 +84,9 @@ type WorkerInstance = Awaited> const files = ref([]) const file = ref(null) const previewUrl = ref('') + +/** 待识别图放大预览(TDesign ImageViewer) */ +const { visible: viewerVisible, index: viewerIndex, images: viewerImages, open: openViewer } = useImageViewer() const busy = ref(false) const progress = ref(0) const progressLabel = ref('') diff --git a/src/views/placeholder-image/PlaceholderImageView.vue b/src/views/placeholder-image/PlaceholderImageView.vue index a902f4a..7409b76 100644 --- a/src/views/placeholder-image/PlaceholderImageView.vue +++ b/src/views/placeholder-image/PlaceholderImageView.vue @@ -5,7 +5,7 @@
- +
@@ -54,6 +54,9 @@ @remove="history.remove" @clear="history.clear" /> + + +
@@ -62,6 +65,8 @@ import { computed, ref, watch } from 'vue' import CopyButton from '@/components/common/CopyButton.vue' import HistoryPanel from '@/components/common/HistoryPanel.vue' import { useHistory } from '@/composables/useHistory' +import { useImageViewer } from '@/composables/useImageViewer' +import AppImageViewer from '@/components/common/AppImageViewer.vue' import { downloadDataUrl } from '@/composables/useDownload' interface PlaceholderParams { @@ -77,6 +82,13 @@ const history = useHistory('placeholder-image') const canvasRef = ref(null) const width = ref(400) const height = ref(300) + +/** 点击画布放大预览(把当前画布导出为 PNG 后交给 ImageViewer) */ +const { visible: viewerVisible, index: viewerIndex, images: viewerImages, open: openViewer } = useImageViewer() +function previewCanvas() { + const canvas = canvasRef.value + if (canvas) openViewer([canvas.toDataURL('image/png')]) +} const bgColor = ref('#667eea') const textColor = ref('#ffffff') const text = ref('400 x 300') diff --git a/src/views/qrcode/QrcodeView.vue b/src/views/qrcode/QrcodeView.vue index 43a9786..557bc7d 100644 --- a/src/views/qrcode/QrcodeView.vue +++ b/src/views/qrcode/QrcodeView.vue @@ -35,7 +35,13 @@
- 生成的二维码 + 生成的二维码
@@ -77,6 +83,9 @@ /> + + +
@@ -90,6 +99,8 @@ import CopyButton from '@/components/common/CopyButton.vue' import DownloadButton from '@/components/common/DownloadButton.vue' import HistoryPanel from '@/components/common/HistoryPanel.vue' import { useHistory } from '@/composables/useHistory' +import { useImageViewer } from '@/composables/useImageViewer' +import AppImageViewer from '@/components/common/AppImageViewer.vue' import { dataUrlToBlob } from '@/utils' const tab = ref('generate') @@ -107,6 +118,9 @@ const text = ref('') const size = ref(256) const level = ref('M') const dark = ref('#000000') + +/** 二维码放大预览(TDesign ImageViewer) */ +const { visible: viewerVisible, index: viewerIndex, images: viewerImages, open: openViewer } = useImageViewer() const light = ref('#ffffff') const genBusy = ref(false) const genError = ref('')