Files

1360 lines
52 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(() => {
const apiBase = '/api/style_check'
// 自动化接口走相对路径,由 8976 web 引擎内部反代到 8977;如需直连可改为 'http://host:8977'
const AUTOMATION_API = ''
const el = id => document.getElementById(id)
const state = {
modalResolver: null,
aiCountdownTimer: 0,
profileKey: 'mature_relaxed',
profiles: {
mature_relaxed: { name: '中老年休闲沉稳版', slogan: '稳重有质感,低饱和、利落、耐看' },
mature_fashion40: { name: '中老年流行时尚版', slogan: '50岁按40岁时尚审美:懂流行、有表达、不另类' }
}
}
const sceneText = v => ({ commute: '通勤', weekend: '周末轻运动', business_light: '轻商务会面' }[v] || v || '')
const slotText = v => ({ top: '上衣', bottom: '下装', shoes: '鞋', outer: '外搭', accessory: '配饰' }[v] || v || '')
const profileText = v => (state.profiles[v] ? state.profiles[v].name : v)
const withProfileUrl = rawUrl => {
const u = new URL(rawUrl, window.location.origin)
u.searchParams.set('profile_key', state.profileKey)
return `${u.pathname}${u.search}`
}
const req = async (url, options = {}) => {
const res = await fetch(url, { credentials: 'same-origin', cache: 'no-store', ...options })
const data = await res.json().catch(() => ({}))
if (!res.ok || !data.ok) throw new Error(data.error || `HTTP_${res.status}`)
return data
}
// ===== 调试跟踪模态窗:记录爬虫开关请求的完整响应链 =====
const trace = {
open(title) {
const m = el('debugTraceModal')
if (!m) return
el('debugTraceTitle').textContent = title || '调试跟踪'
el('debugTraceStatus').textContent = '运行中...'
el('debugTraceStatus').style.color = '#64748b'
el('debugTraceContent').textContent = ''
m.style.display = 'flex'
},
line(text) {
const pre = el('debugTraceContent')
if (!pre) return
const ts = new Date().toISOString().slice(11, 23)
pre.textContent += `[${ts}] ${text}\n`
pre.scrollTop = pre.scrollHeight
},
json(label, obj) {
let s
try { s = JSON.stringify(obj, null, 2) } catch { s = String(obj) }
this.line(label + ' →\n' + s)
},
done(ok, msg) {
const st = el('debugTraceStatus')
if (st) {
st.textContent = ok ? ('✅ ' + (msg || '完成')) : ('❌ ' + (msg || '已中断'))
st.style.color = ok ? '#16a34a' : '#dc2626'
}
this.line(ok ? '===== END (OK) =====' : '===== END (ERROR,保留现场以供拷贝) =====')
}
}
// 带详细跟踪的 fetch:打印请求 URL/method/status/headers/raw body。任意环节出错报错并保留模态窗
const traceFetch = async (label, url, options = {}) => {
trace.line(`→ ${label}`)
trace.line(` URL : ${url}`)
trace.line(` Method : ${(options.method || 'GET').toUpperCase()}`)
if (options.body) trace.line(` Body : ${options.body}`)
let res
try {
res = await fetch(url, { credentials: 'same-origin', cache: 'no-store', ...options })
} catch (netErr) {
trace.line(` ❌ NETWORK ERROR: ${netErr.name} - ${netErr.message}`)
trace.line(` 可能原因:8976 web 引擎未启动、IIS 反代未生效、请求被防火墙拦截、URL 有误`)
throw netErr
}
trace.line(` Status : ${res.status} ${res.statusText}`)
trace.line(` CT : ${res.headers.get('content-type') || '(none)'}`)
const raw = await res.text()
trace.line(` RawLen : ${raw.length}`)
if (raw.length <= 2000) {
trace.line(` RawBody: ${raw || '(empty)'}`)
} else {
trace.line(` RawBody(前2000字): ${raw.slice(0, 2000)} ...`)
}
let json = null
try { json = raw ? JSON.parse(raw) : null } catch (e) {
trace.line(` ⚠ 响应不是合法 JSON:${e.message}`)
}
if (json) trace.json(' JSON', json)
return { res, raw, json }
}
const showMsg = (text) => {
el('msg').textContent = text || ''
}
const applyProfileTheme = () => {
document.body.setAttribute('data-profile', state.profileKey)
const relaxedBtn = el('tabRelaxed')
const fashionBtn = el('tabFashion40')
const waveBtn = el('tabWave')
const koreanBtn = el('tabKorean')
const zhihuBtn = el('tabZhihu')
const xBtn = el('tabX')
if (relaxedBtn) relaxedBtn.classList.toggle('active', state.profileKey === 'mature_relaxed')
if (fashionBtn) fashionBtn.classList.toggle('active', state.profileKey === 'mature_fashion40')
if (waveBtn) waveBtn.classList.remove('active')
if (koreanBtn) koreanBtn.classList.remove('active')
if (zhihuBtn) zhihuBtn.classList.remove('active')
if (xBtn) xBtn.classList.remove('active')
el('mainSections').style.display = 'block'
el('waveSection').style.display = 'none'
el('koreanSection').style.display = 'none'
el('zhihuSection').style.display = 'none'
el('xSection').style.display = 'none'
const sloganEl = el('profileSlogan')
if (sloganEl) {
const p = state.profiles[state.profileKey] || {}
sloganEl.textContent = `${p.name || ''} · ${p.slogan || ''}`
}
}
const setProfile = async profileKey => {
const nextKey = profileKey === 'mature_fashion40' ? 'mature_fashion40' : 'mature_relaxed'
state.profileKey = nextKey
localStorage.setItem('style_check_profile_key', nextKey)
applyProfileTheme()
resetForm()
resetLibForm()
el('aiAuditResult').textContent = `请上传穿搭照片后开始AI审核(当前:${profileText(nextKey)})`
el('aiAuditIssues').textContent = ''
await Promise.all([loadRules(), loadOutfits(), loadRecentChecks(), loadToday(), loadLibrary(), loadUnmatchedReport()])
}
const closeModal = (value) => {
const mask = el('appModal')
if (!mask) return
mask.classList.remove('show')
const resolver = state.modalResolver
state.modalResolver = null
if (resolver) resolver(value)
}
const openModal = ({ title, message, okText = '确定', cancelText = '', danger = false, allowHtml = false }) => {
const mask = el('appModal')
const titleBox = el('modalTitle')
const msgBox = el('modalMessage')
const actions = el('modalActions')
if (!mask || !titleBox || !msgBox || !actions) return Promise.resolve(true)
titleBox.textContent = String(title || '')
if (allowHtml) msgBox.innerHTML = String(message || '')
else msgBox.textContent = String(message || '')
actions.innerHTML = ''
const okBtn = document.createElement('button')
okBtn.textContent = okText
if (danger) okBtn.className = 'danger'
okBtn.onclick = () => closeModal(true)
if (cancelText) {
const cancelBtn = document.createElement('button')
cancelBtn.textContent = cancelText
cancelBtn.className = 'secondary'
cancelBtn.onclick = () => closeModal(false)
actions.appendChild(cancelBtn)
}
actions.appendChild(okBtn)
mask.classList.add('show')
return new Promise(resolve => {
state.modalResolver = resolve
})
}
const modalAlert = (title, message) => openModal({ title, message, okText: '我知道了' })
const modalConfirm = (title, message, okText = '确认', cancelText = '取消', danger = false) => openModal({ title, message, okText, cancelText, danger })
const showProgressModal = title => {
const mask = el('appModal')
const titleBox = el('modalTitle')
const msgBox = el('modalMessage')
const actions = el('modalActions')
if (!mask || !titleBox || !msgBox || !actions) return { log: () => {}, success: () => Promise.resolve(), fail: () => {} }
titleBox.textContent = String(title || '')
msgBox.textContent = ''
actions.innerHTML = ''
const closeBtn = document.createElement('button')
closeBtn.textContent = '关闭'
closeBtn.className = 'secondary'
closeBtn.style.display = 'none'
closeBtn.onclick = () => closeModal(false)
actions.appendChild(closeBtn)
mask.classList.add('show')
const logLines = []
const render = () => {
msgBox.textContent = logLines.join('\n')
}
const api = {
log: line => {
logLines.push(String(line || ''))
render()
},
async success(finalLine, countdownSec = 3) {
if (finalLine) {
logLines.push(String(finalLine))
render()
}
for (let s = countdownSec; s > 0; s -= 1) {
logLines.push(`成功,${s}s 后自动关闭...`)
render()
await new Promise(resolve => { state.aiCountdownTimer = window.setTimeout(resolve, 1000) })
logLines.pop()
}
closeModal(true)
},
fail(errLine) {
if (state.aiCountdownTimer) {
clearTimeout(state.aiCountdownTimer)
state.aiCountdownTimer = 0
}
logLines.push(String(errLine || '处理失败'))
logLines.push('请根据以上错误信息排查。')
render()
closeBtn.style.display = ''
}
}
return api
}
const toDataUrl = file => new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = () => resolve(String(reader.result || ''))
reader.onerror = () => reject(new Error('读取图片失败'))
reader.readAsDataURL(file)
})
const resetForm = () => {
el('fId').value = ''
el('fName').value = ''
el('fScene').value = 'commute'
el('fTempZone').value = '12-22℃'
el('fTop').value = ''
el('fBottom').value = ''
el('fShoes').value = ''
el('fOuter').value = ''
el('fAccessory').value = ''
el('fNotes').value = ''
}
const resetLibForm = () => {
el('libItemId').value = ''
el('libItemName').value = ''
el('libItemSlot').value = 'top'
el('libItemCategory').value = ''
el('libItemTempZone').value = 'ALL'
el('libItemBrands').value = ''
el('libItemPriceMin').value = ''
el('libItemPriceMax').value = ''
el('libItemKeywords').value = ''
el('libItemNotes').value = ''
}
const fillLibForm = (row) => {
el('libItemId').value = row.id || ''
el('libItemName').value = row.item_name || ''
el('libItemSlot').value = row.slot || 'top'
el('libItemCategory').value = row.category || ''
el('libItemTempZone').value = row.temp_zone || 'ALL'
el('libItemBrands').value = row.brand_options || ''
el('libItemPriceMin').value = Number.isFinite(Number(row.price_min)) ? row.price_min : ''
el('libItemPriceMax').value = Number.isFinite(Number(row.price_max)) ? row.price_max : ''
el('libItemKeywords').value = row.keywords || ''
el('libItemNotes').value = row.notes || ''
}
const fillForm = (row) => {
el('fId').value = row.id
el('fName').value = row.name || ''
el('fScene').value = row.scene || 'commute'
el('fTempZone').value = row.temp_zone || '12-22℃'
el('fTop').value = row.top_desc || ''
el('fBottom').value = row.bottom_desc || ''
el('fShoes').value = row.shoes_desc || ''
el('fOuter').value = row.outer_desc || ''
el('fAccessory').value = row.accessory_desc || ''
el('fNotes').value = row.notes || ''
}
const loadOutfits = async () => {
const scene = el('filterScene').value
const tempZone = el('filterTempZone').value
const q = el('filterQ').value.trim()
const sp = new URLSearchParams()
if (scene) sp.set('scene', scene)
if (tempZone) sp.set('temp_zone', tempZone)
if (q) sp.set('q', q)
sp.set('include_links', '1')
const data = await req(withProfileUrl(`${apiBase}/outfits?${sp.toString()}`))
const tbody = el('outfitBody')
tbody.innerHTML = ''
data.rows.forEach(row => {
const tr = document.createElement('tr')
tr.innerHTML = `
<td>${row.name || ''}</td>
<td>${sceneText(row.scene)}</td>
<td>${row.temp_zone || ''}</td>
<td>${row.top_desc || ''}</td>
<td>${row.bottom_desc || ''}</td>
<td>${row.shoes_desc || ''}</td>
<td>${row.outer_desc || ''}</td>
<td>${row.accessory_desc || ''}</td>
<td>${row.notes || ''}</td>
<td>${row.linked_items_summary || '-'}</td>
<td>
<div class="row-actions">
<button data-id="${row.id}" data-act="edit">编辑</button>
<button data-id="${row.id}" data-act="del">删除</button>
</div>
</td>
`
tbody.appendChild(tr)
})
tbody.querySelectorAll('button[data-act="edit"]').forEach(btn => {
btn.onclick = () => {
const id = Number(btn.getAttribute('data-id'))
const row = data.rows.find(r => Number(r.id) === id)
if (row) fillForm(row)
}
})
tbody.querySelectorAll('button[data-act="del"]').forEach(btn => {
btn.onclick = async () => {
const id = Number(btn.getAttribute('data-id'))
if (!id) return
const ok = await modalConfirm('确认删除', '确认删除这条搭配吗?删除后不可恢复。', '确定删除', '再想想', true)
if (!ok) return
try {
await req(withProfileUrl(`${apiBase}/outfits/${id}`), { method: 'DELETE' })
await modalAlert('删除成功', '这条搭配已删除。')
await loadOutfits()
await loadUnmatchedReport()
} catch (e) {
await modalAlert('删除失败', String(e.message || e))
}
}
})
}
const loadRules = async () => {
const data = await req(withProfileUrl(`${apiBase}/rules`))
const box = el('ruleList')
box.innerHTML = ''
data.rows.forEach(r => {
const div = document.createElement('label')
div.className = 'sc-rule-item'
div.innerHTML = `
<input type="checkbox" data-id="${r.id}" data-weight="${r.weight}" checked />
<span>[${r.rule_group}] ${r.rule_text}</span>
`
box.appendChild(div)
})
}
const loadLibrary = async () => {
const slot = el('libSlot').value
const tempZone = el('libTempZone').value
const q = el('libQ').value.trim()
const sp = new URLSearchParams()
if (slot) sp.set('slot', slot)
if (tempZone) sp.set('temp_zone', tempZone)
if (q) sp.set('q', q)
const data = await req(withProfileUrl(`${apiBase}/library/items?${sp.toString()}`))
const tbody = el('libBody')
tbody.innerHTML = ''
data.rows.forEach(row => {
const tr = document.createElement('tr')
tr.innerHTML = `
<td>${row.item_name || ''}</td>
<td>${slotText(row.slot)}</td>
<td>${row.category || ''}</td>
<td>${row.temp_zone || ''}</td>
<td>${row.brand_options || ''}</td>
<td>${Number.isFinite(Number(row.price_min)) && Number.isFinite(Number(row.price_max)) ? `${row.price_min}-${row.price_max}` : '-'}</td>
<td>${Number(row.image_count || 0)} 张</td>
<td>${row.keywords || ''}</td>
<td>${row.notes || ''}</td>
<td>
<div class="row-actions">
<button data-id="${row.id}" data-act="viewimg">查看图</button>
<button data-id="${row.id}" data-act="uploadimg">上传图</button>
<button data-id="${row.id}" data-act="edititem">编辑</button>
</div>
</td>
`
tbody.appendChild(tr)
})
tbody.querySelectorAll('button[data-act="viewimg"]').forEach(btn => {
btn.onclick = async () => {
const id = Number(btn.getAttribute('data-id'))
const row = data.rows.find(r => Number(r.id) === id)
if (!row) return
await showItemImages(row)
}
})
tbody.querySelectorAll('button[data-act="uploadimg"]').forEach(btn => {
btn.onclick = async () => {
const id = Number(btn.getAttribute('data-id'))
const row = data.rows.find(r => Number(r.id) === id)
if (!row) return
await uploadLibraryImage(row)
}
})
tbody.querySelectorAll('button[data-act="edititem"]').forEach(btn => {
btn.onclick = () => {
const id = Number(btn.getAttribute('data-id'))
const row = data.rows.find(r => Number(r.id) === id)
if (row) fillLibForm(row)
}
})
}
const showItemImages = async (itemRow) => {
const data = await req(withProfileUrl(`${apiBase}/library/items/${itemRow.id}/images`))
const rows = Array.isArray(data.rows) ? data.rows : []
if (!rows.length) {
await modalAlert('图片为空', `【${itemRow.item_name}】暂时没有图片,建议先上传。`)
return
}
const cards = rows.map((r, idx) => {
const url = String(r.image_url || '')
return `
<a href="${url}" target="_blank" rel="noreferrer" style="display:block;text-decoration:none;color:inherit">
<div style="border:1px solid rgba(255,255,255,0.15);border-radius:8px;overflow:hidden;background:rgba(255,255,255,0.04)">
<img src="${url}" alt="img-${idx + 1}" style="width:100%;height:180px;object-fit:cover;display:block" />
<div style="padding:8px 10px;font-size:12px;opacity:.92">#${idx + 1} · ${r.source_type || ''}</div>
</div>
</a>
`
}).join('')
await openModal({
title: `${itemRow.item_name} · 图片附件(${rows.length})`,
message: `<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:10px">${cards}</div>`,
okText: '关闭',
allowHtml: true
})
}
const uploadLibraryImage = async (itemRow) => {
const input = el('libUploadInput')
if (!input) return
input.value = ''
const picked = await new Promise(resolve => {
input.onchange = () => {
const file = input.files && input.files[0]
resolve(file || null)
}
input.click()
})
if (!picked) return
const imageDataUrl = await toDataUrl(picked)
await req(`${apiBase}/library/items/${itemRow.id}/images`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
profile_key: state.profileKey,
image_data_url: imageDataUrl,
caption: `${itemRow.item_name} 上传图片`
})
})
await modalAlert('上传成功', `【${itemRow.item_name}】图片已上传。`)
await loadLibrary()
}
const saveLibraryItem = async () => {
const id = Number(el('libItemId').value || 0)
const payload = {
profile_key: state.profileKey,
item_name: el('libItemName').value.trim(),
slot: el('libItemSlot').value,
category: el('libItemCategory').value.trim(),
temp_zone: el('libItemTempZone').value,
brand_options: el('libItemBrands').value.trim(),
price_min: el('libItemPriceMin').value === '' ? null : Number(el('libItemPriceMin').value),
price_max: el('libItemPriceMax').value === '' ? null : Number(el('libItemPriceMax').value),
keywords: el('libItemKeywords').value.trim(),
notes: el('libItemNotes').value.trim()
}
if (!payload.item_name) {
await modalAlert('无法保存', '服饰名称不能为空。')
return
}
if (!payload.category) {
await modalAlert('无法保存', '服饰类别不能为空。')
return
}
if (id > 0) {
await req(`${apiBase}/library/items/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
})
await modalAlert('保存成功', '服饰条目已更新。')
} else {
await req(`${apiBase}/library/items`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
})
await modalAlert('新增成功', '服饰条目已新增。')
}
resetLibForm()
await loadLibrary()
await loadUnmatchedReport()
}
const loadUnmatchedReport = async () => {
const data = await req(withProfileUrl(`${apiBase}/library/unmatched_report`))
if (!data.rows || !data.rows.length) {
el('unmatchedReport').textContent = '未匹配报告:当前全部已匹配'
return
}
const text = data.rows.map(r => `${r.outfit_name}|${slotText(r.slot)}|${r.text}`).join('\n')
el('unmatchedReport').textContent = `未匹配报告(${data.unmatched_count}条):\n${text}`
}
const rebuildLinks = async () => {
const data = await req(`${apiBase}/library/rebuild_links`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ profile_key: state.profileKey })
})
const rows = Array.isArray(data.unmatched) ? data.unmatched : []
if (!rows.length) {
el('unmatchedReport').textContent = `重建完成:已匹配 ${data.matched_count} 条链接,未匹配 0 条`
return
}
const text = rows.map(r => `${r.outfit_name}|${slotText(r.slot)}|${r.text}`).join('\n')
el('unmatchedReport').textContent = `重建完成:已匹配 ${data.matched_count} 条链接,未匹配 ${data.unmatched_count} 条\n${text}`
await modalAlert('重建完成', `已匹配 ${data.matched_count} 条链接,未匹配 ${data.unmatched_count} 条。`)
}
const fetchLocalImages = async () => {
const progress = showProgressModal('本地化找图处理中')
try {
progress.log(`已开始:按当前版本(${profileText(state.profileKey)})逐条服饰抓取图片并保存到本地...`)
const data = await req(`${apiBase}/library/images/fetch_local`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ profile_key: state.profileKey, target_per_item: 5, force: 1 })
})
progress.log(`处理完成:共 ${data.item_count || 0} 条服饰,缺失(<3张) ${data.missing_count || 0} 条`)
if (Array.isArray(data.missing) && data.missing.length) {
const text = data.missing.map(x => `- ${x.item_name}(${slotText(x.slot)}): ${x.image_count}张`).join('\n')
progress.log(`缺失明细:\n${text}`)
}
await loadLibrary()
await progress.success('本地化找图完成。', 3)
} catch (e) {
progress.fail(`本地化找图失败:${String((e && e.message) || e)}`)
}
}
const renderAiReport = (report) => {
const r = report || {}
const ruleResult = r.rule_result || {}
const lib = r.library_result || {}
const rules = Array.isArray(ruleResult.rules) ? ruleResult.rules : []
const failRules = rules.filter(x => !x.pass).map(x => `- ${x.title}|${x.detail}`).join('\n')
const conflicts = Array.isArray(lib.conflict_list) ? lib.conflict_list.map(x => `- ${x}`).join('\n') : ''
const similar = Array.isArray(lib.similar_outfits) ? lib.similar_outfits.map(x => `- ${x.name}(重叠${x.overlap_count})`).join('\n') : ''
el('aiAuditResult').textContent = [
`结论:${r.result_level || '-'}`,
`分数:${r.total_score || 0}`,
`温区:${r.temp_zone || '-'}`,
`摘要:${r.summary || '-'}`,
similar ? `近似搭配:\n${similar}` : '近似搭配:无'
].join('\n')
el('aiAuditIssues').textContent = [
failRules ? `规则未通过:\n${failRules}` : '规则未通过:无',
conflicts ? `库冲突/未匹配:\n${conflicts}` : '库冲突/未匹配:无'
].join('\n\n')
}
const runAiAudit = async () => {
const file = el('aiImage').files && el('aiImage').files[0]
if (!file) {
await modalAlert('缺少照片', '请先选择穿搭照片,再开始AI审核。')
return
}
const progress = showProgressModal('AI审核处理中')
try {
progress.log('已开始处理。')
progress.log('1/4 正在读取图片...')
const imageDataUrl = await toDataUrl(file)
progress.log('2/4 正在调用AI识别(上衣/下装/鞋/外搭/配饰、颜色、版型、LOGO)...')
const payload = {
profile_key: state.profileKey,
scene: el('aiScene').value,
temp_zone: el('aiTempZone').value || el('tempZone').value,
temp_c: Number(el('tempC').value || NaN),
image_data_url: imageDataUrl
}
const data = await req(`${apiBase}/ai/audit`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
})
progress.log('3/4 正在执行规则校验与库冲突匹配...')
renderAiReport(data.report)
progress.log('4/4 已完成结果渲染。')
await progress.success('AI审核完成。', 3)
} catch (e) {
progress.fail(`AI审核失败:${String((e && e.message) || e)}`)
}
}
const loadRecentChecks = async () => {
const data = await req(withProfileUrl(`${apiBase}/checks/recent?limit=7`))
if (!data.rows.length) {
el('recentChecks').textContent = '最近暂无镜检记录'
return
}
const text = data.rows.map(r => {
const issues = Array.isArray(r.issues) ? r.issues.join(';') : ''
return `${r.check_date}|${sceneText(r.scene)}|${r.temp_zone}|评分${r.total_score}|${r.result_level}${issues ? `|问题:${issues}` : ''}`
}).join('\n')
el('recentChecks').textContent = text
}
const loadToday = async () => {
const scene = el('scene').value
const tempC = el('tempC').value.trim()
const tempZone = el('tempZone').value
const sp = new URLSearchParams()
sp.set('scene', scene)
if (tempC) sp.set('temp_c', tempC)
if (tempZone) sp.set('temp_zone', tempZone)
const data = await req(withProfileUrl(`${apiBase}/today?${sp.toString()}`))
const c = data.chosen
if (!c) {
el('todayBox').textContent = '当前条件暂无可用搭配,请在搭配库新增。'
return
}
const alts = (data.alternatives || []).map(x => `- ${x.name}(${x.temp_zone})`).join('\n')
el('todayBox').textContent = [
`推荐:${c.name}`,
`场景:${sceneText(c.scene)},温区:${c.temp_zone}`,
`上衣:${c.top_desc || '-'}`,
`下装:${c.bottom_desc || '-'}`,
`鞋:${c.shoes_desc || '-'}`,
`外搭:${c.outer_desc || '-'}`,
`配饰:${c.accessory_desc || '-'}`,
`备注:${c.notes || '-'}`,
alts ? `备选:\n${alts}` : ''
].filter(Boolean).join('\n')
}
// === WAVE Gallery & Crawler ===
const loadWaveGallery = async () => {
try {
const data = await req(`${apiBase}/wave?page=1&limit=500`)
state.waveList = data.rows || []
renderWaveGrid()
} catch (e) {
console.error(e)
}
}
const renderWaveGrid = () => {
const grid = el('waveGrid')
grid.innerHTML = ''
state.waveList.forEach((w, idx) => {
const card = document.createElement('div')
card.className = 'wave-card'
card.innerHTML = `
<img src="${w.image_url}" loading="lazy" />
<div class="wave-card-text">${w.evaluation || 'AI 正在解析中...'}</div>
`
card.onclick = () => openWaveLightbox(idx)
grid.appendChild(card)
})
}
const openWaveLightbox = (idx) => {
if (!state.waveList || !state.waveList[idx]) return
state.waveIndex = idx
const w = state.waveList[idx]
el('waveLbImg').src = w.image_url
el('waveLbEval').textContent = w.evaluation || '暂无评价'
el('waveLbNotes').textContent = w.notes || '暂无注意事项'
el('waveLbSource').href = w.source_url || '#'
el('waveLightbox').style.display = 'flex'
}
const closeWaveLightbox = () => {
el('waveLightbox').style.display = 'none'
}
const switchWaveLightbox = (dir) => {
let nextIdx = state.waveIndex + dir
if (nextIdx < 0) nextIdx = state.waveList.length - 1
if (nextIdx >= state.waveList.length) nextIdx = 0
openWaveLightbox(nextIdx)
}
const checkWaveCrawlerStatus = async () => {
try {
const data = await req(`${apiBase}/wave/status`)
const btn = el('btnWaveToggleCrawler')
const st = el('waveCrawlerStatus')
const logs = data.logs || []
if (el('waveLogsModal').style.display === 'flex') {
const pre = el('waveLogsContent')
pre.textContent = logs.join('\n')
pre.scrollTop = pre.scrollHeight
}
if (data.isRunning) {
btn.textContent = '⏹ 停止佛系爬虫'
btn.style.background = 'rgba(239, 68, 68, 0.8)'
st.innerHTML = `爬虫运行中 (第${data.currentPage}页)<br>已处理 ${data.totalProcessed},AI 采纳 ${data.totalApproved} 条`
state.waveCrawlerTimer = setTimeout(checkWaveCrawlerStatus, 5000) // Poll every 5s if running
} else {
btn.textContent = '▶ 启动佛系爬虫 (每页缓爬)'
btn.style.background = 'rgba(37, 99, 235, 0.8)'
st.innerHTML = `爬虫已停止<br>已处理 ${data.totalProcessed},AI 采纳 ${data.totalApproved} 条`
if (state.waveCrawlerTimer) {
clearTimeout(state.waveCrawlerTimer)
state.waveCrawlerTimer = null
}
}
} catch (e) {
console.error(e)
}
}
const toggleWaveCrawler = async () => {
trace.open('佛系爬虫开关 (wave) - 全链跟踪')
trace.line(`Page Origin: ${window.location.origin}`)
trace.line(`AUTOMATION_API: ${AUTOMATION_API || '(空串,走相对路径,由 8976 反代到 8977)'}`)
const toggleUrl = `${AUTOMATION_API}${apiBase}/wave/toggle`
const statusUrl = `${apiBase}/wave/status`
try {
// 1) 切换状态
const r1 = await traceFetch('Step 1 切换爬虫状态', toggleUrl, { method: 'POST' })
if (!r1.res.ok) throw new Error(`HTTP_${r1.res.status} (切换接口返回非 2xx)`)
if (!r1.json || r1.json.ok !== true) throw new Error((r1.json && r1.json.error) || '切换接口 ok=false')
trace.line(`✓ 服务端 isRunning = ${r1.json.isRunning}`)
// 2) 拉一次状态确认
const r2 = await traceFetch('Step 2 读取爬虫状态', statusUrl)
if (!r2.res.ok) throw new Error(`HTTP_${r2.res.status} (状态接口返回非 2xx)`)
if (!r2.json || r2.json.ok !== true) throw new Error((r2.json && r2.json.error) || '状态接口 ok=false')
trace.done(true, `isRunning=${r2.json.isRunning}、page=${r2.json.currentPage}、processed=${r2.json.totalProcessed}`)
checkWaveCrawlerStatus()
} catch (e) {
trace.line(`❌ 异常: ${e.name || 'Error'} - ${e.message}`)
if (e.stack) trace.line(`Stack:\n${e.stack}`)
trace.done(false, e.message)
// 不弹 alert,全部信息保留在调试模态窗内,点“复制全部”拷贝即可
}
}
const loadKoreanGallery = async () => {
try {
const data = await req(`${apiBase}/korean?page=1&limit=500`)
state.koreanList = data.rows || []
renderKoreanGrid()
} catch (e) {
console.error(e)
}
}
const renderKoreanGrid = () => {
const grid = el('koreanGrid')
grid.innerHTML = ''
state.koreanList.forEach((w, idx) => {
const card = document.createElement('div')
card.className = 'wave-card'
card.innerHTML = `
<img src="${w.image_url}" loading="lazy" />
<div class="wave-card-text">${w.evaluation || 'AI 正在解析中...'}</div>
`
card.onclick = () => openKoreanLightbox(idx)
grid.appendChild(card)
})
}
const openKoreanLightbox = (idx) => {
if (!state.koreanList || !state.koreanList[idx]) return
state.koreanIndex = idx
const w = state.koreanList[idx]
el('koreanLbImg').src = w.image_url
el('koreanLbEval').textContent = w.evaluation || '暂无评价'
el('koreanLbNotes').textContent = w.notes || '暂无注意事项'
el('koreanLbSource').href = w.source_url || '#'
el('koreanLightbox').style.display = 'flex'
}
const closeKoreanLightbox = () => {
el('koreanLightbox').style.display = 'none'
}
const switchKoreanLightbox = (dir) => {
let nextIdx = state.koreanIndex + dir
if (nextIdx < 0) nextIdx = state.koreanList.length - 1
if (nextIdx >= state.koreanList.length) nextIdx = 0
openKoreanLightbox(nextIdx)
}
const checkKoreanCrawlerStatus = async () => {
try {
const data = await req(`${apiBase}/korean/status`)
const btn = el('btnKoreanToggleCrawler')
const st = el('koreanCrawlerStatus')
const logs = data.logs || []
if (el('koreanLogsModal').style.display === 'flex') {
const pre = el('koreanLogsContent')
pre.textContent = logs.join('\n')
pre.scrollTop = pre.scrollHeight
}
if (data.isRunning) {
btn.textContent = '⏹ 停止佛系爬虫'
btn.style.background = 'rgba(239, 68, 68, 0.8)'
st.innerHTML = `爬虫运行中 (第${data.currentPage}页)<br>已处理 ${data.totalProcessed},AI 采纳 ${data.totalApproved} 条`
state.koreanCrawlerTimer = setTimeout(checkKoreanCrawlerStatus, 5000)
} else {
btn.textContent = '▶ 启动佛系爬虫'
btn.style.background = 'rgba(37, 99, 235, 0.8)'
st.innerHTML = `爬虫已停止<br>已处理 ${data.totalProcessed},AI 采纳 ${data.totalApproved} 条`
if (state.koreanCrawlerTimer) {
clearTimeout(state.koreanCrawlerTimer)
state.koreanCrawlerTimer = null
}
}
} catch (e) {
console.error(e)
}
}
const toggleKoreanCrawler = async () => {
trace.open('佛系爬虫开关 (korean) - 全链跟踪')
trace.line(`Page Origin: ${window.location.origin}`)
trace.line(`AUTOMATION_API: ${AUTOMATION_API || '(空串,走相对路径,由 8976 反代到 8977)'}`)
const toggleUrl = `${AUTOMATION_API}${apiBase}/korean/toggle`
const statusUrl = `${apiBase}/korean/status`
try {
const r1 = await traceFetch('Step 1 切换爬虫状态', toggleUrl, { method: 'POST' })
if (!r1.res.ok) throw new Error(`HTTP_${r1.res.status} (切换接口返回非 2xx)`)
if (!r1.json || r1.json.ok !== true) throw new Error((r1.json && r1.json.error) || '切换接口 ok=false')
trace.line(`✓ 服务端 isRunning = ${r1.json.isRunning}`)
const r2 = await traceFetch('Step 2 读取爬虫状态', statusUrl)
if (!r2.res.ok) throw new Error(`HTTP_${r2.res.status} (状态接口返回非 2xx)`)
if (!r2.json || r2.json.ok !== true) throw new Error((r2.json && r2.json.error) || '状态接口 ok=false')
trace.done(true, `isRunning=${r2.json.isRunning}、page=${r2.json.currentPage}、processed=${r2.json.totalProcessed}`)
checkKoreanCrawlerStatus()
} catch (e) {
trace.line(`❌ 异常: ${e.name || 'Error'} - ${e.message}`)
if (e.stack) trace.line(`Stack:\n${e.stack}`)
trace.done(false, e.message)
}
}
const loadZhihuIndex = async () => {
try {
const res = await fetch('/data/zhihu_articles/index.json', { cache: 'no-store' })
if (!res.ok) {
el('zhihuTocBody').innerHTML = '<div style="padding: 16px; color: #94a3b8">暂无数据,请先运行抓取脚本</div>'
return
}
const data = await res.json()
const grouped = {}
data.forEach((item, i) => {
if (!grouped[item.group]) grouped[item.group] = []
grouped[item.group].push({ ...item, index: i })
})
let html = ''
for (const group in grouped) {
html += `<div class="toc-group">${group}</div>`
grouped[group].forEach(item => {
if (!item.contentHtml) {
html += `<div class="toc-item disabled" title="尚未抓取">${item.title}</div>`
} else {
html += `<div class="toc-item" onclick="window.loadZhihuArticle(${item.index})">${item.title}</div>`
}
})
}
el('zhihuTocBody').innerHTML = html
window.loadZhihuArticle = async (index) => {
document.querySelectorAll('.toc-item').forEach(el => el.classList.remove('active'))
event.currentTarget.classList.add('active')
try {
const aRes = await fetch(`/data/zhihu_articles/article_${index}.html`, { cache: 'no-store' })
if (!aRes.ok) throw new Error('Load failed')
const aHtml = await aRes.text()
el('zhihuContentArea').innerHTML = `<div class="ztext">${aHtml}</div>`
el('zhihuContentArea').scrollTop = 0
} catch (e) {
el('zhihuContentArea').innerHTML = `<div class="zhihu-placeholder"><h3>加载文章失败</h3></div>`
}
}
} catch (e) {
console.error('Failed to load zhihu index', e)
}
}
window.openXLightbox = (src) => {
el('xLbImg').src = src
el('xLightbox').style.display = 'flex'
}
window.closeXLightbox = () => {
el('xLightbox').style.display = 'none'
}
window.loadXAccount = async (account) => {
document.querySelectorAll('.x-nav-item').forEach(el => el.classList.remove('active'))
if (event && event.currentTarget) {
event.currentTarget.classList.add('active')
}
el('xHeaderTitle').textContent = `@${account}`
el('xFeed').innerHTML = `
<div class="x-placeholder">
<div class="x-spinner"></div>
<p>正在加载推文...</p>
</div>
`
try {
const res = await fetch(`/data/x_fashion/${account}/data.json`, { cache: 'no-store' })
if (!res.ok) {
el('xFeed').innerHTML = '<div class="x-placeholder"><p>暂无数据,请先运行抓取脚本</p></div>'
return
}
const data = await res.json()
if (data.length === 0) {
el('xFeed').innerHTML = '<div class="x-placeholder"><p>还没有推文</p></div>'
return
}
let html = ''
data.forEach(tweet => {
const time = new Date(tweet.timestamp).toLocaleString()
let imagesHtml = ''
if (tweet.localImages && tweet.localImages.length > 0) {
imagesHtml = `<div class="x-tweet-images count-${Math.min(tweet.localImages.length, 4)}">`
tweet.localImages.forEach(img => {
imagesHtml += `<img src="${img}" loading="lazy" onclick="openXLightbox('${img}')" style="cursor: zoom-in;" />`
})
imagesHtml += `</div>`
}
html += `
<article class="x-tweet">
<div class="x-tweet-avatar"></div>
<div class="x-tweet-body">
<div class="x-tweet-meta">
<span class="x-tweet-name">${account}</span>
<span class="x-tweet-handle">@${account}</span>
<span class="x-tweet-dot">·</span>
<span class="x-tweet-time">${time}</span>
</div>
<div class="x-tweet-text">${tweet.text.replace(/\n/g, '<br>')}</div>
${imagesHtml}
<div class="x-tweet-actions">
<span>💬</span>
<span>🔁</span>
<span>❤️</span>
<span>📊</span>
</div>
</div>
</article>
`
})
el('xFeed').innerHTML = html
} catch (e) {
console.error(e)
el('xFeed').innerHTML = '<div class="x-placeholder"><p>加载数据失败</p></div>'
}
}
const saveOutfit = async () => {
const id = Number(el('fId').value || 0)
const payload = {
profile_key: state.profileKey,
name: el('fName').value.trim(),
scene: el('fScene').value,
temp_zone: el('fTempZone').value,
top_desc: el('fTop').value.trim(),
bottom_desc: el('fBottom').value.trim(),
shoes_desc: el('fShoes').value.trim(),
outer_desc: el('fOuter').value.trim(),
accessory_desc: el('fAccessory').value.trim(),
notes: el('fNotes').value.trim()
}
if (!payload.name) {
await modalAlert('无法保存', '名称不能为空。')
return
}
if (id > 0) {
await req(`${apiBase}/outfits/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
})
await modalAlert('保存成功', '搭配已更新。')
} else {
await req(`${apiBase}/outfits`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
})
await modalAlert('新增成功', '搭配已新增。')
}
resetForm()
await loadOutfits()
await loadUnmatchedReport()
}
const submitCheck = async () => {
const checkboxes = [...document.querySelectorAll('#ruleList input[type="checkbox"]')]
const passed = checkboxes.filter(x => x.checked)
const failed = checkboxes.filter(x => !x.checked)
const fullScore = checkboxes.reduce((s, x) => s + Number(x.getAttribute('data-weight') || 0), 0)
const score = passed.reduce((s, x) => s + Number(x.getAttribute('data-weight') || 0), 0)
const totalScore = fullScore > 0 ? Math.round(score * 100 / fullScore) : 0
const resultLevel = totalScore >= 80 ? 'pass' : totalScore >= 60 ? 'warn' : 'fail'
const issues = failed.map(x => {
const text = x.parentElement ? x.parentElement.textContent : ''
return String(text || '').trim()
})
const payload = {
profile_key: state.profileKey,
check_date: new Date().toISOString().slice(0, 10),
scene: el('scene').value,
temp_c: Number(el('tempC').value || NaN),
temp_zone: el('tempZone').value,
total_score: totalScore,
result_level: resultLevel,
issues
}
await req(`${apiBase}/checks`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
})
el('recentChecks').textContent = `本次评分:${totalScore}(${resultLevel})`
await loadRecentChecks()
await modalAlert('镜检已提交', `本次评分:${totalScore}(${resultLevel})`)
}
const init = async () => {
const saved = localStorage.getItem('style_check_profile_key')
state.profileKey = saved === 'mature_fashion40' ? 'mature_fashion40' : 'mature_relaxed'
applyProfileTheme()
const urlParams = new URLSearchParams(window.location.search)
const currentTab = urlParams.get('tab')
el('tabRelaxed').onclick = () => {
if (currentTab) {
localStorage.setItem('style_check_profile_key', 'mature_relaxed')
window.location.href = '/tools/style_check'
} else {
setProfile('mature_relaxed').catch(async e => { await modalAlert('切换失败', String(e.message || e)) })
}
}
el('tabFashion40').onclick = () => {
if (currentTab) {
localStorage.setItem('style_check_profile_key', 'mature_fashion40')
window.location.href = '/tools/style_check'
} else {
setProfile('mature_fashion40').catch(async e => { await modalAlert('切换失败', String(e.message || e)) })
}
}
const showWaveTab = () => {
el('tabRelaxed').classList.remove('active')
el('tabFashion40').classList.remove('active')
el('tabKorean').classList.remove('active')
el('tabZhihu').classList.remove('active')
el('tabX').classList.remove('active')
el('tabWave').classList.add('active')
el('mainSections').style.display = 'none'
el('koreanSection').style.display = 'none'
el('zhihuSection').style.display = 'none'
el('xSection').style.display = 'none'
el('waveSection').style.display = 'block'
loadWaveGallery()
checkWaveCrawlerStatus()
}
const showKoreanTab = () => {
el('tabRelaxed').classList.remove('active')
el('tabFashion40').classList.remove('active')
el('tabWave').classList.remove('active')
el('tabZhihu').classList.remove('active')
el('tabX').classList.remove('active')
el('tabKorean').classList.add('active')
el('mainSections').style.display = 'none'
el('waveSection').style.display = 'none'
el('zhihuSection').style.display = 'none'
el('xSection').style.display = 'none'
el('koreanSection').style.display = 'block'
loadKoreanGallery()
checkKoreanCrawlerStatus()
}
const showZhihuTab = () => {
el('tabRelaxed').classList.remove('active')
el('tabFashion40').classList.remove('active')
el('tabWave').classList.remove('active')
el('tabKorean').classList.remove('active')
el('tabX').classList.remove('active')
el('tabZhihu').classList.add('active')
el('mainSections').style.display = 'none'
el('waveSection').style.display = 'none'
el('koreanSection').style.display = 'none'
el('xSection').style.display = 'none'
el('zhihuSection').style.display = 'block'
loadZhihuIndex()
}
const showXTab = () => {
el('tabRelaxed').classList.remove('active')
el('tabFashion40').classList.remove('active')
el('tabWave').classList.remove('active')
el('tabKorean').classList.remove('active')
el('tabZhihu').classList.remove('active')
el('tabX').classList.add('active')
el('mainSections').style.display = 'none'
el('waveSection').style.display = 'none'
el('koreanSection').style.display = 'none'
el('zhihuSection').style.display = 'none'
el('xSection').style.display = 'block'
window.loadXAccount('mensgrinding')
}
// WAVE
el('tabWave').onclick = () => {
if (currentTab !== 'wave') window.open('/tools/style_check?tab=wave', '_blank')
}
el('btnWaveToggleCrawler').onclick = toggleWaveCrawler
// Wave manual links upload
const uploadInput = el('waveLinkFile')
const uploadBtn = el('btnWaveUploadLinks')
if (uploadBtn && uploadInput) {
uploadBtn.onclick = () => uploadInput.click()
uploadInput.onchange = async (e) => {
const file = e.target.files && e.target.files[0]
if (!file) return
try {
const text = await file.text()
let links = []
try {
links = JSON.parse(text)
if (!Array.isArray(links)) throw new Error('Not an array')
} catch {
// fallback to split by newline
links = text.split('\n').map(l => l.trim()).filter(l => l.startsWith('http'))
}
if (links.length === 0) {
alert('文件中没有找到有效的图片链接')
return
}
const res = await req(`${AUTOMATION_API}${apiBase}/wave/upload_links`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ links })
})
alert(`成功投喂 ${links.length} 条链接!目前队列中还有 ${res.queueLength} 张图片等待后台佛系下载和AI审核。`)
checkWaveCrawlerStatus()
} catch (err) {
alert('读取文件失败: ' + err.message + '\n请确认 8977 自动化服务已启动')
}
uploadInput.value = ''
}
}
el('btnWaveShowLogs').onclick = async () => {
el('waveLogsModal').style.display = 'flex'
await checkWaveCrawlerStatus()
}
el('btnWaveLogsClose').onclick = () => {
el('waveLogsModal').style.display = 'none'
}
// 调试跟踪模态窗:关闭 / 复制全部
const btnTraceClose = el('btnDebugTraceClose')
if (btnTraceClose) btnTraceClose.onclick = () => {
el('debugTraceModal').style.display = 'none'
}
const btnTraceCopy = el('btnDebugTraceCopy')
if (btnTraceCopy) btnTraceCopy.onclick = async () => {
const text = el('debugTraceContent').textContent || ''
try {
await navigator.clipboard.writeText(text)
btnTraceCopy.textContent = '✓ 已复制'
setTimeout(() => { btnTraceCopy.textContent = '复制全部' }, 1500)
} catch {
// 降级:选中让用户手动 Ctrl+C
const range = document.createRange()
range.selectNodeContents(el('debugTraceContent'))
const sel = window.getSelection()
sel.removeAllRanges(); sel.addRange(range)
btnTraceCopy.textContent = '已选中,请 Ctrl+C'
setTimeout(() => { btnTraceCopy.textContent = '复制全部' }, 2000)
}
}
el('waveLbClose').onclick = closeWaveLightbox
el('waveLbPrev').onclick = () => switchWaveLightbox(-1)
el('waveLbNext').onclick = () => switchWaveLightbox(1)
// KOREAN
el('tabKorean').onclick = () => {
if (currentTab !== 'korean') window.open('/tools/style_check?tab=korean', '_blank')
}
el('btnKoreanToggleCrawler').onclick = toggleKoreanCrawler
// ZHIHU
el('tabZhihu').onclick = () => {
if (currentTab !== 'zhihu') window.open('/tools/style_check?tab=zhihu', '_blank')
}
// X (Twitter)
el('tabX').onclick = () => {
if (currentTab !== 'x') window.open('/tools/style_check?tab=x', '_blank')
}
if (el('xLbClose')) {
el('xLbClose').addEventListener('click', window.closeXLightbox)
el('xLightbox').addEventListener('click', (e) => {
if (e.target === el('xLightbox')) window.closeXLightbox()
})
}
if (el('btnXHelp')) {
el('btnXHelp').addEventListener('click', () => {
el('xHelpModal').style.display = 'flex'
})
}
if (el('btnXHelpClose')) {
el('btnXHelpClose').addEventListener('click', () => {
el('xHelpModal').style.display = 'none'
})
}
const uploadInputK = el('koreanLinkFile')
const uploadBtnK = el('btnKoreanUploadLinks')
if (uploadBtnK && uploadInputK) {
uploadBtnK.onclick = () => uploadInputK.click()
uploadInputK.onchange = async (e) => {
const file = e.target.files && e.target.files[0]
if (!file) return
try {
const text = await file.text()
let links = []
try {
links = JSON.parse(text)
if (!Array.isArray(links)) throw new Error('Not an array')
} catch {
links = text.split('\n').map(l => l.trim()).filter(l => l.startsWith('http'))
}
if (links.length === 0) {
alert('文件中没有找到有效的图片链接')
return
}
const res = await req(`${AUTOMATION_API}${apiBase}/korean/upload_links`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ links })
})
alert(`成功投喂 ${links.length} 条链接!目前队列中还有 ${res.queueLength} 张图片等待后台佛系下载和AI审核。`)
checkKoreanCrawlerStatus()
} catch (err) {
alert('读取文件失败: ' + err.message + '\n请确认 8977 自动化服务已启动')
}
uploadInputK.value = ''
}
}
el('btnKoreanShowLogs').onclick = async () => {
el('koreanLogsModal').style.display = 'flex'
await checkKoreanCrawlerStatus()
}
el('btnKoreanLogsClose').onclick = () => {
el('koreanLogsModal').style.display = 'none'
}
el('koreanLbClose').onclick = closeKoreanLightbox
el('koreanLbPrev').onclick = () => switchKoreanLightbox(-1)
el('koreanLbNext').onclick = () => switchKoreanLightbox(1)
el('btnToday').onclick = () => loadToday().catch(async e => { await modalAlert('获取失败', String(e.message || e)) })
el('btnSearch').onclick = () => loadOutfits().catch(async e => { await modalAlert('查询失败', String(e.message || e)) })
el('btnLibSearch').onclick = () => loadLibrary().catch(async e => { await modalAlert('服饰库查询失败', String(e.message || e)) })
el('btnRebuildLinks').onclick = () => rebuildLinks().then(() => loadOutfits()).catch(async e => { await modalAlert('重建失败', String(e.message || e)) })
const btnFetch = el('btnFetchLocalImages')
if (btnFetch) btnFetch.onclick = () => fetchLocalImages().catch(async e => { await modalAlert('找图失败', String(e.message || e)) })
el('btnLibSave').onclick = () => saveLibraryItem().catch(async e => { await modalAlert('服饰保存失败', String(e.message || e)) })
el('btnLibReset').onclick = () => resetLibForm()
el('btnAiAudit').onclick = () => runAiAudit().catch(async e => { await modalAlert('AI审核失败', String(e.message || e)) })
el('aiImage').onchange = () => {
const f = el('aiImage').files && el('aiImage').files[0]
if (!f) {
el('aiPreview').removeAttribute('src')
return
}
toDataUrl(f).then(url => {
el('aiPreview').src = url
}).catch(() => {})
}
el('btnSave').onclick = () => saveOutfit().catch(async e => { await modalAlert('保存失败', String(e.message || e)) })
el('btnReset').onclick = () => {
resetForm()
showMsg('')
}
el('btnCheck').onclick = () => submitCheck().catch(async e => { await modalAlert('提交失败', String(e.message || e)) })
if (currentTab === 'wave') {
document.body.classList.add('full-width-mode')
showWaveTab()
} else if (currentTab === 'korean') {
document.body.classList.add('full-width-mode')
showKoreanTab()
} else if (currentTab === 'zhihu') {
document.body.classList.add('full-width-mode')
showZhihuTab()
} else if (currentTab === 'x') {
document.body.classList.add('full-width-mode')
showXTab()
} else {
resetForm()
resetLibForm()
el('aiScene').value = 'commute'
el('aiTempZone').value = ''
el('aiAuditResult').textContent = `请上传穿搭照片后开始AI审核(当前:${profileText(state.profileKey)})`
el('aiAuditIssues').textContent = ''
await loadRules()
await Promise.all([loadOutfits(), loadRecentChecks(), loadToday(), loadLibrary(), loadUnmatchedReport()])
}
}
init().catch(e => {
modalAlert('初始化失败', String((e && e.message) || e))
})
})()