feat(purchase): 소모품 사진 기능 — 검색 썸네일 + 신규/기존 품목 마스터 사진 등록
- 모바일 검색 결과에 품목 사진 썸네일 표시 (photo_path 있으면 이미지, 없으면 아이콘) - 데스크탑 검색 드롭다운에도 사진 썸네일 추가 - 신규 품목 등록 시 사진 촬영 → consumable_items.photo_path에 저장 (bulk API) - 기존 품목에 사진 없을 때 장바구니에서 "품목 사진 등록" → PUT /consumable-items/:id/photo - imageUploadService에 consumables 디렉토리 추가 - HEIC 변환 + 폴백 지원 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -225,9 +225,14 @@ const PurchaseRequestController = {
|
|||||||
if (existing.length > 0) {
|
if (existing.length > 0) {
|
||||||
itemId = existing[0].item_id;
|
itemId = existing[0].item_id;
|
||||||
} else {
|
} else {
|
||||||
|
// 신규 품목 사진 저장 (마스터에)
|
||||||
|
let itemPhotoPath = null;
|
||||||
|
if (item.item_photo) {
|
||||||
|
itemPhotoPath = await saveBase64Image(item.item_photo, 'item', 'consumables');
|
||||||
|
}
|
||||||
const [ins] = await conn.query(
|
const [ins] = await conn.query(
|
||||||
`INSERT INTO consumable_items (item_name, spec, maker, category, is_active) VALUES (?, ?, ?, ?, 1)`,
|
`INSERT INTO consumable_items (item_name, spec, maker, category, photo_path, is_active) VALUES (?, ?, ?, ?, ?, 1)`,
|
||||||
[item.item_name.trim(), item.spec || null, item.maker || null, item.category || 'consumable']
|
[item.item_name.trim(), item.spec || null, item.maker || null, item.category || 'consumable', itemPhotoPath]
|
||||||
);
|
);
|
||||||
itemId = ins.insertId;
|
itemId = ins.insertId;
|
||||||
newItemRegistered = true;
|
newItemRegistered = true;
|
||||||
@@ -261,6 +266,25 @@ const PurchaseRequestController = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 품목 마스터 사진 등록/업데이트
|
||||||
|
updateItemPhoto: async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { photo } = req.body;
|
||||||
|
if (!photo) return res.status(400).json({ success: false, message: '사진을 첨부해주세요.' });
|
||||||
|
const itemPhotoPath = await saveBase64Image(photo, 'item', 'consumables');
|
||||||
|
if (!itemPhotoPath) return res.status(500).json({ success: false, message: '사진 저장에 실패했습니다.' });
|
||||||
|
|
||||||
|
const { getDb } = require('../dbPool');
|
||||||
|
const db = await getDb();
|
||||||
|
await db.query('UPDATE consumable_items SET photo_path = ? WHERE item_id = ?', [itemPhotoPath, req.params.id]);
|
||||||
|
koreanSearch.clearCache();
|
||||||
|
res.json({ success: true, data: { photo_path: itemPhotoPath }, message: '품목 사진이 등록되었습니다.' });
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('updateItemPhoto error:', err);
|
||||||
|
res.status(500).json({ success: false, message: '서버 오류가 발생했습니다.' });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// 스마트 검색 (초성 + 별칭 + substring)
|
// 스마트 검색 (초성 + 별칭 + substring)
|
||||||
search: async (req, res) => {
|
search: async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ const requirePage = createRequirePage(getDb);
|
|||||||
|
|
||||||
// 보조 데이터
|
// 보조 데이터
|
||||||
router.get('/consumable-items', ctrl.getConsumableItems);
|
router.get('/consumable-items', ctrl.getConsumableItems);
|
||||||
|
router.put('/consumable-items/:id/photo', ctrl.updateItemPhoto);
|
||||||
router.get('/vendors', ctrl.getVendors);
|
router.get('/vendors', ctrl.getVendors);
|
||||||
router.get('/search', ctrl.search);
|
router.get('/search', ctrl.search);
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ const UPLOAD_DIRS = {
|
|||||||
issues: path.join(__dirname, '../uploads/issues'),
|
issues: path.join(__dirname, '../uploads/issues'),
|
||||||
equipments: path.join(__dirname, '../uploads/equipments'),
|
equipments: path.join(__dirname, '../uploads/equipments'),
|
||||||
purchase_requests: path.join(__dirname, '../uploads/purchase_requests'),
|
purchase_requests: path.join(__dirname, '../uploads/purchase_requests'),
|
||||||
purchase_received: path.join(__dirname, '../uploads/purchase_received')
|
purchase_received: path.join(__dirname, '../uploads/purchase_received'),
|
||||||
|
consumables: path.join(__dirname, '../uploads/consumables')
|
||||||
};
|
};
|
||||||
const UPLOAD_DIR = UPLOAD_DIRS.issues; // 기존 호환성 유지
|
const UPLOAD_DIR = UPLOAD_DIRS.issues; // 기존 호환성 유지
|
||||||
const MAX_SIZE = { width: 1920, height: 1920 };
|
const MAX_SIZE = { width: 1920, height: 1920 };
|
||||||
|
|||||||
@@ -184,6 +184,26 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.pm-search-results.open { display: block; }
|
.pm-search-results.open { display: block; }
|
||||||
|
.pm-search-thumb {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 6px;
|
||||||
|
object-fit: cover;
|
||||||
|
flex-shrink: 0;
|
||||||
|
background: #f3f4f6;
|
||||||
|
}
|
||||||
|
.pm-search-thumb-empty {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #d1d5db;
|
||||||
|
font-size: 14px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
.pm-search-item {
|
.pm-search-item {
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@@ -255,6 +275,25 @@
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
.pm-cart-thumb {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 6px;
|
||||||
|
object-fit: cover;
|
||||||
|
flex-shrink: 0;
|
||||||
|
background: #f3f4f6;
|
||||||
|
}
|
||||||
|
.pm-cart-photo-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 3px 8px;
|
||||||
|
border: 1px dashed #d1d5db;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #6b7280;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
.pm-cart-remove {
|
.pm-cart-remove {
|
||||||
width: 24px;
|
width: 24px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
<title>소모품 신청 - TK 공장관리</title>
|
<title>소모품 신청 - TK 공장관리</title>
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
<link rel="stylesheet" href="/static/css/tkfb.css?v=2026040103">
|
<link rel="stylesheet" href="/static/css/tkfb.css?v=2026040104">
|
||||||
<link rel="stylesheet" href="/css/purchase-mobile.css?v=2026040103">
|
<link rel="stylesheet" href="/css/purchase-mobile.css?v=2026040104">
|
||||||
<script src="https://cdn.jsdelivr.net/npm/heic2any@0.0.4/dist/heic2any.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/heic2any@0.0.4/dist/heic2any.min.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-gray-50">
|
<body class="bg-gray-50">
|
||||||
@@ -97,8 +97,8 @@
|
|||||||
<div class="pm-sheet-body" id="detailContent"></div>
|
<div class="pm-sheet-body" id="detailContent"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/js/tkfb-core.js?v=2026040103"></script>
|
<script src="/static/js/tkfb-core.js?v=2026040104"></script>
|
||||||
<script src="/static/js/purchase-request-mobile.js?v=2026040103"></script>
|
<script src="/static/js/purchase-request-mobile.js?v=2026040104"></script>
|
||||||
<script src="/static/js/shared-bottom-nav.js?v=2026040102"></script>
|
<script src="/static/js/shared-bottom-nav.js?v=2026040102"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -312,7 +312,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="/static/js/tkfb-core.js?v=2026040103"></script>
|
<script src="/static/js/tkfb-core.js?v=2026040104"></script>
|
||||||
<script src="/static/js/purchase-request.js?v=2026040103"></script>
|
<script src="/static/js/purchase-request.js?v=2026040104"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -186,7 +186,9 @@ function renderSearchResults(items, query) {
|
|||||||
const matchLabel = MATCH_LABELS[item._matchType] || '';
|
const matchLabel = MATCH_LABELS[item._matchType] || '';
|
||||||
const spec = item.spec ? ' [' + escapeHtml(item.spec) + ']' : '';
|
const spec = item.spec ? ' [' + escapeHtml(item.spec) + ']' : '';
|
||||||
const maker = item.maker ? ' (' + escapeHtml(item.maker) + ')' : '';
|
const maker = item.maker ? ' (' + escapeHtml(item.maker) + ')' : '';
|
||||||
|
const photoUrl = item.photo_path ? (item.photo_path.startsWith('http') ? item.photo_path : TKUSER_BASE_URL + item.photo_path) : '';
|
||||||
html += `<div class="pm-search-item" onclick="addToCart(${item.item_id})">
|
html += `<div class="pm-search-item" onclick="addToCart(${item.item_id})">
|
||||||
|
${photoUrl ? `<img src="${photoUrl}" class="pm-search-thumb" onerror="this.style.display='none'">` : '<div class="pm-search-thumb-empty"><i class="fas fa-box"></i></div>'}
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="text-sm font-medium">${escapeHtml(item.item_name)}${spec}${maker}</div>
|
<div class="text-sm font-medium">${escapeHtml(item.item_name)}${spec}${maker}</div>
|
||||||
${catLabel ? `<div class="text-xs text-gray-400 mt-0.5">${catLabel}</div>` : ''}
|
${catLabel ? `<div class="text-xs text-gray-400 mt-0.5">${catLabel}</div>` : ''}
|
||||||
@@ -219,6 +221,7 @@ function addToCart(itemId) {
|
|||||||
spec: item.spec,
|
spec: item.spec,
|
||||||
maker: item.maker,
|
maker: item.maker,
|
||||||
category: item.category,
|
category: item.category,
|
||||||
|
photo_path: item.photo_path,
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
notes: '',
|
notes: '',
|
||||||
is_new: false
|
is_new: false
|
||||||
@@ -239,6 +242,8 @@ function addNewToCart() {
|
|||||||
spec: '',
|
spec: '',
|
||||||
maker: '',
|
maker: '',
|
||||||
category: '',
|
category: '',
|
||||||
|
photo_path: null,
|
||||||
|
item_photo: null, // base64 — 마스터 사진으로 저장될 사진
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
notes: '',
|
notes: '',
|
||||||
is_new: true
|
is_new: true
|
||||||
@@ -285,9 +290,18 @@ function renderCart() {
|
|||||||
const maker = c.maker ? ' (' + escapeHtml(c.maker) + ')' : '';
|
const maker = c.maker ? ' (' + escapeHtml(c.maker) + ')' : '';
|
||||||
const catLabel = CAT_LABELS[c.category] || '';
|
const catLabel = CAT_LABELS[c.category] || '';
|
||||||
|
|
||||||
let newFields = '';
|
// 사진 썸네일
|
||||||
|
let thumbHtml = '';
|
||||||
|
if (c.item_photo) {
|
||||||
|
thumbHtml = `<img src="${c.item_photo}" class="pm-cart-thumb">`;
|
||||||
|
} else if (c.photo_path) {
|
||||||
|
const url = c.photo_path.startsWith('http') ? c.photo_path : TKUSER_BASE_URL + c.photo_path;
|
||||||
|
thumbHtml = `<img src="${url}" class="pm-cart-thumb" onerror="this.style.display='none'">`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let extraFields = '';
|
||||||
if (c.is_new) {
|
if (c.is_new) {
|
||||||
newFields = `<div class="pm-cart-new-fields">
|
extraFields = `<div class="pm-cart-new-fields">
|
||||||
<input type="text" placeholder="규격" value="${escapeHtml(c.spec || '')}" oninput="updateCartNewField(${idx},'spec',this.value)">
|
<input type="text" placeholder="규격" value="${escapeHtml(c.spec || '')}" oninput="updateCartNewField(${idx},'spec',this.value)">
|
||||||
<input type="text" placeholder="제조사" value="${escapeHtml(c.maker || '')}" oninput="updateCartNewField(${idx},'maker',this.value)">
|
<input type="text" placeholder="제조사" value="${escapeHtml(c.maker || '')}" oninput="updateCartNewField(${idx},'maker',this.value)">
|
||||||
<select onchange="updateCartNewField(${idx},'category',this.value)">
|
<select onchange="updateCartNewField(${idx},'category',this.value)">
|
||||||
@@ -297,14 +311,29 @@ function renderCart() {
|
|||||||
<option value="repair" ${c.category==='repair'?'selected':''}>수선비</option>
|
<option value="repair" ${c.category==='repair'?'selected':''}>수선비</option>
|
||||||
<option value="equipment" ${c.category==='equipment'?'selected':''}>설비</option>
|
<option value="equipment" ${c.category==='equipment'?'selected':''}>설비</option>
|
||||||
</select>
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="pm-cart-new-fields" style="margin-top:4px">
|
||||||
|
<label class="pm-cart-photo-btn">
|
||||||
|
<i class="fas fa-camera"></i> ${c.item_photo ? '사진 변경' : '품목 사진'}
|
||||||
|
<input type="file" accept="image/*,.heic,.heif" capture="environment" class="hidden" onchange="onItemPhotoSelected(${idx},this)">
|
||||||
|
</label>
|
||||||
|
</div>`;
|
||||||
|
} else if (!c.photo_path) {
|
||||||
|
// 기존 품목이지만 사진 없음 → 사진 등록 가능
|
||||||
|
extraFields = `<div class="pm-cart-new-fields" style="margin-top:4px">
|
||||||
|
<label class="pm-cart-photo-btn">
|
||||||
|
<i class="fas fa-camera"></i> 품목 사진 등록
|
||||||
|
<input type="file" accept="image/*,.heic,.heif" capture="environment" class="hidden" onchange="uploadExistingItemPhoto(${idx},this)">
|
||||||
|
</label>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `<div class="pm-cart-item">
|
return `<div class="pm-cart-item">
|
||||||
|
${thumbHtml}
|
||||||
<div class="pm-cart-item-info">
|
<div class="pm-cart-item-info">
|
||||||
<div class="pm-cart-item-name">${escapeHtml(c.item_name)}${spec}${maker}</div>
|
<div class="pm-cart-item-name">${escapeHtml(c.item_name)}${spec}${maker}</div>
|
||||||
<div class="pm-cart-item-meta">${catLabel}${c.is_new ? ' <span class="pm-cart-item-new">(신규등록)</span>' : ''}</div>
|
<div class="pm-cart-item-meta">${catLabel}${c.is_new ? ' <span class="pm-cart-item-new">(신규등록)</span>' : ''}</div>
|
||||||
${newFields}
|
${extraFields}
|
||||||
</div>
|
</div>
|
||||||
<input type="number" class="pm-cart-qty" value="${c.quantity}" min="1" inputmode="numeric" onchange="updateCartQty(${idx},this.value)">
|
<input type="number" class="pm-cart-qty" value="${c.quantity}" min="1" inputmode="numeric" onchange="updateCartQty(${idx},this.value)">
|
||||||
<input type="text" class="pm-cart-memo" placeholder="메모" value="${escapeHtml(c.notes || '')}" oninput="updateCartNotes(${idx},this.value)">
|
<input type="text" class="pm-cart-memo" placeholder="메모" value="${escapeHtml(c.notes || '')}" oninput="updateCartNotes(${idx},this.value)">
|
||||||
@@ -324,7 +353,61 @@ function updateSubmitBtn() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== 사진 ===== */
|
/* ===== 품목 사진 (장바구니 내 신규/기존 품목) ===== */
|
||||||
|
async function onItemPhotoSelected(idx, inputEl) {
|
||||||
|
const file = inputEl.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
let processFile = file;
|
||||||
|
const isHeic = file.type === 'image/heic' || file.type === 'image/heif' ||
|
||||||
|
file.name.toLowerCase().endsWith('.heic') || file.name.toLowerCase().endsWith('.heif');
|
||||||
|
if (isHeic && typeof heic2any !== 'undefined') {
|
||||||
|
try {
|
||||||
|
const blob = await heic2any({ blob: file, toType: 'image/jpeg', quality: 0.85 });
|
||||||
|
processFile = new File([blob], file.name.replace(/\.hei[cf]$/i, '.jpg'), { type: 'image/jpeg' });
|
||||||
|
} catch (e) { console.warn('HEIC 변환 실패:', e); }
|
||||||
|
}
|
||||||
|
if (processFile.size > 10 * 1024 * 1024) { showToast('10MB 이하만 가능합니다.', 'error'); return; }
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
cartItems[idx].item_photo = e.target.result;
|
||||||
|
renderCart();
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(processFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 기존 품목(사진 없음)에 사진 업로드 → 마스터에 저장
|
||||||
|
async function uploadExistingItemPhoto(idx, inputEl) {
|
||||||
|
const file = inputEl.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
let processFile = file;
|
||||||
|
const isHeic = file.type === 'image/heic' || file.type === 'image/heif' ||
|
||||||
|
file.name.toLowerCase().endsWith('.heic') || file.name.toLowerCase().endsWith('.heif');
|
||||||
|
if (isHeic && typeof heic2any !== 'undefined') {
|
||||||
|
try {
|
||||||
|
const blob = await heic2any({ blob: file, toType: 'image/jpeg', quality: 0.85 });
|
||||||
|
processFile = new File([blob], file.name.replace(/\.hei[cf]$/i, '.jpg'), { type: 'image/jpeg' });
|
||||||
|
} catch (e) { console.warn('HEIC 변환 실패:', e); }
|
||||||
|
}
|
||||||
|
if (processFile.size > 10 * 1024 * 1024) { showToast('10MB 이하만 가능합니다.', 'error'); return; }
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = async (e) => {
|
||||||
|
const base64 = e.target.result;
|
||||||
|
try {
|
||||||
|
const res = await api(`/purchase-requests/consumable-items/${cartItems[idx].item_id}/photo`, {
|
||||||
|
method: 'PUT', body: JSON.stringify({ photo: base64 })
|
||||||
|
});
|
||||||
|
if (res.data?.photo_path) {
|
||||||
|
cartItems[idx].photo_path = res.data.photo_path;
|
||||||
|
cartItems[idx].item_photo = null;
|
||||||
|
}
|
||||||
|
showToast('품목 사진이 등록되었습니다.');
|
||||||
|
renderCart();
|
||||||
|
} catch (err) { showToast(err.message, 'error'); }
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(processFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 참고 사진 (신청 공통) ===== */
|
||||||
async function onMobilePhotoSelected(inputEl) {
|
async function onMobilePhotoSelected(inputEl) {
|
||||||
const file = inputEl.files[0];
|
const file = inputEl.files[0];
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
@@ -364,7 +447,7 @@ async function submitRequest() {
|
|||||||
try {
|
try {
|
||||||
const items = cartItems.map(c => {
|
const items = cartItems.map(c => {
|
||||||
if (c.is_new) {
|
if (c.is_new) {
|
||||||
return { item_name: c.item_name, spec: c.spec || null, maker: c.maker || null, category: c.category || null, quantity: c.quantity, notes: c.notes || null, is_new: true };
|
return { item_name: c.item_name, spec: c.spec || null, maker: c.maker || null, category: c.category || null, quantity: c.quantity, notes: c.notes || null, is_new: true, item_photo: c.item_photo || null };
|
||||||
}
|
}
|
||||||
return { item_id: c.item_id, quantity: c.quantity, notes: c.notes || null };
|
return { item_id: c.item_id, quantity: c.quantity, notes: c.notes || null };
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -130,7 +130,9 @@ function showDropdown(items, query) {
|
|||||||
const fg = CAT_FG[item.category] || '#374151';
|
const fg = CAT_FG[item.category] || '#374151';
|
||||||
const spec = _fmtSpec(item.spec ? escapeHtml(item.spec) : '');
|
const spec = _fmtSpec(item.spec ? escapeHtml(item.spec) : '');
|
||||||
const maker = item.maker ? ` (${escapeHtml(item.maker)})` : '';
|
const maker = item.maker ? ` (${escapeHtml(item.maker)})` : '';
|
||||||
|
const photoSrc = item.photo_path ? (item.photo_path.startsWith('http') ? item.photo_path : TKUSER_BASE_URL + item.photo_path) : '';
|
||||||
html += `<div class="item-dropdown-item" data-item-id="${item.item_id}" onclick="selectItem(${item.item_id})">
|
html += `<div class="item-dropdown-item" data-item-id="${item.item_id}" onclick="selectItem(${item.item_id})">
|
||||||
|
${photoSrc ? `<img src="${photoSrc}" style="width:28px;height:28px;border-radius:4px;object-fit:cover" onerror="this.style.display='none'">` : ''}
|
||||||
<span class="cat-tag" style="background:${bg};color:${fg}">${catLabel}</span>
|
<span class="cat-tag" style="background:${bg};color:${fg}">${catLabel}</span>
|
||||||
<span>${escapeHtml(item.item_name)}${spec}${maker}</span>
|
<span>${escapeHtml(item.item_name)}${spec}${maker}</span>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|||||||
Reference in New Issue
Block a user