<template>
    <el-main class="material-main">
        <el-card shadow="never" class="material-card">
            <el-tabs v-model="fileType" class="material-tabs" @tab-change="onFileTypeChange">
                <el-tab-pane label="图片" name="1" />
                <el-tab-pane label="视频" name="2" />
            </el-tabs>

            <div class="material-body">
                <aside class="material-side" v-loading="categoryLoading">
                    <div class="category-head">
                        <span>分类</span>
                        <el-button type="primary" link @click="openCategoryDialog()">新建</el-button>
                    </div>
                    <el-scrollbar class="category-scroll">
                        <ul class="category-list">
                            <li
                                :class="{ active: activeCategoryId === '' }"
                                @click="selectCategory('')"
                            >
                                <el-icon><el-icon-folder /></el-icon>
                                <span>{{ fileType === '1' ? '全部图片' : '全部视频' }}</span>
                            </li>
                            <li
                                v-for="item in flatCategories"
                                :key="item.id"
                                :class="{ active: String(activeCategoryId) === String(item.id) }"
                                :style="{ paddingLeft: 12 + item.level * 14 + 'px' }"
                                @click="selectCategory(item.id)"
                            >
                                <el-icon><el-icon-folder /></el-icon>
                                <span class="category-name" :title="item.name">{{ item.name }}</span>
                                <div class="category-actions" @click.stop>
                                    <el-button type="primary" link size="small" @click="openCategoryDialog(item)">编辑</el-button>
                                    <el-button type="danger" link size="small" @click="removeCategory(item)">删除</el-button>
                                </div>
                            </li>
                        </ul>
                    </el-scrollbar>
                </aside>

                <section class="material-content">
                    <div class="toolbar">
                        <div class="toolbar-left">
                            <el-upload
                                ref="uploaderRef"
                                class="material-upload"
                                action=""
                                multiple
                                :show-file-list="false"
                                :accept="uploadAccept"
                                :http-request="uploadRequest"
                            >
                                <el-button type="primary">{{ fileType === '1' ? '上传图片' : '上传视频' }}</el-button>
                            </el-upload>
                            <el-button @click="toggleSelectAll">{{ allSelected ? '取消全选' : '全选' }}</el-button>
                            <el-button type="danger" plain :disabled="!selectedIds.length" @click="removeSelected">
                                {{ fileType === '1' ? '删除图片' : '删除视频' }}
                            </el-button>
                        </div>
                        <div class="toolbar-right">
                            <el-input
                                v-model="keyword"
                                clearable
                                :placeholder="fileType === '1' ? '搜索图片名称' : '搜索视频名称'"
                                style="width: 220px"
                                @keyup.enter="searchFiles"
                                @clear="searchFiles"
                            >
                                <template #append>
                                    <el-button :icon="Search" @click="searchFiles" />
                                </template>
                            </el-input>
                            <el-button-group class="view-toggle">
                                <el-button :type="viewMode === 'grid' ? 'primary' : 'default'" @click="setViewMode('grid')">
                                    <el-icon><GridIcon /></el-icon>
                                </el-button>
                                <el-button :type="viewMode === 'list' ? 'primary' : 'default'" @click="setViewMode('list')">
                                    <el-icon><ListIcon /></el-icon>
                                </el-button>
                            </el-button-group>
                        </div>
                    </div>

                    <div v-loading="listLoading" class="file-panel">
                        <el-empty v-if="!fileList.length && !listLoading" description="暂无素材" />

                        <el-scrollbar v-else-if="viewMode === 'grid'" class="file-grid-scroll">
                            <div class="file-grid">
                                <div
                                    v-for="item in fileList"
                                    :key="item.id"
                                    class="file-item"
                                    :class="{ selected: isSelected(item.id) }"
                                >
                                    <div class="file-thumb" @click="toggleSelect(item.id)">
                                        <el-image
                                            v-if="fileType === '1'"
                                            :src="item.url"
                                            fit="cover"
                                            lazy
                                        />
                                        <div v-else class="video-thumb">
                                            <video v-if="item.url" :src="item.url" preload="metadata" />
                                            <el-icon v-else class="video-icon"><el-icon-video-play /></el-icon>
                                        </div>
                                        <div class="file-check">
                                            <el-icon v-if="isSelected(item.id)"><el-icon-check /></el-icon>
                                        </div>
                                    </div>
                                    <p class="file-name" :title="item.name">{{ item.name }}</p>
                                    <div class="file-actions">
                                        <span @click.stop="previewItem(item)">查看</span>
                                        <span class="danger" @click.stop="removeOne(item)">删除</span>
                                    </div>
                                </div>
                            </div>
                        </el-scrollbar>

                        <el-table
                            v-else
                            ref="fileTableRef"
                            :data="fileList"
                            row-key="id"
                            border
                            stripe
                            @selection-change="onTableSelectionChange"
                        >
                            <el-table-column type="selection" width="48" />
                            <el-table-column label="预览" width="100">
                                <template #default="{ row }">
                                    <el-image
                                        v-if="fileType === '1'"
                                        :src="row.url"
                                        fit="cover"
                                        style="width: 56px; height: 56px; border-radius: 4px;"
                                        :preview-src-list="[row.url]"
                                        preview-teleported
                                    />
                                    <el-tag v-else size="small">视频</el-tag>
                                </template>
                            </el-table-column>
                            <el-table-column prop="name" label="名称" min-width="200" show-overflow-tooltip />
                            <el-table-column label="大小" width="90" show-overflow-tooltip>
                                <template #default="{ row }">{{ row.raw?.att_size || "-" }}</template>
                            </el-table-column>
                            <el-table-column prop="id" label="ID" width="80" />
                        </el-table>

                        <div v-if="total > query.limit" class="pager">
                            <el-pagination
                                background
                                layout="total, prev, pager, next, sizes"
                                :total="total"
                                :current-page="query.page"
                                :page-size="query.limit"
                                :page-sizes="[20, 40, 60, 100]"
                                @current-change="handlePageChange"
                                @size-change="handleSizeChange"
                            />
                        </div>
                    </div>
                </section>
            </div>
        </el-card>

        <el-dialog
            v-model="categoryDialogVisible"
            :title="categoryForm.id ? '编辑分类' : '新建分类'"
            width="440px"
            destroy-on-close
            @closed="resetCategoryForm"
        >
            <el-form ref="categoryFormRef" :model="categoryForm" :rules="categoryRules" label-width="90px">
                <el-form-item label="分类名称" prop="name">
                    <el-input v-model="categoryForm.name" clearable maxlength="40" />
                </el-form-item>
                <el-form-item label="上级分类" prop="pid">
                    <el-select v-model="categoryForm.pid" placeholder="顶级分类" clearable style="width: 100%">
                        <el-option label="顶级分类" value="0" />
                        <el-option
                            v-for="c in parentCategoryOptions"
                            :key="c.id"
                            :label="c.name"
                            :value="String(c.id)"
                        />
                    </el-select>
                </el-form-item>
            </el-form>
            <template #footer>
                <el-button @click="categoryDialogVisible = false">取消</el-button>
                <el-button type="primary" :loading="categorySaving" @click="submitCategory">保存</el-button>
            </template>
        </el-dialog>

        <el-dialog
            v-model="previewVisible"
            :title="previewTitle"
            width="auto"
            class="material-preview-dialog"
            append-to-body
            destroy-on-close
            align-center
            @closed="onPreviewClosed"
        >
            <img v-if="previewType === 'image' && previewUrl" :src="previewUrl" class="preview-media" alt="" />
            <video
                v-else-if="previewType === 'video' && previewUrl"
                :src="previewUrl"
                class="preview-media"
                controls
                autoplay
            />
        </el-dialog>
    </el-main>
