|
@@ -0,0 +1,368 @@
|
|
|
|
|
+{Template header}
|
|
|
|
|
+
|
|
|
|
|
+<style>
|
|
|
|
|
+ input[type="file"] {
|
|
|
|
|
+ display: none;
|
|
|
|
|
+ }
|
|
|
|
|
+ /* 图片网格 */
|
|
|
|
|
+ .image-grid {
|
|
|
|
|
+ display: grid;
|
|
|
|
|
+ grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
|
|
|
|
+ gap: 1.2rem;
|
|
|
|
|
+ margin-top: 1rem;
|
|
|
|
|
+ }
|
|
|
|
|
+ .image-card {
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ background: #f1f5f9;
|
|
|
|
|
+ border-radius: 20px;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ box-shadow: 0 4px 8px rgba(0,0,0,0.05);
|
|
|
|
|
+ transition: transform 0.2s, box-shadow 0.2s;
|
|
|
|
|
+ }
|
|
|
|
|
+ .image-card:hover {
|
|
|
|
|
+ transform: translateY(-4px);
|
|
|
|
|
+ box-shadow: 0 12px 20px -8px rgba(0,0,0,0.2);
|
|
|
|
|
+ }
|
|
|
|
|
+ .image-card img {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ aspect-ratio: 1 / 1;
|
|
|
|
|
+ object-fit: cover;
|
|
|
|
|
+ display: block;
|
|
|
|
|
+ background: #e2e8f0;
|
|
|
|
|
+ }
|
|
|
|
|
+ .delete-btn {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: 8px;
|
|
|
|
|
+ right: 8px;
|
|
|
|
|
+ background: rgba(0,0,0,0.65);
|
|
|
|
|
+ backdrop-filter: blur(4px);
|
|
|
|
|
+ border: none;
|
|
|
|
|
+ color: white;
|
|
|
|
|
+ width: 28px;
|
|
|
|
|
+ height: 28px;
|
|
|
|
|
+ border-radius: 40px;
|
|
|
|
|
+ font-size: 16px;
|
|
|
|
|
+ font-weight: bold;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ transition: 0.2s;
|
|
|
|
|
+ font-family: monospace;
|
|
|
|
|
+ }
|
|
|
|
|
+ .delete-btn:hover {
|
|
|
|
|
+ background: #dc2626;
|
|
|
|
|
+ transform: scale(1.05);
|
|
|
|
|
+ }
|
|
|
|
|
+ .upload-status {
|
|
|
|
|
+ margin-top: 0.8rem;
|
|
|
|
|
+ font-size: 0.85rem;
|
|
|
|
|
+ color: #2563eb;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 6px;
|
|
|
|
|
+ }
|
|
|
|
|
+ .loading-spinner {
|
|
|
|
|
+ width: 16px;
|
|
|
|
|
+ height: 16px;
|
|
|
|
|
+ border: 2px solid #bfdbfe;
|
|
|
|
|
+ border-top-color: #2563eb;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ animation: spin 0.7s linear infinite;
|
|
|
|
|
+ display: inline-block;
|
|
|
|
|
+ }
|
|
|
|
|
+ @keyframes spin {
|
|
|
|
|
+ to { transform: rotate(360deg); }
|
|
|
|
|
+ }
|
|
|
|
|
+ hr {
|
|
|
|
|
+ margin: 1.5rem 0;
|
|
|
|
|
+ border: none;
|
|
|
|
|
+ border-top: 1px solid #e2e8f0;
|
|
|
|
|
+ }
|
|
|
|
|
+ .info-note {
|
|
|
|
|
+ background: #fef9c3;
|
|
|
|
|
+ color: #854d0e;
|
|
|
|
|
+ font-size: 0.8rem;
|
|
|
|
|
+ padding: 0.6rem 1rem;
|
|
|
|
|
+ border-radius: 20px;
|
|
|
|
|
+ margin-top: 1rem;
|
|
|
|
|
+ }
|
|
|
|
|
+ button {
|
|
|
|
|
+ background: none;
|
|
|
|
|
+ border: none;
|
|
|
|
|
+ }
|
|
|
|
|
+ .btn-outline {
|
|
|
|
|
+ background: white;
|
|
|
|
|
+ border: 1px solid #cbd5e1;
|
|
|
|
|
+ padding: 0.4rem 1rem;
|
|
|
|
|
+ border-radius: 40px;
|
|
|
|
|
+ font-size: 0.8rem;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+</style>
|
|
|
|
|
+<body>
|
|
|
|
|
+<div class="warp">
|
|
|
|
|
+<div class="title winnone">货品图片 - 编辑</div>
|
|
|
|
|
+<ul class="setting">
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+<li class="length">
|
|
|
|
|
+<em>SKU:</em>
|
|
|
|
|
+<b>{$goods['sku']}</b>
|
|
|
|
|
+</li>
|
|
|
|
|
+<li class="length">
|
|
|
|
|
+<em>商品名称:</em>
|
|
|
|
|
+<b>{$goods['title']}</b>
|
|
|
|
|
+</li>
|
|
|
|
|
+<li class="length">
|
|
|
|
|
+<em>仓库名称:</em>
|
|
|
|
|
+<b>{$goods['zh']}</b>
|
|
|
|
|
+</li>
|
|
|
|
|
+<li class="length">
|
|
|
|
|
+<em>料号:</em>
|
|
|
|
|
+<b>{$goods['jm']}</b>
|
|
|
|
|
+</li>
|
|
|
|
|
+
|
|
|
|
|
+<li class="length">
|
|
|
|
|
+ <em>上传图片</em>
|
|
|
|
|
+ <span>
|
|
|
|
|
+ <a href="javascript:void(0);" id = "upload-btn"><span style="background-color: #2ca8a1;padding:4px 10px;color:#fff;border-radius: 3px;font-size: 16px;"> <span>点击添加图片</span></span> </a>
|
|
|
|
|
+
|
|
|
|
|
+ <input type="file" id="fileInput" accept="image/jpeg,image/png,image/gif,image/webp,image/avif,image/bmp,image/svg+xml,image/tiff,video/mp4,video/webm,video/ogg,video/quicktime,video/x-msvideo,video/3gpp" multiple>
|
|
|
|
|
+ </span>
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+<!-- 状态显示区 (上传中) -->
|
|
|
|
|
+<div id="statusMsg" class="upload-status" style="min-height: 32px;"></div>
|
|
|
|
|
+<!-- 图片列表展示区 -->
|
|
|
|
|
+<div id="imageGallery" class="image-grid">
|
|
|
|
|
+ {foreach $goods['images'] as $img}
|
|
|
|
|
+ {if $img['type'] == 'video'}
|
|
|
|
|
+ <div class="image-card">
|
|
|
|
|
+ <video controls="true" width="150" src="{$img['url']}" alt="video image"></video>
|
|
|
|
|
+ <input type="hidden" name="img[]" value="{$img['url']}" />
|
|
|
|
|
+ <button class="delete-btn" aria-label="删除文件">✕</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {else}
|
|
|
|
|
+ <div class="image-card">
|
|
|
|
|
+ <img src="{$img['url']}" alt="uploaded image">
|
|
|
|
|
+ <input type="hidden" name="img[]" value="{$img['url']}" />
|
|
|
|
|
+ <button class="delete-btn" aria-label="删除文件">✕</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ {/if}
|
|
|
|
|
+
|
|
|
|
|
+ {/foreach}
|
|
|
|
|
+</div>
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+<div style="clear:both;"></div>
|
|
|
|
|
+</ul>
|
|
|
|
|
+
|
|
|
|
|
+<input type="hidden" name="id" value="{$goods['id']}" />
|
|
|
|
|
+<div class="button"><font class="datasave">提 交</font> <font class="fh">关 闭</font></div>
|
|
|
|
|
+</div>
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+<script>
|
|
|
|
|
+
|
|
|
|
|
+var addedit="/goodimglibrary/edit/";
|
|
|
|
|
+const uploadZone = document.getElementById("upload-btn");
|
|
|
|
|
+const fileInput = document.getElementById('fileInput');
|
|
|
|
|
+const statusDiv = document.getElementById('statusMsg');
|
|
|
|
|
+const gallery = document.getElementById('imageGallery');
|
|
|
|
|
+
|
|
|
|
|
+// 向oss上传文件
|
|
|
|
|
+async function uploadSingleFile(file, credentials, onProgress) {
|
|
|
|
|
+ return new Promise(async (resolve, reject) => {
|
|
|
|
|
+ // 生成最终存储路径:目录 + 时间戳 + 随机数 + 原文件名
|
|
|
|
|
+ const timestamp = Date.now();
|
|
|
|
|
+ const random = Math.floor(Math.random() * 10000);
|
|
|
|
|
+ const safeName = `${timestamp}_${random}_${file.name.replace(/\s/g, '_')}`;
|
|
|
|
|
+ const objectKey = credentials.dir + safeName;
|
|
|
|
|
+
|
|
|
|
|
+ // 初始化 OSS 客户端
|
|
|
|
|
+ const client = new OSS({
|
|
|
|
|
+ region: credentials.region,
|
|
|
|
|
+ accessKeyId: credentials.accessKeyId,
|
|
|
|
|
+ accessKeySecret: credentials.accessKeySecret,
|
|
|
|
|
+ stsToken: credentials.stsToken,
|
|
|
|
|
+ bucket: credentials.bucket,
|
|
|
|
|
+ secure: true // 使用 HTTPS
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 使用分片上传(适合大文件,小文件也会自动处理)
|
|
|
|
|
+ const result = await client.multipartUpload(objectKey, file, {
|
|
|
|
|
+ progress: (p, checkpoint) => {
|
|
|
|
|
+ const percent = Math.floor(p * 100);
|
|
|
|
|
+ onProgress(percent);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ // 拼接完整的访问 URL
|
|
|
|
|
+ //const fileUrl = `https://${credentials.bucket}.${credentials.region}.aliyuncs.com/${objectKey}`;
|
|
|
|
|
+ const fileUrl = credentials.show_url+'/'+`${objectKey}`;
|
|
|
|
|
+ resolve(fileUrl);
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ reject(err);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+}
|
|
|
|
|
+// 显示短暂状态消息
|
|
|
|
|
+function showStatus(text, isLoading = false) {
|
|
|
|
|
+ if (!statusDiv) return;
|
|
|
|
|
+ if (isLoading) {
|
|
|
|
|
+ statusDiv.innerHTML = `<span class="loading-spinner"></span><span>${text}</span>`;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ statusDiv.innerHTML = `<span>✅ ${text}</span>`;
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ if (statusDiv.innerHTML.includes(text)) {
|
|
|
|
|
+ statusDiv.innerHTML = '';
|
|
|
|
|
+ }
|
|
|
|
|
+ }, 2000);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 添加图片卡片到画廊
|
|
|
|
|
+function addImageToGallery(imageUrl, fileName = '',fileType='') {
|
|
|
|
|
+ const card = document.createElement('div');
|
|
|
|
|
+ card.className = 'image-card';
|
|
|
|
|
+ let img ;
|
|
|
|
|
+ // 存储图片URL以便释放 (对于base64无需revoke,但如果是blob URL需要,这里base64无内存问题)
|
|
|
|
|
+ if (fileType.includes('image')) {
|
|
|
|
|
+ img = document.createElement('img');
|
|
|
|
|
+ img.src = imageUrl;
|
|
|
|
|
+ img.alt = fileName || 'uploaded image';
|
|
|
|
|
+ } else {
|
|
|
|
|
+ img = document.createElement('video');
|
|
|
|
|
+ img.src = imageUrl;
|
|
|
|
|
+ img.controls = true;
|
|
|
|
|
+ img.alt = fileName || 'uploaded video';
|
|
|
|
|
+ img.width="150"
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ const delBtn = document.createElement('button');
|
|
|
|
|
+ delBtn.innerHTML = '✕';
|
|
|
|
|
+ delBtn.className = 'delete-btn';
|
|
|
|
|
+ delBtn.setAttribute('aria-label', '删除图片');
|
|
|
|
|
+
|
|
|
|
|
+ // 删除事件
|
|
|
|
|
+ delBtn.addEventListener('click', (e) => {
|
|
|
|
|
+ e.stopPropagation();
|
|
|
|
|
+ card.remove();
|
|
|
|
|
+ showStatus('图片已删除', false);
|
|
|
|
|
+ // 可选:如果未来使用blob URL, 可在这里 revokeObjectURL(imageUrl)
|
|
|
|
|
+ });
|
|
|
|
|
+ const input = document.createElement('input');
|
|
|
|
|
+ input.type = 'hidden';
|
|
|
|
|
+ input.name = 'img[]';
|
|
|
|
|
+ input.value = imageUrl;
|
|
|
|
|
+
|
|
|
|
|
+ card.appendChild(img);
|
|
|
|
|
+ card.appendChild(input);
|
|
|
|
|
+ card.appendChild(delBtn);
|
|
|
|
|
+ gallery.appendChild(card);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 1. 从后端获取上传凭证(STS 临时授权参数)
|
|
|
|
|
+async function getUploadCredentials() {
|
|
|
|
|
+ const response = await fetch('/aliyuntp/get_oss_sign'); // 替换成你的后端接口地址
|
|
|
|
|
+ if (!response.ok) throw new Error('获取上传凭证失败');
|
|
|
|
|
+ const data = await response.json();
|
|
|
|
|
+ // 后端需返回: { region, accessKeyId, accessKeySecret, stsToken, bucket, dir }
|
|
|
|
|
+ return data;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+// 处理文件上传的核心流程
|
|
|
|
|
+async function handleFiles(files) {
|
|
|
|
|
+ if (!files || files.length === 0) return;
|
|
|
|
|
+
|
|
|
|
|
+ // 获取后端凭证
|
|
|
|
|
+ let res = await getUploadCredentials();
|
|
|
|
|
+ let credentials = res.data
|
|
|
|
|
+
|
|
|
|
|
+ // 遍历所有选择的文件(支持多选)
|
|
|
|
|
+ for (let i = 0; i < files.length; i++) {
|
|
|
|
|
+ const file = files[i];
|
|
|
|
|
+ // 显示每张图片的上传状态(可以统一显示)
|
|
|
|
|
+ showStatus(`正在上传: ${file.name}...`, true);
|
|
|
|
|
+ const fileId = `upload_${Date.now()}_${i}`;
|
|
|
|
|
+ try {
|
|
|
|
|
+ //获取上传凭证
|
|
|
|
|
+ const imgDataUrl = await uploadSingleFile(file,credentials, (percent) => {
|
|
|
|
|
+ updateProgress(fileId, percent);
|
|
|
|
|
+ });
|
|
|
|
|
+ // 上传成功,添加到页面
|
|
|
|
|
+ addImageToGallery(imgDataUrl, file.name,file.type);
|
|
|
|
|
+ showStatus(`${file.name} 上传成功!`, false);
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ console.error(err);
|
|
|
|
|
+ showStatus(`${file.name} 失败: ${err.message}`, false);
|
|
|
|
|
+ // 错误提示2秒后恢复
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ if (statusDiv.innerText.includes(file.name)) statusDiv.innerHTML = '';
|
|
|
|
|
+ }, 2500);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ // 清空 input 的值,允许重复上传同一个文件
|
|
|
|
|
+ fileInput.value = '';
|
|
|
|
|
+ // 最后清空上传中状态如果没有更多任务
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ if (statusDiv.innerHTML.includes('正在上传') === false && statusDiv.innerHTML !== '') {
|
|
|
|
|
+ // 保留最后一条成功提示但2秒后已经清空,不做额外处理
|
|
|
|
|
+ }
|
|
|
|
|
+ }, 500);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function updateProgress(fileId, percent) {
|
|
|
|
|
+ console.log(`{$fileId}上传进度: ${percent}%`);
|
|
|
|
|
+ // const fill = document.getElementById(`progress-${fileId}`);
|
|
|
|
|
+ // if (fill) fill.style.width = percent + '%';
|
|
|
|
|
+}
|
|
|
|
|
+// 点击上传区域触发文件选择
|
|
|
|
|
+uploadZone.addEventListener('click', (e) => {
|
|
|
|
|
+ // 防止点到内部冒泡导致重复触发
|
|
|
|
|
+ if (e.target === uploadZone || uploadZone.contains(e.target)) {
|
|
|
|
|
+ fileInput.click();
|
|
|
|
|
+ }
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+// 文件选择变化时触发上传
|
|
|
|
|
+fileInput.addEventListener('change', (e) => {
|
|
|
|
|
+ const files = e.target.files;
|
|
|
|
|
+ if (files.length) {
|
|
|
|
|
+ handleFiles(files);
|
|
|
|
|
+ }
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+// 阻止拖拽默认事件 (让拖拽体验更好, 可选)
|
|
|
|
|
+uploadZone.addEventListener('dragover', (e) => {
|
|
|
|
|
+ e.preventDefault();
|
|
|
|
|
+ uploadZone.style.borderColor = '#3b82f6';
|
|
|
|
|
+ uploadZone.style.background = '#eff6ff';
|
|
|
|
|
+});
|
|
|
|
|
+uploadZone.addEventListener('dragleave', (e) => {
|
|
|
|
|
+ e.preventDefault();
|
|
|
|
|
+ uploadZone.style.borderColor = '#cbd5e1';
|
|
|
|
|
+ uploadZone.style.background = '#f8fafc';
|
|
|
|
|
+});
|
|
|
|
|
+uploadZone.addEventListener('drop', (e) => {
|
|
|
|
|
+ e.preventDefault();
|
|
|
|
|
+ uploadZone.style.borderColor = '#cbd5e1';
|
|
|
|
|
+ uploadZone.style.background = '#f8fafc';
|
|
|
|
|
+ const files = e.dataTransfer.files;
|
|
|
|
|
+ if (files.length) {
|
|
|
|
|
+ handleFiles(files);
|
|
|
|
|
+ }
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+</script>
|
|
|
|
|
+<script type="text/javascript" src="{$theme}js/aliyun-oss-sdk-6.20.0.min.js"></script>
|
|
|
|
|
+<script type="text/javascript" src="{$theme}js/ajaxupload.3.5.js"></script>
|
|
|
|
|
+{Template footer}
|