<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>字典管理</title>
    <script>
        (function () {
            var token = localStorage.getItem('pb_manage_token') || ''
            var isLoggedIn = localStorage.getItem('pb_manage_logged_in') === '1'
            if (!token || !isLoggedIn) {
                window.location.replace('/pb/manage/login')
            }
        })()
    </script>
    <style>
        * { box-sizing: border-box; }
        body { margin: 0; font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; background: linear-gradient(180deg, #f3f6fb 0%, #eef2ff 100%); color: #1f2937; }
        .container { max-width: 1440px; margin: 0 auto; padding: 24px 14px 40px; }
        .topbar, .panel, .modal-card { background: rgba(255,255,255,0.96); border: 1px solid #e5e7eb; box-shadow: 0 18px 50px rgba(15, 23, 42, 0.08); }
        .topbar { border-radius: 20px; padding: 18px; margin-bottom: 14px; }
        .topbar h1 { margin: 0 0 8px; font-size: 30px; }
        .topbar p { margin: 0; color: #4b5563; line-height: 1.7; }
        .actions { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 12px; }
        .btn { border: none; border-radius: 12px; padding: 10px 16px; font-weight: 600; cursor: pointer; }
        .btn-primary { background: #2563eb; color: #fff; }
        .btn-secondary { background: #e0e7ff; color: #1e3a8a; }
        .btn-danger { background: #dc2626; color: #fff; }
        .btn-light { background: #f8fafc; color: #334155; border: 1px solid #dbe3f0; }
        .panel { border-radius: 20px; padding: 18px; }
        .toolbar { display: grid; grid-template-columns: 1.3fr auto 1fr 1fr auto auto; gap: 12px; margin-bottom: 18px; }
        input, textarea, select { width: 100%; border: 1px solid #cbd5e1; border-radius: 12px; padding: 10px 12px; font-size: 14px; background: #fff; }
        textarea { min-height: 88px; resize: vertical; }
        table { width: 100%; border-collapse: collapse; overflow: hidden; border-radius: 16px; }
        thead { background: #eff6ff; }
        th, td { padding: 14px 12px; border-bottom: 1px solid #e5e7eb; text-align: left; vertical-align: top; }
        th { font-size: 13px; color: #475569; }
        tr:hover td { background: #f8fafc; }
        .badge { display: inline-flex; align-items: center; padding: 4px 10px; border-radius: 999px; font-size: 12px; font-weight: 700; }
        .badge-on { background: #dcfce7; color: #166534; }
        .badge-off { background: #fee2e2; color: #991b1b; }
        .muted { color: #64748b; font-size: 13px; }
        .inline-input { min-width: 120px; }
        .immutable-input { background: #fffdf7; }
        .link { color: #2563eb; text-decoration: none; font-weight: 600; }
        .status { margin: 14px 0 0; min-height: 24px; font-size: 14px; }
        .status.success { color: #15803d; }
        .status.error { color: #b91c1c; }
        .auth-box { display: grid; grid-template-columns: 1fr auto; gap: 12px; margin-top: 18px; }
        .modal { position: fixed; inset: 0; display: none; align-items: center; justify-content: center; background: rgba(15, 23, 42, 0.4); padding: 14px; }
        .modal.show { display: flex; }
        .modal-card { width: min(1104px, 100%); border-radius: 20px; padding: 18px; max-height: 90vh; overflow: auto; }
        .modal-header { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 16px; }
        .modal-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; }
        .full { grid-column: 1 / -1; }
        .item-table { margin-top: 16px; border: 1px solid #e5e7eb; border-radius: 16px; overflow: hidden; }
        .item-table table { border-radius: 0; }
        .modal-actions { display: flex; justify-content: space-between; flex-wrap: wrap; gap: 12px; margin-top: 16px; }
        .empty { text-align: center; padding: 32px 16px; color: #64748b; }
        .thumb { width: 48px; height: 48px; border-radius: 10px; object-fit: cover; border: 1px solid #dbe3f0; background: #fff; }
        .thumb.previewable { cursor: zoom-in; }
        .thumb-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
        .thumb-meta { font-size: 12px; color: #64748b; word-break: break-all; }
        .enum-preview-item { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
        .enum-preview-more { margin-top: 8px; display: none; }
        .enum-preview-toggle { margin-top: 6px; }
        .item-image-grid { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 10px; }
        .image-tile { width: 86px; border: 1px solid #dbe3f0; border-radius: 12px; background: #fff; padding: 6px; display: flex; flex-direction: column; gap: 6px; cursor: grab; }
        .image-tile.dragging { opacity: 0.55; }
        .image-tile-thumb { width: 100%; height: 56px; border-radius: 8px; border: 1px solid #e2e8f0; background: #f8fafc; display: flex; align-items: center; justify-content: center; overflow: hidden; font-size: 12px; color: #64748b; }
        .image-tile-thumb img { width: 100%; height: 100%; object-fit: cover; }
        .image-tile-actions { display: flex; align-items: center; justify-content: space-between; gap: 6px; }
        .image-tile-order { font-size: 11px; color: #64748b; }
        .item-field-stack { display: flex; flex-direction: column; gap: 10px; min-width: 260px; }
        .item-field-label { font-size: 12px; color: #64748b; margin-bottom: 4px; }
        .item-delete-wrap { margin-top: 2px; }
        .image-upload-row { display: grid; grid-template-columns: minmax(0, 1fr); align-items: center; gap: 8px; margin-top: 10px; }
        .image-upload-row input[type="file"] { width: 100%; min-width: 0; }
        .icon-btn { min-width: 36px; padding: 6px 10px; line-height: 1; font-size: 18px; }
        .drop-tip { color: #64748b; font-size: 12px; margin-top: 6px; }
        .image-viewer { position: fixed; inset: 0; display: none; align-items: center; justify-content: center; padding: 24px; background: rgba(15, 23, 42, 0.82); z-index: 9999; }
        .image-viewer.show { display: flex; }
        .image-viewer img { max-width: min(92vw, 1600px); max-height: 88vh; border-radius: 18px; box-shadow: 0 24px 70px rgba(15, 23, 42, 0.38); background: #fff; }
        .image-viewer-close { position: absolute; top: 18px; right: 18px; min-width: 44px; min-height: 44px; border-radius: 999px; border: none; background: rgba(255,255,255,0.92); color: #0f172a; font-size: 24px; cursor: pointer; }
        .loading-mask { position: fixed; inset: 0; display: none; align-items: center; justify-content: center; padding: 24px; background: rgba(15, 23, 42, 0.42); backdrop-filter: blur(4px); z-index: 9998; }
        .loading-mask.show { display: flex; }
        .loading-card { min-width: min(92vw, 360px); padding: 24px 22px; border-radius: 20px; background: rgba(255,255,255,0.98); box-shadow: 0 28px 70px rgba(15, 23, 42, 0.22); border: 1px solid #dbe3f0; text-align: center; }
        .loading-spinner { width: 44px; height: 44px; margin: 0 auto 14px; border-radius: 999px; border: 4px solid #dbeafe; border-top-color: #2563eb; animation: dictSpin 0.9s linear infinite; }
        .loading-text { color: #0f172a; font-size: 15px; font-weight: 700; }
        @keyframes dictSpin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
        @media (max-width: 960px) {
            .toolbar, .auth-box, .modal-grid { grid-template-columns: 1fr; }
            table, thead, tbody, th, td, tr { display: block; }
            thead { display: none; }
            tr { margin-bottom: 14px; border: 1px solid #e5e7eb; border-radius: 16px; overflow: hidden; }
            td { display: flex; justify-content: space-between; gap: 12px; }
            td::before { content: attr(data-label); font-weight: 700; color: #475569; }
        }
    </style>
    {{ template "theme_head" . }}
</head>
<body>
    <div class="container">
        <section class="topbar">
            <h1>字典管理</h1>
            <div class="actions">
                <a class="btn btn-light" href="/pb/manage">返回主页</a>
                <button class="btn btn-primary" id="createBtn" type="button">新增字典</button>
            </div>
            <div class="status" id="status"></div>
        </section>

        <section class="panel">
            <div class="toolbar">
                <input id="keywordInput" placeholder="按字典名称模糊搜索" />
                <select id="categorySelect"><option value="">全部分类</option></select>
                <input id="detailInput" placeholder="查询指定字典名称" />
                <button class="btn btn-secondary" id="listBtn" type="button">查询全部</button>
                <button class="btn btn-light" id="detailBtn" type="button">查询指定</button>
                <button class="btn btn-light" id="resetBtn" type="button">重置</button>
            </div>
            <div style="overflow:auto;">
                <table>
                    <thead>
                        <tr>
                            <th>字典名称</th>
                            <th>启用</th>
                            <th>备注</th>
                            <th>枚举项</th>
                            <th>创建时间</th>
                            <th>操作</th>
                        </tr>
                    </thead>
                    <tbody id="tableBody">
                        <tr><td colspan="7" class="empty">暂无数据，请先查询。</td></tr>
                    </tbody>
                </table>
            </div>
        </section>
    </div>

    <div class="modal" id="editorModal">
        <div class="modal-card">
            <div class="modal-header">
                <div>
                    <h2 id="modalTitle" style="margin:0;">新增字典</h2>
                </div>
                <button class="btn btn-light" id="closeModalBtn" type="button">关闭</button>
            </div>
            <div class="modal-grid">
                <div>
                    <label>字典名称</label>
                    <input id="dictNameInput" placeholder="请输入字典名称" />
                </div>
                <div>
                    <label>是否启用</label>
                    <select id="enabledInput">
                        <option value="true">启用</option>
                        <option value="false">禁用</option>
                    </select>
                </div>
                <div class="full">
                    <label>备注说明</label>
                    <textarea id="remarkInput" placeholder="请输入备注说明"></textarea>
                </div>
            </div>

            <div class="item-table">
                <table>
                    <thead>
                        <tr>
                            <th>配置</th>
                            <th>图片</th>
                        </tr>
                    </thead>
                    <tbody id="itemsBody"></tbody>
                </table>
            </div>

            <div class="modal-actions">
                <div>
                    <button class="btn btn-secondary" id="addItemBtn" type="button">新增枚举项</button>
                </div>
                <div>
                    <button class="btn btn-light" id="cancelBtn" type="button">取消</button>
                    <button class="btn btn-primary" id="saveBtn" type="button">保存</button>
                </div>
            </div>
        </div>
    </div>

    <div class="image-viewer" id="imageViewer">
        <button class="image-viewer-close" id="closeImageViewerBtn" type="button">×</button>
        <img id="imageViewerImg" src="" alt="预览原图" />
    </div>

    <div class="loading-mask" id="loadingMask">
        <div class="loading-card">
            <div class="loading-spinner"></div>
            <div class="loading-text" id="loadingText">处理中，请稍候...</div>
        </div>
    </div>

    <script>
        const API_BASE = '/pb/api'
        const tokenKey = 'pb_manage_token'
        const enumChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
        const state = {
            list: [],
            mode: 'create',
            editingOriginalName: '',
            items: [],
            enumSeed: '',
            enumCounter: 1,
            expandedPreviewKey: '',
            draggingImage: null,
        }

        const statusEl = document.getElementById('status')
        const keywordInput = document.getElementById('keywordInput')
        const categorySelect = document.getElementById('categorySelect')
        const detailInput = document.getElementById('detailInput')
        const tableBody = document.getElementById('tableBody')
        const editorModal = document.getElementById('editorModal')
        const modalTitle = document.getElementById('modalTitle')
        const dictNameInput = document.getElementById('dictNameInput')
        const enabledInput = document.getElementById('enabledInput')
        const remarkInput = document.getElementById('remarkInput')
        const itemsBody = document.getElementById('itemsBody')
        const imageViewer = document.getElementById('imageViewer')
        const imageViewerImg = document.getElementById('imageViewerImg')
        const loadingMask = document.getElementById('loadingMask')
        const loadingText = document.getElementById('loadingText')
        const loadingState = { count: 0 }

        function setStatus(message, type) {
            statusEl.textContent = message || ''
            statusEl.className = 'status' + (type ? ' ' + type : '')
        }

        function showLoading(message) {
            loadingState.count += 1
            if (loadingText) {
                loadingText.textContent = message || '处理中，请稍候...'
            }
            if (loadingMask) {
                loadingMask.classList.add('show')
            }
        }

        function hideLoading() {
            loadingState.count = Math.max(0, loadingState.count - 1)
            if (loadingState.count === 0 && loadingMask) {
                loadingMask.classList.remove('show')
                if (loadingText) {
                    loadingText.textContent = '处理中，请稍候...'
                }
            }
        }

        function escapeJsString(value) {
            return String(value || '')
                .replace(/\\\\/g, '\\\\\\\\')
                .replace(/'/g, "\\'")
        }

        function getToken() {
            return localStorage.getItem(tokenKey) || ''
        }

        function decodeErrMsg(value) {
            if (!value) {
                return ''
            }

            try {
                return decodeURIComponent(value)
            } catch (_err) {
                return String(value)
            }
        }

        function unwrapApiResponse(data, res) {
            const safe = data && typeof data === 'object' ? data : {}
            const headerCode = Number(res && res.headers ? (res.headers.get('X-Status-Code') || 0) : 0)
            const headerErrMsg = decodeErrMsg(res && res.headers ? res.headers.get('X-Err-Msg') : '')
            const code = Number(safe.statusCode || safe.code || headerCode || 0)
            const errMsg = safe.errMsg || safe.msg || headerErrMsg || ''
            const hasLegacyEnvelope = Object.prototype.hasOwnProperty.call(safe, 'data')
                && (Object.prototype.hasOwnProperty.call(safe, 'code')
                    || Object.prototype.hasOwnProperty.call(safe, 'statusCode')
                    || Object.prototype.hasOwnProperty.call(safe, 'msg')
                    || Object.prototype.hasOwnProperty.call(safe, 'errMsg'))
            const payload = hasLegacyEnvelope ? (safe.data || {}) : safe
            return { code: code, errMsg: errMsg, payload: payload }
        }

        async function request(url, payload) {
            const token = getToken()
            if (!token) {
                localStorage.removeItem('pb_manage_logged_in')
                window.location.replace('/pb/manage/login')
                throw new Error('登录状态已失效，请重新登录')
            }

            const res = await fetch(url, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    Authorization: 'Bearer ' + token,
                },
                body: JSON.stringify(payload || {}),
            })

            const data = await res.json()
            const unwrapped = unwrapApiResponse(data, res)
            if (!res.ok || unwrapped.code >= 400) {
                if (res.status === 401 || res.status === 403 || unwrapped.code === 401 || unwrapped.code === 403) {
                    localStorage.removeItem('pb_manage_token')
                    localStorage.removeItem('pb_manage_logged_in')
                    localStorage.removeItem('pb_manage_login_account')
                    localStorage.removeItem('pb_manage_login_time')
                    window.location.replace('/pb/manage/login')
                }
                throw new Error(unwrapped.errMsg || '请求失败')
            }

            return unwrapped.payload
        }

        async function parseJsonSafe(res) {
            const contentType = String(res.headers.get('content-type') || '').toLowerCase()
            const rawText = await res.text()
            const isJson = contentType.indexOf('application/json') !== -1

            if (!rawText) {
                return {
                    json: null,
                    text: '',
                    isJson: false,
                }
            }

            if (isJson) {
                try {
                    return {
                        json: JSON.parse(rawText),
                        text: rawText,
                        isJson: true,
                    }
                } catch (_error) {}
            }

            return {
                json: null,
                text: rawText,
                isJson: false,
            }
        }

        async function uploadAttachment(file, label) {
            const token = getToken()
            if (!token) {
                localStorage.removeItem('pb_manage_logged_in')
                window.location.replace('/pb/manage/login')
                throw new Error('登录状态已失效，请重新登录')
            }

            const form = new FormData()
            form.append('attachments_link', file)
            form.append('attachments_filename', file.name || '')
            form.append('attachments_filetype', file.type || '')
            form.append('attachments_size', String(file.size || 0))
            form.append('attachments_status', 'active')
            form.append('attachments_remark', 'dictionary-manage:' + label)

            const res = await fetch(API_BASE + '/attachment/upload', {
                method: 'POST',
                headers: {
                    Authorization: 'Bearer ' + token,
                },
                body: form,
            })

            const parsed = await parseJsonSafe(res)
            const data = parsed.json
            const unwrapped = unwrapApiResponse(data, res)
            if (!res.ok || !data || unwrapped.code >= 400) {
                if (res.status === 413) {
                    throw new Error('上传图片失败：文件已超过当前网关允许的请求体大小，或线上服务仍在运行旧版 hooks。')
                }
                if (!parsed.isJson && parsed.text) {
                    throw new Error('上传图片失败：服务端返回了非 JSON 响应，通常表示网关或反向代理提前拦截了上传请求。')
                }
                throw new Error(unwrapped.errMsg || '上传图片失败')
            }

            return unwrapped.payload
        }

        function escapeHtml(value) {
            return String(value || '')
                .replace(/&/g, '&amp;')
                .replace(/</g, '&lt;')
                .replace(/>/g, '&gt;')
                .replace(/"/g, '&quot;')
                .replace(/'/g, '&#39;')
        }

        function extractCategory(dictName) {
            var name = String(dictName || '')
            if (name.length >= 2) {
                return name.substring(0, 2)
            }
            return ''
        }

        function populateCategorySelect(list) {
            var categories = {}
            for (var i = 0; i < list.length; i++) {
                var cat = extractCategory(list[i].dict_name)
                if (cat) {
                    categories[cat] = true
                }
            }
            var sorted = Object.keys(categories).sort()
            var current = categorySelect.value
            categorySelect.innerHTML = '<option value="">全部分类</option>'
            for (var j = 0; j < sorted.length; j++) {
                var opt = document.createElement('option')
                opt.value = sorted[j]
                opt.textContent = sorted[j]
                categorySelect.appendChild(opt)
            }
            if (current && categories[current]) {
                categorySelect.value = current
            }
        }

        function filterByCategory(list) {
            var selected = categorySelect.value
            if (!selected) return list
            return list.filter(function (item) {
                return extractCategory(item.dict_name) === selected
            })
        }

        function randomEnumSeed() {
            const first = enumChars[Math.floor(Math.random() * enumChars.length)]
            const second = enumChars[Math.floor(Math.random() * enumChars.length)]
            return first + second
        }

        function ensureEnumSeed() {
            if (!state.enumSeed) {
                state.enumSeed = randomEnumSeed()
            }
        }

        function nextAutoEnum(usedSet) {
            ensureEnumSeed()
            let guard = 0
            while (guard < 10000) {
                const candidate = state.enumSeed + String(state.enumCounter)
                state.enumCounter += 1
                if (!usedSet.has(candidate)) {
                    return candidate
                }
                guard += 1
            }

            state.enumSeed = randomEnumSeed()
            state.enumCounter = 1
            return nextAutoEnum(usedSet)
        }

        function normalizeItemEnums() {
            const used = new Set()
            state.items = state.items.map(function (item) {
                const current = {
                    enum: String((item && item.enum) || '').trim(),
                    description: String((item && item.description) || ''),
                    image: (item && item.image) || '',
                    imageIds: [],
                    imageUrls: [],
                    imageAttachments: [],
                    imageUrl: '',
                    imageAttachment: null,
                    sortOrder: Number((item && item.sortOrder) || 0),
                }

                const imageIds = normalizeAttachmentIdList(item && (item.imageIds || item.image))
                const imageUrls = normalizeImageUrlList(item && item.imageUrls, item && item.imageUrl, imageIds.length)
                current.imageIds = imageIds
                current.imageUrls = imageUrls
                current.imageAttachments = normalizeImageAttachmentList(item && item.imageAttachments, item && item.imageAttachment, imageIds.length)
                applyItemImageSummary(current)

                if (!current.enum || used.has(current.enum)) {
                    current.enum = nextAutoEnum(used)
                }

                used.add(current.enum)
                if (!Number.isFinite(current.sortOrder) || current.sortOrder <= 0) {
                    current.sortOrder = used.size
                }

                return current
            })
        }

        function normalizeAttachmentIdList(value) {
            if (Array.isArray(value)) {
                return value.map(function (item) {
                    return String(item || '').trim()
                }).filter(function (item) {
                    return !!item
                })
            }

            const text = String(value || '').trim()
            if (!text) {
                return []
            }

            return text.split('|').map(function (item) {
                return String(item || '').trim()
            }).filter(function (item) {
                return !!item
            })
        }

        function normalizeImageUrlList(urls, singleUrl, expectedLength) {
            const list = Array.isArray(urls)
                ? urls.map(function (item) { return String(item || '') })
                : (singleUrl ? [String(singleUrl)] : [])

            while (list.length < expectedLength) {
                list.push('')
            }

            return list.slice(0, Math.max(expectedLength, list.length))
        }

        function normalizeImageAttachmentList(attachments, singleAttachment, expectedLength) {
            const list = Array.isArray(attachments)
                ? attachments.slice()
                : (singleAttachment ? [singleAttachment] : [])

            while (list.length < expectedLength) {
                list.push(null)
            }

            return list.slice(0, Math.max(expectedLength, list.length))
        }

        function applyItemImageSummary(item) {
            const imageIds = normalizeAttachmentIdList(item && item.imageIds)
            item.imageIds = imageIds
            item.image = imageIds.join('|')
            item.imageUrls = normalizeImageUrlList(item && item.imageUrls, item && item.imageUrl, imageIds.length)
            item.imageAttachments = normalizeImageAttachmentList(item && item.imageAttachments, item && item.imageAttachment, imageIds.length)
            item.imageUrl = item.imageUrls.length ? item.imageUrls[0] : ''
            item.imageAttachment = item.imageAttachments.length ? item.imageAttachments[0] : null
            return item
        }

        function renderEnumPreviewItem(item) {
            const desc = escapeHtml(item && item.description ? item.description : '（无描述）')
            const imageUrls = normalizeImageUrlList(item && item.imageUrls, item && item.imageUrl, normalizeAttachmentIdList(item && item.image).length)
            const imageHtml = imageUrls.length
                ? '<div class="thumb-row">' + imageUrls.map(function (url) {
                    if (!url) {
                        return '<span class="thumb" style="display:inline-flex;align-items:center;justify-content:center;color:#94a3b8;">无</span>'
                    }
                    return '<img class="thumb previewable" src="' + escapeHtml(url) + '" alt="" onclick="window.__previewImage(\'' + escapeJsString(url) + '\')" />'
                }).join('') + '</div>'
                : '<span class="muted">无图</span>'
            return '<div class="enum-preview-item"><div>' + imageHtml + '</div><span>' + desc + '</span></div>'
        }

        function renderItemsPreview(items, previewKey, isExpanded) {
            if (!items || !items.length) return '<span class="muted">无</span>'

            const first = renderEnumPreviewItem(items[0])
            if (items.length === 1) {
                return first
            }

            const rest = items.slice(1).map(function (item) {
                return renderEnumPreviewItem(item)
            }).join('')
            const hiddenCount = items.length - 1
            const moreStyle = isExpanded ? 'display:block;' : 'display:none;'
            const toggleText = isExpanded ? '收起' : ('展开其余 ' + hiddenCount + ' 项')

            return first
                + '<div class="enum-preview-more" style="' + moreStyle + '">' + rest + '</div>'
                + '<button class="btn btn-light enum-preview-toggle" type="button" onclick="window.__toggleEnumPreview(\'' + previewKey + '\')">' + toggleText + '</button>'
        }

        function renderTable(list) {
            if (!list.length) {
                tableBody.innerHTML = '<tr><td colspan="7" class="empty">暂无匹配数据。</td></tr>'
                return
            }

            tableBody.innerHTML = list.map(function (item, index) {
                const enabledClass = item.dict_word_is_enabled ? 'badge badge-on' : 'badge badge-off'
                const enabledText = item.dict_word_is_enabled ? '启用' : '禁用'
                const previewKey = String(index)
                return '<tr>'
                    + '<td data-label="字典名称"><input class="inline-input immutable-input" data-field="dict_name" data-name="' + escapeHtml(item.dict_name) + '" data-immutable-name="' + escapeHtml(item.dict_name) + '" value="' + escapeHtml(item.dict_name) + '" title="字典名称创建后不可修改，可选中复制" /></td>'
                    + '<td data-label="启用"><select class="inline-input" data-field="dict_word_is_enabled" data-name="' + escapeHtml(item.dict_name) + '"><option value="true"' + (item.dict_word_is_enabled ? ' selected' : '') + '>启用</option><option value="false"' + (!item.dict_word_is_enabled ? ' selected' : '') + '>禁用</option></select><div class="' + enabledClass + '" style="margin-top:8px;">' + enabledText + '</div></td>'
                    + '<td data-label="备注"><textarea class="inline-input" data-field="dict_word_remark" data-name="' + escapeHtml(item.dict_name) + '">' + escapeHtml(item.dict_word_remark) + '</textarea></td>'
                    + '<td data-label="枚举项">' + renderItemsPreview(item.items, previewKey, state.expandedPreviewKey === previewKey) + '</td>'
                    + '<td data-label="创建时间"><span class="muted">' + escapeHtml(item.created) + '</span></td>'
                    + '<td data-label="操作">'
                    + '<div style="display:flex;flex-wrap:wrap;gap:8px;">'
                    + '<button class="btn btn-light" type="button" onclick="window.__editItems(\'' + encodeURIComponent(item.dict_name) + '\')">编辑枚举</button>'
                    + '<button class="btn btn-secondary" type="button" onclick="window.__saveInline(\'' + encodeURIComponent(item.dict_name) + '\')">保存行</button>'
                    + '<button class="btn btn-danger" type="button" onclick="window.__deleteRow(\'' + encodeURIComponent(item.dict_name) + '\')">删除</button>'
                    + '</div>'
                    + '</td>'
                    + '</tr>'
            }).join('')
        }

        function openModal(mode, record) {
            state.mode = mode
            state.editingOriginalName = record ? record.dict_name : ''
            state.enumSeed = randomEnumSeed()
            state.enumCounter = 1
            modalTitle.textContent = mode === 'create' ? '新增字典' : '编辑枚举项'
            dictNameInput.value = record ? record.dict_name : ''
            if (mode === 'edit' && record) {
                dictNameInput.setAttribute('data-immutable-name', record.dict_name || '')
                dictNameInput.classList.add('immutable-input')
                dictNameInput.title = '字典名称创建后不可修改，可选中复制'
            } else {
                dictNameInput.removeAttribute('data-immutable-name')
                dictNameInput.classList.remove('immutable-input')
                dictNameInput.title = ''
            }
            enabledInput.value = record && !record.dict_word_is_enabled ? 'false' : 'true'
            remarkInput.value = record ? (record.dict_word_remark || '') : ''
            state.items = record && Array.isArray(record.items) && record.items.length
                ? record.items.map(function (item) {
                    return applyItemImageSummary({
                        enum: item.enum,
                        description: item.description,
                        image: item.image || '',
                        imageIds: normalizeAttachmentIdList(item.imageIds || item.image),
                        imageUrls: Array.isArray(item.imageUrls) ? item.imageUrls : (item.imageUrl ? [item.imageUrl] : []),
                        imageAttachments: Array.isArray(item.imageAttachments) ? item.imageAttachments : (item.imageAttachment ? [item.imageAttachment] : []),
                        imageUrl: item.imageUrl || '',
                        imageAttachment: item.imageAttachment || null,
                        sortOrder: item.sortOrder,
                    })
                })
                : [applyItemImageSummary({ enum: '', description: '', image: '', imageIds: [], imageUrls: [], imageAttachments: [], imageUrl: '', imageAttachment: null, sortOrder: 1 })]
            normalizeItemEnums()
            renderItemsEditor()
            editorModal.classList.add('show')
        }

        function closeModal() {
            editorModal.classList.remove('show')
        }

        function scrollEditorModalToBottom() {
            const modalCard = editorModal.querySelector('.modal-card')
            if (!modalCard) {
                return
            }

            const doScroll = function () {
                modalCard.scrollTo({
                    top: modalCard.scrollHeight,
                    behavior: 'smooth',
                })
            }

            if (typeof requestAnimationFrame === 'function') {
                requestAnimationFrame(function () {
                    requestAnimationFrame(doScroll)
                })
            } else {
                setTimeout(doScroll, 40)
            }

            // Fallback: handle heavy DOM updates that render slightly later.
            setTimeout(doScroll, 120)
        }

        function renderItemsEditor() {
            itemsBody.innerHTML = state.items.map(function (item, index) {
                const imageIds = normalizeAttachmentIdList(item.imageIds || item.image)
                const imageUrls = normalizeImageUrlList(item.imageUrls, item.imageUrl, imageIds.length)
                const imageCell = imageIds.length
                    ? '<div class="item-image-grid">' + imageIds.map(function (imageId, imageIndex) {
                        const imageUrl = imageUrls[imageIndex] || ''
                        const thumbHtml = imageUrl
                            ? '<img src="' + escapeHtml(imageUrl) + '" alt="" onclick="window.__previewImage(\'' + escapeJsString(imageUrl) + '\')" />'
                            : '<span>无图</span>'
                        return '<div class="image-tile" draggable="true" ondragstart="window.__startItemImageDrag(' + index + ',' + imageIndex + ', event)" ondragend="window.__endItemImageDrag(event)" ondragover="window.__allowItemImageDrop(event)" ondrop="window.__dropItemImageAt(' + index + ',' + imageIndex + ', event)">'
                            + '<div class="image-tile-thumb">' + thumbHtml + '</div>'
                            + '<div class="image-tile-actions">'
                            + '<span class="image-tile-order">#' + (imageIndex + 1) + '</span>'
                            + '<button class="btn btn-light icon-btn" type="button" title="删除图片" onclick="window.__removeItemImage(' + index + ',' + imageIndex + ')">🗑</button>'
                            + '</div>'
                            + '</div>'
                    }).join('') + '</div>'
                    : '<div class="muted">未上传图片</div>'
                return '<tr>'
                    + '<td>'
                    + '<div class="item-field-stack">'
                    + '<div><div class="item-field-label">描述</div><input data-item-field="description" data-index="' + index + '" value="' + escapeHtml(item.description) + '" /></div>'
                    + '<div><div class="item-field-label">排序</div><input type="number" data-item-field="sortOrder" data-index="' + index + '" value="' + escapeHtml(item.sortOrder) + '" /></div>'
                    + '<div class="item-delete-wrap"><button class="btn btn-danger" type="button" onclick="window.__removeItem(' + index + ')">删除</button></div>'
                    + '</div>'
                    + '</td>'
                    + '<td ondragover="window.__allowItemDrop(event)" ondrop="window.__dropItemImage(' + index + ', event)">'
                    + imageCell
                    + '<div class="image-upload-row">'
                    + '<input type="file" accept="image/*" multiple onchange="window.__uploadItemImages(' + index + ', this)" />'
                    + '<button class="btn btn-light" type="button" onclick="window.__clearItemImage(' + index + ')">清空全部</button>'
                    + '</div>'
                    + '<div class="drop-tip">支持拖拽多张图片上传；图片可拖动调整顺序。</div>'
                    + '</td>'
                    + '</tr>'
            }).join('')
        }

        async function addItemImagesFromFiles(index, files) {
            const validFiles = Array.from(files || []).filter(function (file) {
                return !!file
            })

            if (!validFiles.length) {
                return
            }

            syncItemsStateFromEditor()
            if (!state.items[index]) {
                return
            }

            setStatus('正在上传字典项图片...', '')
            showLoading('正在上传字典项图片，请稍候...')
            try {
                for (let fileIndex = 0; fileIndex < validFiles.length; fileIndex += 1) {
                    const attachment = await uploadAttachment(validFiles[fileIndex], 'dict-item-' + (index + 1) + '-' + (fileIndex + 1))
                    if (!state.items[index]) {
                        break
                    }

                    state.items[index].imageIds = normalizeAttachmentIdList(state.items[index].imageIds || state.items[index].image)
                    state.items[index].imageUrls = normalizeImageUrlList(state.items[index].imageUrls, state.items[index].imageUrl, state.items[index].imageIds.length)
                    state.items[index].imageAttachments = normalizeImageAttachmentList(state.items[index].imageAttachments, state.items[index].imageAttachment, state.items[index].imageIds.length)

                    state.items[index].imageIds.push(attachment.attachments_id || '')
                    state.items[index].imageUrls.push(attachment.attachments_url || '')
                    state.items[index].imageAttachments.push(attachment)
                    applyItemImageSummary(state.items[index])
                }

                renderItemsEditor()
                setStatus('字典项图片上传成功。', 'success')
            } catch (err) {
                setStatus(err.message || '字典项图片上传失败', 'error')
            } finally {
                hideLoading()
            }
        }

        function collectItemsFromEditor() {
            const rows = Array.from(itemsBody.querySelectorAll('tr'))
            return rows.map(function (row, index) {
                const descriptionInput = row.querySelector('[data-item-field="description"]')
                const sortOrderInput = row.querySelector('[data-item-field="sortOrder"]')
                return {
                    enum: state.items[index] ? String(state.items[index].enum || '') : '',
                    description: descriptionInput.value.trim(),
                    image: state.items[index] ? (state.items[index].image || '') : '',
                    imageIds: state.items[index] ? normalizeAttachmentIdList(state.items[index].imageIds || state.items[index].image) : [],
                    imageUrls: state.items[index] ? normalizeImageUrlList(state.items[index].imageUrls, state.items[index].imageUrl, normalizeAttachmentIdList(state.items[index].imageIds || state.items[index].image).length) : [],
                    imageAttachments: state.items[index] ? normalizeImageAttachmentList(state.items[index].imageAttachments, state.items[index].imageAttachment, normalizeAttachmentIdList(state.items[index].imageIds || state.items[index].image).length) : [],
                    imageUrl: state.items[index] ? (state.items[index].imageUrl || '') : '',
                    imageAttachment: state.items[index] ? (state.items[index].imageAttachment || null) : null,
                    sortOrder: Number(sortOrderInput.value || index + 1),
                }
            })
        }

        function syncItemsStateFromEditor() {
            const rows = itemsBody.querySelectorAll('tr')
            if (!rows.length) {
                return
            }
            state.items = collectItemsFromEditor().map(function (item) {
                return applyItemImageSummary(item)
            })
        }

        async function uploadItemImages(index, inputEl) {
            const files = inputEl && inputEl.files ? inputEl.files : []
            await addItemImagesFromFiles(index, files)
        }

        async function loadList() {
            setStatus('正在查询字典列表...', '')
            showLoading('正在查询字典列表...')
            try {
                const data = await request(API_BASE + '/dictionary/list', { keyword: keywordInput.value.trim() })
                state.list = data.items || []
                state.expandedPreviewKey = ''
                populateCategorySelect(state.list)
                renderTable(filterByCategory(state.list))
                setStatus('查询成功，共 ' + state.list.length + ' 条。', 'success')
            } catch (err) {
                setStatus(err.message || '查询失败', 'error')
            } finally {
                hideLoading()
            }
        }

        async function loadDetail() {
            const dictName = detailInput.value.trim()
            if (!dictName) {
                setStatus('请输入 dict_name', 'error')
                return
            }

            setStatus('正在查询字典详情...', '')
            showLoading('正在查询字典详情...')
            try {
                const data = await request(API_BASE + '/dictionary/detail', { dict_name: dictName })
                state.list = [data]
                state.expandedPreviewKey = ''
                populateCategorySelect(state.list)
                renderTable(filterByCategory(state.list))
                setStatus('查询详情成功。', 'success')
            } catch (err) {
                setStatus(err.message || '查询失败', 'error')
            } finally {
                hideLoading()
            }
        }

        async function saveModalRecord() {
            syncItemsStateFromEditor()
            normalizeItemEnums()
            const items = state.items
            const payload = {
                dict_name: state.mode === 'edit' ? state.editingOriginalName : dictNameInput.value.trim(),
                original_dict_name: state.editingOriginalName,
                dict_word_parent_id: '',
                dict_word_is_enabled: enabledInput.value === 'true',
                dict_word_remark: remarkInput.value.trim(),
                items: items,
            }

            setStatus('正在保存字典...', '')
            showLoading('正在保存字典，请稍候...')
            try {
                await request(state.mode === 'create' ? API_BASE + '/dictionary/create' : API_BASE + '/dictionary/update', payload)
                closeModal()
                await loadList()
                setStatus(state.mode === 'create' ? '新增成功。' : '修改成功。', 'success')
            } catch (err) {
                setStatus(err.message || '保存失败', 'error')
            } finally {
                hideLoading()
            }
        }

        async function saveInline(dictName) {
            const targetName = decodeURIComponent(dictName)
            const row = Array.from(tableBody.querySelectorAll('[data-name]')).filter(function (el) {
                return el.getAttribute('data-name') === targetName
            })
            const record = state.list.find(function (item) { return item.dict_name === targetName })

            if (!record) {
                setStatus('未找到对应记录', 'error')
                return
            }

            const payload = {
                original_dict_name: targetName,
                dict_name: targetName,
                dict_word_is_enabled: ((row.find(function (el) { return el.getAttribute('data-field') === 'dict_word_is_enabled' }) || {}).value || 'true') === 'true',
                dict_word_remark: (row.find(function (el) { return el.getAttribute('data-field') === 'dict_word_remark' }) || {}).value || '',
                dict_word_parent_id: '',
                items: record.items || [],
            }

            setStatus('正在保存行数据...', '')
            showLoading('正在保存行数据，请稍候...')
            try {
                await request(API_BASE + '/dictionary/update', payload)
                await loadList()
                setStatus('行内保存成功。', 'success')
            } catch (err) {
                setStatus(err.message || '保存失败', 'error')
            } finally {
                hideLoading()
            }
        }

        async function deleteRow(dictName) {
            const targetName = decodeURIComponent(dictName)
            if (!window.confirm('确认删除字典「' + targetName + '」吗？此操作不可恢复。')) {
                return
            }

            setStatus('正在删除字典...', '')
            showLoading('正在删除字典，请稍候...')
            try {
                await request(API_BASE + '/dictionary/delete', { dict_name: targetName })
                await loadList()
                setStatus('删除成功。', 'success')
            } catch (err) {
                setStatus(err.message || '删除失败', 'error')
            } finally {
                hideLoading()
            }
        }

        window.__editItems = function (dictName) {
            const targetName = decodeURIComponent(dictName)
            const record = state.list.find(function (item) { return item.dict_name === targetName })
            if (!record) {
                setStatus('未找到对应字典', 'error')
                return
            }
            openModal('edit', record)
        }

        window.__saveInline = saveInline
        window.__deleteRow = deleteRow
        window.__toggleEnumPreview = function (previewKey) {
            state.expandedPreviewKey = state.expandedPreviewKey === previewKey ? '' : previewKey
            renderTable(state.list)
        }
        window.__previewImage = function (url) {
            if (!url) {
                return
            }
            imageViewerImg.src = url
            imageViewer.classList.add('show')
        }
        window.__uploadItemImages = uploadItemImages
        window.__allowItemDrop = function (event) {
            event.preventDefault()
        }
        window.__dropItemImage = function (index, event) {
            event.preventDefault()
            const files = event.dataTransfer && event.dataTransfer.files
            addItemImagesFromFiles(index, files)
        }
        window.__startItemImageDrag = function (itemIndex, imageIndex, event) {
            state.draggingImage = {
                itemIndex: itemIndex,
                imageIndex: imageIndex,
            }

            if (event && event.dataTransfer) {
                event.dataTransfer.effectAllowed = 'move'
            }

            const tile = event && event.currentTarget
            if (tile && tile.classList) {
                tile.classList.add('dragging')
            }
        }
        window.__endItemImageDrag = function (event) {
            const tile = event && event.currentTarget
            if (tile && tile.classList) {
                tile.classList.remove('dragging')
            }
        }
        window.__allowItemImageDrop = function (event) {
            event.preventDefault()
            if (event && event.dataTransfer) {
                event.dataTransfer.dropEffect = 'move'
            }
        }
        window.__dropItemImageAt = function (itemIndex, targetImageIndex, event) {
            event.preventDefault()

            const dragging = state.draggingImage
            state.draggingImage = null

            if (!dragging || dragging.itemIndex !== itemIndex) {
                return
            }

            syncItemsStateFromEditor()
            const item = state.items[itemIndex]
            if (!item) {
                return
            }

            const imageIds = normalizeAttachmentIdList(item.imageIds || item.image)
            if (!imageIds.length) {
                return
            }

            const sourceIndex = dragging.imageIndex
            if (sourceIndex === targetImageIndex || sourceIndex < 0 || sourceIndex >= imageIds.length || targetImageIndex < 0 || targetImageIndex >= imageIds.length) {
                return
            }

            const imageUrls = normalizeImageUrlList(item.imageUrls, item.imageUrl, imageIds.length)
            const imageAttachments = normalizeImageAttachmentList(item.imageAttachments, item.imageAttachment, imageIds.length)

            const movedId = imageIds.splice(sourceIndex, 1)[0]
            const movedUrl = imageUrls.splice(sourceIndex, 1)[0]
            const movedAttachment = imageAttachments.splice(sourceIndex, 1)[0]

            imageIds.splice(targetImageIndex, 0, movedId)
            imageUrls.splice(targetImageIndex, 0, movedUrl)
            imageAttachments.splice(targetImageIndex, 0, movedAttachment)

            item.imageIds = imageIds
            item.imageUrls = imageUrls
            item.imageAttachments = imageAttachments
            applyItemImageSummary(item)
            renderItemsEditor()
        }
        window.__removeItemImage = function (index, imageIndex) {
            syncItemsStateFromEditor()
            const item = state.items[index]
            if (!item) {
                return
            }

            const imageIds = normalizeAttachmentIdList(item.imageIds || item.image)
            if (imageIndex < 0 || imageIndex >= imageIds.length) {
                return
            }

            const imageUrls = normalizeImageUrlList(item.imageUrls, item.imageUrl, imageIds.length)
            const imageAttachments = normalizeImageAttachmentList(item.imageAttachments, item.imageAttachment, imageIds.length)

            imageIds.splice(imageIndex, 1)
            imageUrls.splice(imageIndex, 1)
            imageAttachments.splice(imageIndex, 1)

            item.imageIds = imageIds
            item.imageUrls = imageUrls
            item.imageAttachments = imageAttachments
            applyItemImageSummary(item)
            renderItemsEditor()
        }
        window.__clearItemImage = function (index) {
            syncItemsStateFromEditor()
            if (!state.items[index]) {
                return
            }
            state.items[index].image = ''
            state.items[index].imageIds = []
            state.items[index].imageUrls = []
            state.items[index].imageAttachments = []
            state.items[index].imageUrl = ''
            state.items[index].imageAttachment = null
            renderItemsEditor()
            setStatus('已清空该枚举项所有图片。', 'success')
        }
        window.__removeItem = function (index) {
            syncItemsStateFromEditor()
            state.items.splice(index, 1)
            if (!state.items.length) {
                state.items.push(applyItemImageSummary({ enum: '', description: '', image: '', imageIds: [], imageUrls: [], imageAttachments: [], imageUrl: '', imageAttachment: null, sortOrder: 1 }))
            }
            normalizeItemEnums()
            renderItemsEditor()
        }

        function isImmutableNameInput(target) {
            return !!(target && target.matches && target.matches('input[data-immutable-name]'))
        }

        function restoreImmutableInputValue(target) {
            if (!isImmutableNameInput(target)) {
                return
            }
            target.value = target.getAttribute('data-immutable-name') || ''
        }

        function shouldAllowImmutableKey(event) {
            if (event.ctrlKey || event.metaKey || event.altKey) {
                return true
            }

            const key = event.key || ''
            return key === 'Tab'
                || key === 'Shift'
                || key === 'Control'
                || key === 'Meta'
                || key === 'Alt'
                || key === 'CapsLock'
                || key === 'Escape'
                || key === 'Enter'
                || key === 'ArrowLeft'
                || key === 'ArrowRight'
                || key === 'ArrowUp'
                || key === 'ArrowDown'
                || key === 'Home'
                || key === 'End'
                || key === 'PageUp'
                || key === 'PageDown'
        }

        document.addEventListener('beforeinput', function (event) {
            if (!isImmutableNameInput(event.target)) {
                return
            }
            event.preventDefault()
        })

        document.addEventListener('paste', function (event) {
            if (!isImmutableNameInput(event.target)) {
                return
            }
            event.preventDefault()
        })

        document.addEventListener('drop', function (event) {
            if (!isImmutableNameInput(event.target)) {
                return
            }
            event.preventDefault()
        })

        document.addEventListener('keydown', function (event) {
            if (!isImmutableNameInput(event.target)) {
                return
            }
            if (shouldAllowImmutableKey(event)) {
                return
            }
            event.preventDefault()
        })

        document.addEventListener('input', function (event) {
            restoreImmutableInputValue(event.target)
        })

        document.getElementById('listBtn').addEventListener('click', loadList)
        categorySelect.addEventListener('change', function () {
            renderTable(filterByCategory(state.list))
        })
        document.getElementById('detailBtn').addEventListener('click', loadDetail)
        document.getElementById('resetBtn').addEventListener('click', function () {
            keywordInput.value = ''
            categorySelect.value = ''
            detailInput.value = ''
            state.list = []
            renderTable([])
            setStatus('已重置查询条件。', 'success')
        })
        document.getElementById('createBtn').addEventListener('click', function () { openModal('create') })
        document.getElementById('closeModalBtn').addEventListener('click', closeModal)
        document.getElementById('cancelBtn').addEventListener('click', closeModal)
        document.getElementById('closeImageViewerBtn').addEventListener('click', function () {
            imageViewer.classList.remove('show')
            imageViewerImg.src = ''
        })
        imageViewer.addEventListener('click', function (event) {
            if (event.target === imageViewer) {
                imageViewer.classList.remove('show')
                imageViewerImg.src = ''
            }
        })
        document.addEventListener('keydown', function (event) {
            if (event.key === 'Escape' && imageViewer.classList.contains('show')) {
                imageViewer.classList.remove('show')
                imageViewerImg.src = ''
            }
        })
        document.getElementById('addItemBtn').addEventListener('click', function () {
            syncItemsStateFromEditor()
            const used = new Set(state.items.map(function (item) { return String(item.enum || '') }))
            state.items.push(applyItemImageSummary({ enum: nextAutoEnum(used), description: '', image: '', imageIds: [], imageUrls: [], imageAttachments: [], imageUrl: '', imageAttachment: null, sortOrder: state.items.length + 1 }))
            renderItemsEditor()
            scrollEditorModalToBottom()
        })
        document.getElementById('saveBtn').addEventListener('click', saveModalRecord)

        loadList()
    </script>
    {{ template "theme_body" . }}
</body>
</html>