</template>

<script>
import { Search, Grid as GridIcon, List as ListIcon } from "@element-plus/icons-vue";
import uploadConfig from "@/config/upload";

export default {
    name: "MaterialManage",
    components: { GridIcon, ListIcon },
    data() {
        return {
            Search,
            fileType: "1",
            viewMode: "grid",
            keyword: "",
            activeCategoryId: "",
            categoryLoading: false,
            listLoading: false,
            categorySaving: false,
            categories: [],
            flatCategories: [],
            parentCategoryOptions: [],
            fileList: [],
            selectedIds: [],
            total: 0,
            query: { page: 1, limit: 40 },
            categoryDialogVisible: false,
            categoryForm: { id: "", name: "", pid: "0" },
            categoryRules: {
                name: [{ required: true, message: "请输入分类名称", trigger: "blur" }],
            },
            /** 程序同步表格勾选时，忽略 selection-change 避免清空 selectedIds */
            syncingTableSelection: false,
            previewVisible: false,
            previewUrl: "",
            previewTitle: "",
            previewType: "image",
        };
    },
    computed: {
        uploadAccept() {
            return this.fileType === "1" ? "image/*" : "video/*";
        },
        allSelected() {
            if (!this.fileList.length) return false;
            const set = new Set(this.selectedIds.map((id) => String(id)));
            return this.fileList.every((f) => set.has(String(f.id)));
        },
    },
    watch: {
        fileList() {
            if (this.viewMode === "list") {
                this.$nextTick(() => this.syncTableSelection());
            }
        },
    },
    created() {
        this.bootstrap();
    },
    methods: {
        /** 兼容 code / status 两种成功标识 */
        apiOk(res) {
            if (!res) return false;
            const code = res.code ?? res.status;
            return code === 1 || code === 200;
        },
        async bootstrap() {
            await this.loadCategories();
            await this.loadFiles();
        },
        onFileTypeChange() {
            this.activeCategoryId = "";
            this.keyword = "";
            this.query.page = 1;
            this.selectedIds = [];
            this.loadCategories();
            this.loadFiles();
        },
        selectCategory(id) {
            this.activeCategoryId = id === "" ? "" : String(id);
            this.query.page = 1;
            this.selectedIds = [];
            this.loadFiles();
        },
        flattenCategories(nodes, level = 0, out = []) {
            (nodes || []).forEach((node) => {
                const id = node.id ?? node.value;
                const name = node.name ?? node.title ?? node.label ?? node.cate_name ?? `分类#${id}`;
                if (id != null && id !== "") {
                    out.push({
                        id,
                        name,
                        level,
                        pid: node.pid ?? node.parent_id ?? 0,
                        raw: node,
                    });
                }
                const children = node.children || node.child || [];
                if (children.length) this.flattenCategories(children, level + 1, out);
            });
            return out;
        },
        /** 分类：{ data: { list: [{ id, pid, name, title, children }] } } */
        parseCategoryRows(data) {
            if (Array.isArray(data)) return data;
            const d = data || {};
            if (Array.isArray(d.list)) return d.list;
            if (Array.isArray(d.data)) return d.data;
            return d.children || [];
        },
        parseFileRows(data) {
            if (Array.isArray(data)) return { list: data, total: data.length };
            const d = data || {};
            const list = d.list || d.data || d.rows || [];
            const total = Number(d.count ?? d.total ?? list.length ?? 0);
            return { list, total };
        },
        /** 文件项：att_id、real_name、att_dir（缩略图可用 satt_dir） */
        normalizeFileRow(row) {
            const rawId = row.att_id ?? row.id;
            const id = rawId != null && rawId !== "" ? String(rawId) : "";
            const url = row.att_dir || row.satt_dir || row.url || row.src || "";
            const name = row.real_name || row.name || row.file_name || `素材#${id || ""}`;
            return {
                id,
                name,
                url,
                raw: row,
            };
        },
        isSelected(id) {
            if (id == null || id === "") return false;
            return this.selectedIds.some((x) => String(x) === String(id));
        },
        async loadCategories() {
            this.categoryLoading = true;
            try {
                const res = await this.$API.material.categoryList.get(this.fileType);
                if (!this.apiOk(res)) {
                    this.$message.error(res.msg || res.message || "获取分类失败");
                    this.categories = [];
                    this.flatCategories = [];
                    return;
                }
                this.categories = this.parseCategoryRows(res.data);
                this.flatCategories = this.flattenCategories(this.categories);
            } finally {
                this.categoryLoading = false;
            }
        },
        async loadParentCategories() {
            const res = await this.$API.material.categoryParentList.get(this.fileType);
            if (!this.apiOk(res)) {
                this.parentCategoryOptions = [];
                return;
            }
            const rows = this.parseCategoryRows(res.data);
            this.parentCategoryOptions = this.flattenCategories(rows).map((x) => ({
                id: x.id,
                name: x.name,
            }));
        },
        buildFileQuery() {
            const params = {
                file_type: this.fileType,
                real_name: this.keyword || "",
                page: this.query.page,
                limit: this.query.limit,
            };
            if (this.activeCategoryId !== "") {
                params.pid = this.activeCategoryId;
                params.category_id = this.activeCategoryId;
                params.cate_id = this.activeCategoryId;
                params.relation_id = this.activeCategoryId;
            }
            return params;
        },
        async loadFiles() {
            this.listLoading = true;
            try {
                const res = await this.$API.material.fileList.get(this.buildFileQuery());
                if (!this.apiOk(res)) {
                    this.$message.error(res.msg || res.message || "获取素材列表失败");
                    this.fileList = [];
                    this.total = 0;
                    return;
                }
                const { list, total } = this.parseFileRows(res.data);
                this.fileList = list.map((row) => this.normalizeFileRow(row)).filter((x) => x.id != null);
                this.total = total;
                this.selectedIds = this.selectedIds.filter((id) =>
                    this.fileList.some((f) => String(f.id) === String(id))
                );
            } finally {
                this.listLoading = false;
            }
        },
        searchFiles() {
            this.query.page = 1;
            this.loadFiles();
        },
        handlePageChange(page) {
            this.query.page = page;
            this.loadFiles();
        },
        handleSizeChange(limit) {
            this.query.limit = limit;
            this.query.page = 1;
            this.loadFiles();
        },
        toggleSelect(id) {
            const sid = String(id);
            if (this.isSelected(id)) {
                this.selectedIds = this.selectedIds.filter((x) => String(x) !== sid);
            } else {
                this.selectedIds = [...this.selectedIds, sid];
            }
        },
        setViewMode(mode) {
            this.viewMode = mode;
            if (mode === "list") {
                this.$nextTick(() => this.syncTableSelection());
            }
        },
        syncTableSelection() {
            const table = this.$refs.fileTableRef;
            if (!table || this.viewMode !== "list") return;
            this.syncingTableSelection = true;
            table.clearSelection();
            this.fileList.forEach((row) => {
                if (this.isSelected(row.id)) {
                    table.toggleRowSelection(row, true);
                }
            });
            this.$nextTick(() => {
                this.syncingTableSelection = false;
            });
        },
        toggleSelectAll() {
            if (this.allSelected) {
                this.selectedIds = [];
                if (this.viewMode === "list") {
                    this.$refs.fileTableRef?.clearSelection();
                }
                return;
            }
            this.selectedIds = this.fileList.map((f) => f.id);
            if (this.viewMode === "list") {
                this.$nextTick(() => this.syncTableSelection());
            }
        },
        onTableSelectionChange(rows) {
            if (this.syncingTableSelection) return;
            this.selectedIds = rows.map((r) => r.id);
        },
        previewItem(item) {
            if (!item?.url) {
                this.$message.warning("暂无预览地址");
                return;
            }
            this.previewUrl = item.url;
            this.previewTitle = item.name || "预览";
            this.previewType = this.fileType === "2" ? "video" : "image";
            this.previewVisible = true;
        },
        onPreviewClosed() {
            this.previewUrl = "";
            this.previewTitle = "";
        },
        async removeOne(item) {
            const confirm = await this.$confirm(`确认删除「${item.name}」吗？`, "提示", {
                type: "warning",
            }).catch(() => {});
            if (confirm !== "confirm") return;
            const res = await this.$API.material.fileDelete.delete([item.id]);
            if (!this.apiOk(res)) {
                this.$message.error(res.msg || res.message || "删除失败");
                return;
            }
            this.$message.success(res.msg || res.message || "删除成功");
            this.selectedIds = this.selectedIds.filter((id) => String(id) !== String(item.id));
            this.loadFiles();
        },
        async removeSelected() {
            if (!this.selectedIds.length) return;
            const confirm = await this.$confirm(
                `确认删除选中的 ${this.selectedIds.length} 个素材吗？`,
                "提示",
                { type: "warning" }
            ).catch(() => {});
            if (confirm !== "confirm") return;
            const res = await this.$API.material.fileDelete.delete(this.selectedIds);
            if (!this.apiOk(res)) {
                this.$message.error(res.msg || res.message || "删除失败");
                return;
            }
            this.$message.success(res.msg || res.message || "删除成功");
            this.selectedIds = [];
            this.loadFiles();
        },
        uploadRequest(param) {
            const data = new FormData();
            data.append(uploadConfig.filename || "file", param.file);
            if (this.activeCategoryId !== "") {
                data.append("pid", String(this.activeCategoryId));
                data.append("category_id", String(this.activeCategoryId));
            }
            this.$API.material.upload
                .post(this.fileType, data, {
                    onUploadProgress: (e) => {
                        if (e.total) {
                            const percent = Math.round((e.loaded / e.total) * 100);
                            param.onProgress({ percent });
                        }
                    },
                })
                .then((res) => {
                    const parsed = uploadConfig.parseData(res);
                    const uploadCode = parsed.code ?? res.status ?? res.code;
                    if (uploadCode == uploadConfig.successCode || this.apiOk(res)) {
                        param.onSuccess(res);
                        this.loadFiles();
                    } else {
                        param.onError(parsed.msg || res.msg || "上传失败");
                    }
                })
                .catch((err) => {
                    param.onError(err);
                });
        },
        resetCategoryForm() {
            this.categoryForm = { id: "", name: "", pid: "0" };
        },
        async openCategoryDialog(row) {
            await this.loadParentCategories();
            if (row) {
                const raw = row.raw || row;
                this.categoryForm = {
                    id: String(row.id),
                    name: row.name,
                    pid: String(raw.pid ?? "0"),
                };
            } else {
                this.resetCategoryForm();
                if (this.activeCategoryId !== "") {
                    this.categoryForm.pid = String(this.activeCategoryId);
                }
            }
            this.categoryDialogVisible = true;
            this.$nextTick(() => this.$refs.categoryFormRef?.clearValidate());
        },
        submitCategory() {
            this.$refs.categoryFormRef.validate(async (valid) => {
                if (!valid) return false;
                this.categorySaving = true;
                try {
                    const payload = {
                        name: this.categoryForm.name,
                        pid: String(this.categoryForm.pid || "0"),
                        file_type: this.fileType,
                    };
                    const res = await this.$API.material.categorySave.post(
                        this.categoryForm.id || 0,
                        payload
                    );
                    if (!this.apiOk(res)) {
                        this.$message.error(res.msg || res.message || "保存失败");
                        return false;
                    }
                    this.$message.success(res.msg || res.message || "保存成功");
                    this.categoryDialogVisible = false;
                    await this.loadCategories();
                } finally {
                    this.categorySaving = false;
                }
            });
        },
        async removeCategory(row) {
            const confirm = await this.$confirm(`确认删除分类「${row.name}」吗？`, "提示", {
                type: "warning",
            }).catch(() => {});
            if (confirm !== "confirm") return;
            const res = await this.$API.material.categoryDelete.delete(row.id);
            if (!this.apiOk(res)) {
                this.$message.error(res.msg || res.message || "删除失败");
                return;
            }
            this.$message.success(res.msg || res.message || "删除成功");
            if (String(this.activeCategoryId) === String(row.id)) {
                this.activeCategoryId = "";
            }
            await this.loadCategories();
            this.loadFiles();
        },
    },
};
</script>

<style scoped>
.material-main {
    padding: 0;
}
.material-card :deep(.el-card__body) {
    padding-top: 8px;
}
.material-tabs {
    margin-bottom: 0;
}
.material-body {
    display: flex;
    min-height: calc(100vh - 220px);
    border-top: 1px solid var(--el-border-color-lighter);
    margin-top: 8px;
}
.material-side {
    width: 220px;
    flex-shrink: 0;
    border-right: 1px solid var(--el-border-color-lighter);
    display: flex;
    flex-direction: column;
}
.category-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 12px 12px 8px;
    font-size: 13px;
    color: var(--el-text-color-secondary);
}
.category-scroll {
    flex: 1;
    height: 0;
}
.category-list {
    list-style: none;
    margin: 0;
    padding: 0 0 12px;
}
.category-list li {
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 10px 12px;
    cursor: pointer;
    font-size: 14px;
    transition: background 0.15s;
}
.category-list li:hover {
    background: var(--el-fill-color-light);
}
.category-list li.active {
    background: var(--el-color-primary-light-9);
    color: var(--el-color-primary);
}
.category-name {
    flex: 1;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.category-actions {
    display: none;
    flex-shrink: 0;
}
.category-list li:hover .category-actions {
    display: flex;
    gap: 2px;
}
.material-content {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    padding: 16px;
}
.toolbar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    margin-bottom: 16px;
}
.toolbar-left,
.toolbar-right {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 10px;
}
.material-upload {
    display: inline-block;
}
.view-toggle {
    margin-left: 4px;
}
.file-panel {
    flex: 1;
    min-height: 320px;
}
.file-grid-scroll {
    height: calc(100vh - 320px);
}
.file-grid {
    --material-thumb-size: 120px;
    display: grid;
    grid-template-columns: repeat(auto-fill, var(--material-thumb-size));
    gap: 16px;
    padding: 4px 4px 16px;
    justify-content: start;
}
.file-item {
    width: var(--material-thumb-size);
    user-select: none;
}
.file-item .file-thumb {
    cursor: pointer;
}
.file-thumb {
    position: relative;
    width: var(--material-thumb-size);
    height: var(--material-thumb-size);
    flex-shrink: 0;
    border: 1px solid var(--el-border-color-lighter);
    border-radius: 4px;
    overflow: hidden;
    background: var(--el-fill-color-lighter);
}
.file-thumb :deep(.el-image) {
    display: block;
    width: 100%;
    height: 100%;
}
.file-thumb :deep(.el-image__inner) {
    width: 100%;
    height: 100%;
    object-fit: cover;
}
.video-thumb {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    background: #1a1a1a;
}
.video-thumb video {
    width: 100%;
    height: 100%;
    max-width: 100%;
    max-height: 100%;
    object-fit: cover;
    display: block;
}
.video-icon {
    font-size: 36px;
    color: #fff;
}
.file-check {
    position: absolute;
    top: 6px;
    right: 6px;
    z-index: 2;
    width: 20px;
    height: 20px;
    border: 1px solid #fff;
    background: rgba(0, 0, 0, 0.35);
    display: flex;
    align-items: center;
    justify-content: center;
    color: #fff;
    font-size: 12px;
    box-sizing: border-box;
}
.file-item.selected .file-thumb {
    border-color: var(--el-color-primary);
    box-shadow: 0 0 0 2px var(--el-color-primary-light-7);
}
.file-item.selected .file-check {
    background: var(--el-color-primary);
    border-color: var(--el-color-primary);
}
.file-item.selected .file-check .el-icon {
    display: flex;
}
.file-check .el-icon {
    display: none;
}
.file-name {
    width: var(--material-thumb-size);
    margin: 8px 0 0;
    font-size: 12px;
    line-height: 1.4;
    text-align: center;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    color: var(--el-text-color-regular);
}
.file-actions {
    display: none;
    align-items: center;
    justify-content: center;
    gap: 16px;
    margin-top: 6px;
    font-size: 12px;
    line-height: 1.5;
}
.file-item:hover .file-actions {
    display: flex;
}
.file-actions span {
    color: var(--el-color-primary);
    cursor: pointer;
}
.file-actions span:hover {
    opacity: 0.85;
}
.file-actions span.danger {
    color: var(--el-color-danger);
}
.material-preview-dialog :deep(.el-dialog__body) {
    padding: 12px 20px 20px;
    text-align: center;
}
.preview-media {
    display: block;
    max-width: min(90vw, 960px);
    max-height: 75vh;
    margin: 0 auto;
    object-fit: contain;
}
.pager {
    margin-top: 16px;
    display: flex;
    justify-content: flex-end;
}
</style>
