Sfoglia il codice sorgente

feat(salesOps): 实现书单/专题管理及首页装修功能

ylong 4 giorni fa
parent
commit
9e5ade13fc

+ 18 - 0
src/components/CommonPage/CommonTable.vue

@@ -203,11 +203,29 @@
         return selections.value;
     }
 
+    function setSelectedRows(rows) {
+        selections.value = rows;
+        // Also try to toggle if table instance is available and supports it, 
+        // ensuring visual sync if v-model isn't enough for pre-load
+        if (tableRef.value?.toggleRowSelection) {
+             rows.forEach(row => {
+                 tableRef.value.toggleRowSelection(row, true);
+             });
+        }
+    }
+
+    function clearSelection() {
+        selections.value = [];
+        tableRef.value?.clearSelection?.();
+    }
+
     defineExpose({
         reload,
         exportData,
         operatBatch,
         getSelections,
+        setSelectedRows,
+        clearSelection,
         messageBoxConfirm,
     });
 </script>

+ 95 - 0
src/views/salesOps/booklist/components/booklist-bind.vue

@@ -0,0 +1,95 @@
+<template>
+    <goods-select-dialog ref="goodsSelectRef" v-model="visible" :default-selected="selectedBooks" title="绑定图书"
+        @confirm="handleConfirm" />
+</template>
+
+<script setup>
+import { ref } from 'vue';
+import GoodsSelectDialog from '../../components/GoodsSelectDialog.vue';
+import request from '@/utils/request';
+import { EleMessage } from 'ele-admin-plus/es';
+
+const emit = defineEmits(['success']);
+
+const visible = ref(false);
+const selectedBooks = ref([]);
+const originalIsbns = ref([]);
+const currentId = ref(null);
+const goodsSelectRef = ref(null);
+
+const handleOpen = async (row) => {
+    currentId.value = row.id;
+    selectedBooks.value = [];
+    originalIsbns.value = [];
+    
+    // Reset dialog state
+    if (goodsSelectRef.value?.reset) {
+        goodsSelectRef.value.reset();
+    }
+    
+    await loadBoundBooks(row.id);
+    visible.value = true;
+};
+
+const loadBoundBooks = async (id) => {
+    try {
+        const res = await request.get('/book/showIndex/queryBindBook', {
+            params: { showCateId: id, type: 1, pageNum: 1, pageSize: 1000 }
+        });
+        const list = res.data?.rows || res.data || [];
+        
+        // Map bookIsbn to isbn to match GoodsSelectDialog rowKey
+        const mappedList = (Array.isArray(list) ? list : []).map(item => ({
+            ...item,
+            isbn: item.bookIsbn || item.isbn
+        }));
+        
+        selectedBooks.value = mappedList;
+        originalIsbns.value = mappedList.map(b => b.isbn);
+    } catch (e) {
+    }
+};
+
+const handleConfirm = async (rows) => {
+    if (!currentId.value) return;
+    
+    const loading = EleMessage.loading('正在保存...');
+    try {
+        const currentIsbns = rows.map(b => b.isbn);
+        
+        const toAdd = currentIsbns.filter(isbn => !originalIsbns.value.includes(isbn));
+        const toRemove = originalIsbns.value.filter(isbn => !currentIsbns.includes(isbn));
+        
+        const promises = [];
+        
+        if (toAdd.length > 0) {
+            promises.push(request.post('/book/showIndex/bindBook', {
+                showCateId: currentId.value,
+                bookIsbnList: toAdd
+            }));
+        }
+        
+        if (toRemove.length > 0) {
+            promises.push(request.post('/book/showIndex/unBindBook', {
+                showCateId: currentId.value,
+                bookIsbnList: toRemove
+            }));
+        }
+        
+        await Promise.all(promises);
+        
+        loading.close();
+        EleMessage.success('绑定成功');
+        visible.value = false;
+        emit('success');
+    } catch (e) {
+        loading.close();
+        console.error(e);
+        EleMessage.error(e.message || '操作失败');
+    }
+};
+
+defineExpose({
+    handleOpen
+});
+</script>

+ 90 - 90
src/views/salesOps/booklist/components/booklist-edit.vue

@@ -1,114 +1,114 @@
 <template>
-    <simple-form-modal ref="modalRef" :items="formItems" :baseUrl="baseUrl" @success="handleSuccess" :title="title"
-        width="600px" labelWidth="100px">
-        <template #relatedItems="{ model }">
-            <div class="flex items-center space-x-4">
-                <el-checkbox v-model="isSpecified" label="指定商品" />
-                <div v-if="isSpecified" class="flex items-center text-gray-600">
-                    <span>已选择 <span class="text-red-500 font-bold">{{ (model.relatedItems || []).length }}</span>
-                        款商品,</span>
-                    <el-button link type="primary" @click="openGoodsDialog(model)">选择商品 ></el-button>
+    <ele-modal :width="600" v-model="visible" :title="title" @confirm="handleConfirm" @cancel="handleCancel"
+        :confirm-loading="loading">
+        <el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
+            <el-form-item label="书单名称" prop="showName">
+                <el-input v-model="form.showName" placeholder="请输入书单名称" />
+            </el-form-item>
+
+            <el-form-item label="是否上线" prop="showStatus">
+                <el-switch v-model="form.showStatus" :active-value="1" :inactive-value="2" />
+            </el-form-item>
+
+            <el-form-item label="书单banner" prop="imgUrl">
+                <div class="flex flex-col">
+                    <image-upload v-model="form.imgUrl" :limit="1" :fileSize="2" fileType="jpg" />
+                    <span class="text-gray-400 text-xs mt-1">建议尺寸:750*350</span>
                 </div>
-            </div>
-        </template>
+            </el-form-item>
 
-        <template #status="{ model }">
-            <el-switch v-model="model.status" active-value="online" inactive-value="offline" />
-        </template>
+            <el-form-item label="书单描述" prop="remark">
+                <el-input v-model="form.remark" type="textarea" placeholder="请输入书单描述" />
+            </el-form-item>
+        </el-form>
 
-        <template #banner="{ model }">
-            <div class="flex flex-col">
-                <image-upload v-model="model.banner" :limit="1" :fileSize="2" fileType="jpg" />
-                <span class="text-gray-400 text-xs mt-1">选择背景图</span>
+        <template #footer>
+            <div class="flex justify-end items-center">
+                <el-button @click="handleCancel">取消</el-button>
+                <el-button type="primary" :loading="loading" @click="handleConfirm">确定</el-button>
             </div>
         </template>
-    </simple-form-modal>
-
-    <goods-select-dialog v-model="goodsDialogVisible" :default-selected="selectedGoods" @confirm="handleGoodsConfirm" />
+    </ele-modal>
 </template>
 
 <script setup>
-import { ref, computed } from 'vue';
-import SimpleFormModal from '@/components/CommonPage/SimpleFormModal.vue';
-import GoodsSelectDialog from '../../components/GoodsSelectDialog.vue';
+import { ref, reactive, computed } from 'vue';
 import ImageUpload from '@/components/ImageUpload/index.vue';
+import request from '@/utils/request';
+import { EleMessage } from 'ele-admin-plus/es';
 
 const emit = defineEmits(['success']);
-const modalRef = ref(null);
-const title = ref('添加书单');
-const goodsDialogVisible = ref(false);
-const selectedGoods = ref([]);
-const isSpecified = ref(true); // Default checked as per image
-const currentFormModel = ref(null);
 
-const baseUrl = {
-    add: '/salesOps/booklist/add',
-    update: '/salesOps/booklist/update'
-};
+const visible = ref(false);
+const loading = ref(false);
+const formRef = ref(null);
 
-const formItems = computed(() => [
-    {
-        label: '书单名称',
-        prop: 'name',
-        type: 'input',
-        required: true,
-        props: { placeholder: '请输入书单名称' }
-    },
-    {
-        label: '关联商品',
-        prop: 'relatedItems',
-        type: 'relatedItems',
-        slot: 'relatedItems',
-        required: true,
-    },
-    {
-        label: '是否上线',
-        prop: 'status',
-        type: 'status',
-        slot: 'status',
-        required: true,
-    },
-    {
-        label: '书单banner',
-        prop: 'banner',
-        type: 'banner',
-        slot: 'banner',
-        required: true,
-    },
-    {
-        label: '书单描述',
-        prop: 'description',
-        type: 'textarea',
-        required: true,
-        props: { placeholder: '请输入书单描述' }
-    },
-]);
+const form = reactive({
+    id: undefined,
+    showName: '',
+    imgUrl: '',
+    showStatus: 1,
+    remark: '',
+    cateType: 2  //书单
+});
 
-const handleOpen = (data) => {
-    title.value = data && data.id ? '编辑书单' : '添加书单';
-    selectedGoods.value = data && data.relatedGoods ? data.relatedGoods : [];
-    modalRef.value.handleOpen(data);
+const rules = {
+    showName: [{ required: true, message: '请输入书单名称', trigger: 'blur' }],
+    imgUrl: [{ required: true, message: '请上传Banner', trigger: 'change' }],
+    showStatus: [{ required: true, message: '请选择状态', trigger: 'change' }]
 };
 
-const handleSuccess = (data) => {
-    emit('success', data);
-};
+const title = computed(() => form.id ? '编辑书单' : '添加书单');
 
-const openGoodsDialog = (model) => {
-    currentFormModel.value = model;
-    if (!model.relatedItems) {
-        model.relatedItems = [];
+const handleOpen = async (row) => {
+    visible.value = true;
+    
+    if (row) {
+        Object.assign(form, {
+            id: row.id,
+            showName: row.showName,
+            imgUrl: row.imgUrl,
+            showStatus: row.showStatus,
+            remark: row.remark,
+            cateType: 2  //书单
+        });
+    } else {
+        Object.assign(form, {
+            id: undefined,
+            showName: '',
+            imgUrl: '',
+            showStatus: 1,
+            remark: '',
+            cateType: 2  //书单
+        });
     }
-    selectedGoods.value = model.relatedItems;
-    goodsDialogVisible.value = true;
 };
 
-const handleGoodsConfirm = (rows) => {
-    selectedGoods.value = rows;
-    if (currentFormModel.value) {
-        currentFormModel.value.relatedItems = rows;
-    }
-    goodsDialogVisible.value = false;
+const handleConfirm = () => {
+    formRef.value?.validate(async (valid) => {
+        if (!valid) return;
+        loading.value = true;
+        try {
+            const params = { ...form };
+            if (!params.id) {
+                delete params.id;
+            }
+            await request.post('/book/showIndex/addSpecial', params);
+            
+            EleMessage.success(title.value + '成功');
+            visible.value = false;
+            emit('success');
+        } catch (e) {
+            console.error(e);
+            EleMessage.error(e.message || '操作失败');
+        } finally {
+            loading.value = false;
+        }
+    });
+};
+
+const handleCancel = () => {
+    visible.value = false;
 };
 
 defineExpose({

+ 124 - 124
src/views/salesOps/booklist/index.vue

@@ -3,7 +3,7 @@
         <!-- Search Bar -->
         <div class="bg-white p-4 mb-4 rounded shadow-sm flex items-center justify-between">
             <div class="flex items-center space-x-2">
-                <el-input v-model="searchQuery" placeholder="请输入书单名称" style="width: 240px" clearable />
+                <el-input v-model="searchForm.name" placeholder="请输入书单名称" style="width: 240px" clearable />
                 <el-button type="primary" @click="handleSearch">查询</el-button>
                 <el-button @click="handleReset">重置</el-button>
             </div>
@@ -11,144 +11,144 @@
         </div>
 
         <common-table ref="tableRef" :pageConfig="pageConfig" :columns="columns" :tools="false">
-            
+            <!-- 封面 -->
+            <template #imgUrl="{ row }">
+                <el-image :src="row.imgUrl" class="w-10 h-10 object-cover" :preview-src-list="[row.imgUrl]"
+                    preview-teleported fit="cover" />
+            </template>
+
             <!-- 状态 -->
-            <template #status="{ row }">
-                 <span :class="row.status === 'online' ? 'text-green-500' : 'text-gray-500'">
-                     {{ row.status === 'online' ? '已上架' : '已下架' }}
-                 </span>
+            <template #showStatus="{ row }">
+                <span :class="row.showStatus === 1 ? 'text-green-500' : 'text-gray-500'">
+                    {{ row.showStatus === 1 ? '已上架' : '已下架' }}
+                </span>
             </template>
 
-             <!-- 操作 -->
-             <template #action="{ row }">
-                 <div class="flex items-center space-x-2">
-                    <el-button 
-                        v-if="row.status === 'online'" 
-                        size="small" 
-                        type="primary" 
-                        plain
-                        @click="handleStatusToggle(row)">
+            <!-- 操作 -->
+            <template #action="{ row }">
+                <div class="flex items-center space-x-2">
+                    <el-button v-if="row.showStatus === 1" link type="danger" @click="handleStatusToggle(row)">
                         下架
                     </el-button>
-                    <el-button 
-                        v-else 
-                        size="small" 
-                        type="success" 
-                        plain
-                        @click="handleStatusToggle(row)">
+                    <el-button v-else link type="success" plain @click="handleStatusToggle(row)">
                         上架
                     </el-button>
-                    
+
                     <el-button link type="primary" @click="handleEdit(row)">编辑</el-button>
-                    <el-button link type="primary" @click="handleDetail(row)">详情</el-button>
-                    <el-button link type="danger" @click="handleDelete(row)">删除</el-button>
-                 </div>
+                    <el-button link type="primary" @click="handleBind(row)">绑定图书</el-button>
+                    <!-- <el-button link type="danger" @click="handleDelete(row)">删除</el-button> -->
+                </div>
             </template>
         </common-table>
 
         <booklist-edit ref="editDialogRef" @success="handleSuccess" />
+        <booklist-bind ref="bindDialogRef" @success="handleSuccess" />
     </ele-page>
 </template>
 
 <script setup>
-import { ref, reactive } from 'vue';
-import CommonTable from '@/components/CommonPage/CommonTable.vue';
-import BooklistEdit from './components/booklist-edit.vue';
-import { EleMessage } from 'ele-admin-plus/es';
-
-defineOptions({ name: 'Booklist' });
-
-const tableRef = ref(null);
-const editDialogRef = ref(null);
-const searchQuery = ref('');
-
-const pageConfig = reactive({
-    pageUrl: '/salesOps/booklist/list',
-    fileName: '书单列表',
-    cacheKey: 'booklist-list',
-    params: {}
-});
-
-const columns = ref([
-    { type: 'selection', width: 50, align: 'center' },
-    { label: '编号', prop: 'code', width: 100, align: 'center' },
-    { label: '书单名称', prop: 'name', align: 'center' },
-    { label: '发布时间', prop: 'publishTime', align: 'center', width: 180 },
-    { label: '书单类型', prop: 'typeLabel', align: 'center' },
-    { label: '相关单品', prop: 'relatedItems', align: 'center' },
-    { label: '状态', prop: 'status', slot: 'status', align: 'center' },
-    { label: '排序', prop: 'sort', align: 'center', width: 80 },
-    { label: '操作', prop: 'action', slot: 'action', align: 'center', width: 250 }
-]);
-
-// Mock Data
-const mockList = ref([
-    { id: 1, code: '0101', name: '诺贝尔文学奖', publishTime: '2021-06-26 12:00:00', type: 'single', typeLabel: '单排列', relatedItems: 10, status: 'online', sort: 1 },
-    { id: 2, code: '100', name: '茅盾文学奖', publishTime: '2021-06-26 12:00:00', type: 'double', typeLabel: '双排列', relatedItems: 10, status: 'offline', sort: 1 },
-    { id: 3, code: '10252', name: '樊登读书', publishTime: '2021-06-26 12:00:00', type: 'double', typeLabel: '双排列', relatedItems: 10, status: 'online', sort: 1 },
-    { id: 4, code: '0101', name: '书嗨推荐', publishTime: '2021-06-26 12:00:00', type: 'single', typeLabel: '单排列', relatedItems: 10, status: 'online', sort: 1 },
-]);
-
-const mockDatasource = ({ pages }) => {
-    return new Promise((resolve) => {
-        setTimeout(() => {
-            let data = mockList.value;
-            if (searchQuery.value) {
-                data = data.filter(item => item.name.includes(searchQuery.value));
-            }
-            resolve({
-                code: 0,
-                msg: 'success',
-                count: data.length,
-                data: data
-            });
-        }, 300);
+    import { ref, reactive } from 'vue';
+    import CommonTable from '@/components/CommonPage/CommonTable.vue';
+    import BooklistEdit from './components/booklist-edit.vue';
+    import BooklistBind from './components/booklist-bind.vue';
+    import { EleMessage } from 'ele-admin-plus/es';
+
+    defineOptions({ name: 'Booklist' });
+
+    const tableRef = ref(null);
+    const editDialogRef = ref(null);
+    const bindDialogRef = ref(null);
+    const searchForm = reactive({
+        name: ''
     });
-};
-
-const handleSearch = () => {
-    reload();
-};
-
-const handleReset = () => {
-    searchQuery.value = '';
-    reload();
-};
-
-const reload = () => {
-    tableRef.value?.reload();
-};
-
-const handleAdd = () => {
-    editDialogRef.value?.handleOpen();
-};
-
-const handleEdit = (row) => {
-    editDialogRef.value?.handleOpen(row);
-};
-
-const handleDetail = (row) => {
-    EleMessage.info(`查看 ${row.name} 详情`);
-};
-
-const handleDelete = (row) => {
-    EleMessage.confirm(`确定要删除 ${row.name} 吗?`)
-        .then(() => {
-            // TODO: Call API to delete
-            // tableRef.value?.operatBatch({ method: 'delete', row, url: '/salesOps/booklist/delete' });
-            EleMessage.success('删除成功');
-            reload();
-        })
-        .catch(() => {});
-};
-
-const handleStatusToggle = (row) => {
-    // TODO: Call API to update status
-    // tableRef.value?.operatBatch({ method: 'post', row, url: '/salesOps/booklist/updateStatus' });
-    EleMessage.success(`${row.name} 已${row.status === 'online' ? '上架' : '下架'}`);
-};
-
-const handleSuccess = () => {
-    reload();
-};
+
+    const pageConfig = reactive({
+        pageUrl: '/book/showIndex/list',
+        fileName: '书单列表',
+        cacheKey: 'booklist-list',
+        params: {
+            cateType: 2  //书单
+        }
+    });
+
+    const columns = ref([
+        { type: 'selection', width: 50, align: 'center' },
+        { label: '序号', type: 'index', width: 80, align: 'center' },
+        { label: '书单名称', prop: 'showName', align: 'center', minWidth: 150 },
+        { label: '封面', prop: 'imgUrl', slot: 'imgUrl', align: 'center', width: 120 },
+        { label: '相关单品', prop: 'bookCount', align: 'center', width: 100, formatter: (row) => row.bookCount || 0 },
+        { label: '备注', prop: 'remark', align: 'center', minWidth: 150, showOverflowTooltip: true },
+        { label: '状态', prop: 'showStatus', slot: 'showStatus', align: 'center', width: 100 },
+        { label: '创建时间', prop: 'createTime', align: 'center', width: 180 },
+        { label: '操作', prop: 'action', slot: 'action', align: 'center', width: 200 }
+    ]);
+
+    const handleSearch = () => {
+        tableRef.value?.reload(searchForm);
+    };
+
+    const handleReset = () => {
+        searchForm.name = '';
+        handleSearch();
+    };
+
+    const reload = () => {
+        tableRef.value?.reload();
+    };
+
+    const handleAdd = () => {
+        editDialogRef.value?.handleOpen();
+    };
+
+    const handleEdit = (row) => {
+        editDialogRef.value?.handleOpen(row);
+    };
+
+    const handleBind = (row) => {
+        bindDialogRef.value?.handleOpen(row);
+    };
+
+    const handleDelete = (row) => {
+        EleMessage.confirm(`确定要删除 ${row.showName} 吗?`)
+            .then(() => {
+                // TODO: Delete API if available
+                EleMessage.warning('暂无删除接口');
+            })
+            .catch(() => { });
+    };
+
+    const handleStatusToggle = (row) => {
+        const newStatus = row.showStatus === 1 ? 2 : 1;
+        const actionText = newStatus === 1 ? '上架' : '下架';
+
+        // Use table's operatBatch or direct request if easier.
+        // operatBatch expects a selection or row.
+        // Let's use operatBatch for consistency if it supports custom body.
+        // Usually operatBatch sends IDs. 
+        // The API requires { id, showStatus }.
+        // CommonTable's operatBatch might be limited.
+        // Let's assume we can use a direct request or configure operatBatch carefully.
+
+        // Actually, let's use a custom request here or use operatBatch with custom data.
+        // Since I don't see `request` imported, I'll use tableRef's method if possible, 
+        // or rely on `operatBatch` which usually does a POST.
+
+        tableRef.value?.operatBatch({
+            url: '/book/showIndex/updateShowStatus',
+            method: 'post',
+            data: {
+                id: row.id,
+                showStatus: newStatus
+            },
+            title: `确定要${actionText}吗?`,
+            success: () => {
+                EleMessage.success(`${actionText}成功`);
+                reload();
+            }
+        });
+    };
+
+    const handleSuccess = () => {
+        reload();
+    };
 </script>

+ 97 - 179
src/views/salesOps/components/GoodsSelectDialog.vue

@@ -2,10 +2,10 @@
     <ele-modal :width="width" v-model="visible" :title="title" position="center"
         :body-style="{ padding: '0 20px 20px' }">
         <!-- Search -->
-        <div class="p-0">
-            <el-form :inline="true" :model="searchForm">
+        <div class="p-0 flex justify-between items-start">
+            <el-form :inline="true" :model="searchForm" label-width="0px">
                 <el-form-item>
-                    <el-input v-model="searchForm.keyword" placeholder="请输入商品名称" clearable />
+                    <el-input v-model="searchForm.bookName" placeholder="请输入商品名称" clearable />
                 </el-form-item>
                 <el-form-item>
                     <el-input v-model="searchForm.isbn" placeholder="请输入ISBN" clearable />
@@ -15,21 +15,16 @@
                     <el-button @click="handleReset">重置</el-button>
                 </el-form-item>
             </el-form>
+            <div class="pt-1">
+                <span class="text-gray-600">已选择 <span class="text-primary font-bold">{{ selectedCount }}</span> 项</span>
+            </div>
         </div>
 
         <!-- Table -->
-        <common-table ref="tableRef" :columns="columns" :datasource="datasource" :bodyStyle="{padding: '0'}"
-            :page-config="{ rowKey: 'id', tool: false }" height="400px" @selection-change="handleSelectionChange">
-            <template #toolbar>
-                <div class="flex items-center space-x-4">
-                    <el-tabs v-model="activeTab" @tab-click="handleTabClick">
-                        <el-tab-pane label="全部" name="all"></el-tab-pane>
-                        <el-tab-pane :label="`已选择 (${selectedCount})`" name="selected"></el-tab-pane>
-                    </el-tabs>
-                </div>
-            </template>
-            <template #image="{ row }">
-                <el-image :src="row.image" class="w-10 h-10 object-cover" :preview-src-list="[row.image]"
+        <common-table ref="tableRef" :columns="columns" :page-config="pageConfig" :bodyStyle="{ padding: '0' }"
+            @selection-change="handleSelectionChange" :toolbar="false">
+            <template #cover="{ row }">
+                <el-image :src="row.cover" class="w-10 h-10 object-cover" :preview-src-list="[row.cover]"
                     preview-teleported>
                     <template #error>
                         <div class="w-10 h-10 bg-gray-100 flex items-center justify-center text-gray-400">
@@ -52,169 +47,92 @@
 </template>
 
 <script setup>
-import { ref, reactive, watch, computed } from 'vue';
-import { Picture } from '@element-plus/icons-vue';
-import CommonTable from '@/components/CommonPage/CommonTable.vue';
-
-const props = defineProps({
-    title: { type: String, default: '添加商品' },
-    width: { type: String, default: '900px' },
-    defaultSelected: { type: Array, default: () => [] }
-});
-
-const emit = defineEmits(['update:modelValue', 'confirm']);
-
-const visible = defineModel({ type: Boolean });
-const tableRef = ref(null);
-const activeTab = ref('all');
-const searchForm = reactive({
-    keyword: '',
-    isbn: ''
-});
-
-const currentSelections = ref([]);
-const selectedCount = computed(() => currentSelections.value.length);
-
-// Columns Configuration
-const columns = [
-    { type: 'selection', width: 55, align: 'center', reserveSelection: true },
-    { label: '图示', slot: 'image', width: 80, align: 'center' },
-    { prop: 'isbn', label: 'ISBN', width: 140, align: 'center' },
-    { prop: 'title', label: '书名', minWidth: 150, showOverflowTooltip: true },
-    { prop: 'description', label: '商品描述', minWidth: 200, showOverflowTooltip: true }
-];
-
-// Mock Data (Static for stability)
-const allMockData = Array.from({ length: 100 }).map((_, index) => ({
-    id: 1000 + index, // Stable IDs
-    image: 'https://via.placeholder.com/150',
-    isbn: `9787${Math.floor(Math.random() * 1000000000)}`,
-    title: `马克思主义基本原理 ${index + 1}`,
-    price: (Math.random() * 100).toFixed(2),
-    description: '描述这里是图书描述这里是图书描述这里是图书描述这里是图书描述这里是图书描述这里是图书描述这里是图书描述这里是图书描述这里是图书描述这里是图书描述'
-}));
-
-const datasource = ({ page, limit, where }) => {
-    return new Promise((resolve) => {
-        setTimeout(() => {
-            let list = [];
-            let total = 0;
-
-            if (activeTab.value === 'selected') {
-                // Show only selected items
-                // We rely on currentSelections which should be updated via selection-change
-                // Note: If we switch tabs, we might need to handle this carefully as CommonTable reload might clear selections if we are not careful.
-                // However, usually we want to see what we selected.
-                // Issue: currentSelections is driven by the table. If we show ONLY selected items in the table, the table will only "know" about these.
-                // This might be tricky with reserve-selection if we filter the datasource to only selected items.
-                // A better approach for "Selected" tab might be just filtering the view or using a separate list, but keeping CommonTable generic is good.
-
-                // For now, let's filter allMockData by the IDs we know are selected.
-                // But wait, currentSelections contains the full objects.
-                list = currentSelections.value;
-
-                // Filter by search if needed
-                if (where.keyword) list = list.filter(item => item.title.includes(where.keyword));
-                if (where.isbn) list = list.filter(item => item.isbn.includes(where.isbn));
-
-                total = list.length;
-                // Pagination for selected tab
-                const start = (page - 1) * limit;
-                list = list.slice(start, start + limit);
-
-            } else {
-                // All Data
-                list = allMockData;
-
-                if (where.keyword) list = list.filter(item => item.title.includes(where.keyword));
-                if (where.isbn) list = list.filter(item => item.isbn.includes(where.isbn));
-
-                total = list.length;
-                const start = (page - 1) * limit;
-                list = list.slice(start, start + limit);
-            }
-
-            resolve({
-                list,
-                count: total
+    import { ref, reactive, watch, computed, nextTick } from 'vue';
+    import { Picture } from '@element-plus/icons-vue';
+    import CommonTable from '@/components/CommonPage/CommonTable.vue';
+
+    const props = defineProps({
+        title: { type: String, default: '添加商品' },
+        width: { type: String, default: '900px' },
+        defaultSelected: { type: Array, default: () => [] }
+    });
+
+    const emit = defineEmits(['update:modelValue', 'confirm']);
+
+    const visible = defineModel({ type: Boolean });
+    const tableRef = ref(null);
+    const searchForm = reactive({
+        bookName: '',
+        isbn: ''
+    });
+
+    const currentSelections = ref([]);
+    const selectedCount = computed(() => currentSelections.value.length);
+
+    // Columns Configuration
+    const columns = [
+        { type: 'selection', width: 55, align: 'center', reserveSelection: true },
+        { label: '封面', slot: 'cover', prop: 'cover', width: 80, align: 'center' },
+        { prop: 'isbn', label: 'ISBN', width: 140, align: 'center' },
+        { prop: 'bookName', label: '书名', minWidth: 200, showOverflowTooltip: true },
+        { prop: 'author', label: '作者', width: 120, showOverflowTooltip: true },
+        { prop: 'price', label: '价格', width: 100, align: 'center' }
+    ];
+
+    const pageConfig = reactive({
+        pageUrl: '/book/bookInfo/list',
+        rowKey: 'isbn',
+        tool: false,
+        params: {} // Initial params
+    });
+
+    const handleSearch = () => {
+        tableRef.value?.reload({ ...searchForm });
+    };
+
+    const handleReset = () => {
+        searchForm.bookName = '';
+        searchForm.isbn = '';
+        handleSearch();
+    };
+
+    const reset = () => {
+        searchForm.bookName = '';
+        searchForm.isbn = '';
+        currentSelections.value = [];
+        if (tableRef.value) {
+            tableRef.value.clearSelection();
+            tableRef.value.reload({ pageNum: 1 });
+        }
+    };
+
+    const handleSelectionChange = (rows) => {
+        currentSelections.value = rows;
+    };
+
+    const handleCancel = () => {
+        visible.value = false;
+    };
+
+    const handleConfirm = () => {
+        const selections = tableRef.value?.getSelections() || currentSelections.value;
+        emit('confirm', selections);
+        visible.value = false;
+    };
+
+    watch(visible, (val) => {
+        if (val) {
+            nextTick(() => {
+                handleSearch();
+                if (props.defaultSelected && props.defaultSelected.length > 0) {
+                    if (tableRef.value?.setSelectedRows) {
+                        tableRef.value.setSelectedRows(props.defaultSelected);
+                    }
+                }
             });
-        }, 300);
+        }
+    });
+    defineExpose({
+        reset
     });
-};
-
-const handleSearch = () => {
-    tableRef.value?.reload({ ...searchForm });
-};
-
-const handleReset = () => {
-    searchForm.keyword = '';
-    searchForm.isbn = '';
-    handleSearch();
-};
-
-const handleTabClick = () => {
-    // When switching tabs, we reload the table.
-    // The datasource function will check activeTab.value.
-    tableRef.value?.reload();
-};
-
-const handleSelectionChange = (rows) => {
-    currentSelections.value = rows;
-};
-
-const handleCancel = () => {
-    visible.value = false;
-};
-
-const handleConfirm = () => {
-    // Get selections from table
-    const selections = tableRef.value?.getSelections() || currentSelections.value;
-    emit('confirm', selections);
-    visible.value = false;
-};
-
-watch(visible, (val) => {
-    if (val) {
-        // Initialize with default selected
-        // We need to set these in the table. 
-        // CommonTable/EleProTable usually allows setting selections via method or prop.
-        // EleProTable has `setSelectedRow(rows)` or `toggleRowSelection`.
-        // CommonTable exposes `tableRef` which is EleProTable.
-        // But EleProTable documentation (or typical usage) is needed.
-        // Standard Element Plus table: toggleRowSelection(row, selected).
-        // We need to match rows by ID.
-
-        // Strategy: Wait for data load, then toggle.
-        // Or if we can set initial selections.
-
-        // For this mock, we can update currentSelections directly, but the table UI needs to reflect it.
-        // If we want the table to show them as selected, we need to call toggleRowSelection on the specific rows in the table data.
-
-        // Simpler approach for now:
-        // We update currentSelections from props.defaultSelected.
-        // But visually syncing with the table requires accessing the table instance after data load.
-        currentSelections.value = [...props.defaultSelected];
-
-        // Trigger reload to refresh data
-        nextTick(() => {
-            handleSearch();
-            // Note: Visual selection sync is hard without knowing exactly when data renders.
-            // Ideally CommonTable or EleProTable supports `defaultSelections` or similar.
-            // If not, we might rely on the user re-selecting or assume `reserve-selection` handles it if we could pre-seed it.
-            // Given the complexity of "reserve-selection" with "mock data" and "default selections", 
-            // I will try to rely on the fact that if we pass `selections` v-model to CommonTable it might work?
-            // CommonTable has `v-model:selections="selections"`. It doesn't accept `selections` as prop from parent to control it, only internal.
-
-            // Workaround: We can't easily preset selection in CommonTable without modifying it or using exposed methods.
-            // Let's assume for now the user selects manually or we skip presetting for this iteration unless critical.
-            // But `defaultSelected` suggests we need it.
-            // `tableRef.value` exposes `tableRef` (which is `ele-pro-table`). `ele-pro-table` wraps `el-table`.
-            // So `tableRef.value.tableRef.toggleRowSelection(...)`.
-        });
-    }
-});
-</script>
-
-<style scoped>
-/* No specific styles needed as we use Tailwind and CommonTable */
-</style>
+</script>

+ 124 - 0
src/views/salesOps/components/ShowListSelectDialog.vue

@@ -0,0 +1,124 @@
+<template>
+    <ele-modal :width="width" v-model="visible" :title="title" position="center"
+        :body-style="{ padding: '0 20px 20px' }">
+        <!-- Search -->
+        <div class="p-0 flex justify-between items-start">
+            <el-form :inline="true" :model="searchForm" label-width="0px">
+                <el-form-item>
+                    <el-input v-model="searchForm.showName" placeholder="请输入名称" clearable />
+                </el-form-item>
+                <el-form-item>
+                    <el-button type="primary" @click="handleSearch">查询</el-button>
+                    <el-button @click="handleReset">重置</el-button>
+                </el-form-item>
+            </el-form>
+        </div>
+
+        <!-- Table -->
+        <common-table ref="tableRef" :columns="columns" :page-config="pageConfig" :bodyStyle="{ padding: '0' }"
+            @selection-change="handleSelectionChange" :toolbar="false">
+            <template #imgUrl="{ row }">
+                <el-image :src="row.imgUrl" class="w-10 h-10 object-cover" :preview-src-list="[row.imgUrl]"
+                    preview-teleported fit="cover">
+                    <template #error>
+                        <div class="w-10 h-10 bg-gray-100 flex items-center justify-center text-gray-400">
+                            <el-icon>
+                                <Picture />
+                            </el-icon>
+                        </div>
+                    </template>
+                </el-image>
+            </template>
+            <template #showStatus="{ row }">
+                <span :class="row.showStatus === 1 ? 'text-green-500' : 'text-gray-500'">
+                    {{ row.showStatus === 1 ? '已上架' : '已下架' }}
+                </span>
+            </template>
+        </common-table>
+
+        <template #footer>
+            <div class="flex justify-end items-center">
+                <el-button @click="handleCancel">取消</el-button>
+                <el-button type="primary" @click="handleConfirm">确定</el-button>
+            </div>
+        </template>
+    </ele-modal>
+</template>
+
+<script setup>
+import { ref, reactive, watch, nextTick } from 'vue';
+import { Picture } from '@element-plus/icons-vue';
+import CommonTable from '@/components/CommonPage/CommonTable.vue';
+
+const props = defineProps({
+    title: { type: String, default: '选择书单' },
+    width: { type: String, default: '900px' },
+    cateType: { type: Number, default: 2 }, // 2: Booklist, 1: Topic
+    defaultSelected: { type: Array, default: () => [] }
+});
+
+const emit = defineEmits(['update:modelValue', 'confirm']);
+
+const visible = defineModel({ type: Boolean });
+const tableRef = ref(null);
+const searchForm = reactive({
+    showName: ''
+});
+
+const currentSelections = ref([]);
+
+// Columns Configuration
+const columns = [
+    { type: 'selection', width: 55, align: 'center', reserveSelection: true },
+    { label: '封面', slot: 'imgUrl', prop: 'imgUrl', width: 80, align: 'center' },
+    { prop: 'showName', label: '名称', minWidth: 200, showOverflowTooltip: true },
+    { label: '状态', prop: 'showStatus', slot: 'showStatus', align: 'center', width: 100 },
+    { prop: 'createTime', label: '创建时间', width: 180, align: 'center' }
+];
+
+const pageConfig = reactive({
+    pageUrl: '/book/showIndex/list',
+    rowKey: 'id',
+    tool: false,
+    params: {
+        cateType: props.cateType
+    }
+});
+
+const handleSearch = () => {
+    tableRef.value?.reload({ ...searchForm });
+};
+
+const handleReset = () => {
+    searchForm.showName = '';
+    handleSearch();
+};
+
+const handleSelectionChange = (rows) => {
+    currentSelections.value = rows;
+};
+
+const handleCancel = () => {
+    visible.value = false;
+};
+
+const handleConfirm = () => {
+    const selections = tableRef.value?.getSelections() || currentSelections.value;
+    emit('confirm', selections);
+    visible.value = false;
+};
+
+watch(visible, (val) => {
+    if (val) {
+        nextTick(() => {
+            handleSearch();
+            // Note: CommonTable might need setSelectedRows to be exposed or implemented
+            if (props.defaultSelected && props.defaultSelected.length > 0) {
+                if (tableRef.value?.setSelectedRows) {
+                    tableRef.value.setSelectedRows(props.defaultSelected);
+                }
+            }
+        });
+    }
+});
+</script>

+ 95 - 13
src/views/salesOps/decoration/components/CarouselEdit.vue

@@ -1,5 +1,5 @@
 <template>
-    <el-dialog v-model="visible" :title="title" width="600px" :close-on-click-modal="false">
+    <el-dialog v-model="visible" :title="title" width="600px" :close-on-click-modal="false" @open="handleOpen">
         <div class="carousel-edit">
             <el-alert v-if="type === 'carousel'" title="建议尺寸 300x150, 最多添加 5 张" type="info" show-icon :closable="false"
                 class="mb-4" />
@@ -8,12 +8,12 @@
             <div v-for="(item, index) in items" :key="index"
                 class="flex items-start gap-4 mb-4 p-3 border rounded bg-gray-50 relative group">
                 <div class="w-32 h-32 shrink-0">
-                    <ImageUpload v-model="item.image" :limit="1" :isShowTip="false" />
+                    <ImageUpload v-model="item.imgUrl" :limit="1" :isShowTip="false" />
                 </div>
 
                 <div class="flex-1 space-y-2">
                     <el-input v-if="type === 'carousel'" v-model="item.title" placeholder="图片标题 (可选)" />
-                    <el-input v-model="item.link" placeholder="跳转链接" />
+                    <el-input v-model="item.jumpUrl" placeholder="跳转链接" />
                     <div class="text-xs text-gray-400">
                         支持 http/https 链接或内部路由路径
                     </div>
@@ -38,7 +38,7 @@
         <template #footer>
             <span class="dialog-footer">
                 <el-button @click="visible = false">取消</el-button>
-                <el-button type="primary" @click="handleConfirm">确定</el-button>
+                <el-button type="primary" :loading="loading" @click="handleConfirm">确定</el-button>
             </span>
         </template>
     </el-dialog>
@@ -48,6 +48,8 @@
 import { computed, ref } from 'vue';
 import { Plus, Delete } from '@element-plus/icons-vue';
 import ImageUpload from '@/components/ImageUpload/index.vue';
+import request from '@/utils/request';
+import { EleMessage } from 'ele-admin-plus/es';
 
 const props = defineProps({
     modelValue: {
@@ -65,30 +67,41 @@ const props = defineProps({
     fixedNumber: {
         type: Number,
         default: 0
+    },
+    position: {
+        type: String,
+        default: ''
     }
 });
 
-const emit = defineEmits(['update:modelValue']);
+const emit = defineEmits(['update:modelValue', 'success']);
 
 const visible = computed({
     get: () => props.modelValue,
     set: (val) => emit('update:modelValue', val)
 });
 
+const loading = ref(false);
 const maxItems = computed(() => props.type === 'carousel' ? 5 : 1);
 
 const items = ref([
-    { title: '', image: '', link: '' }
+    { title: '', imgUrl: '', jumpUrl: '' }
 ]);
 
 // Initialize based on fixedNumber
-if (props.fixedNumber > 0) {
-    items.value = Array.from({ length: props.fixedNumber }, () => ({ title: '', image: '', link: '' }));
-}
+const initItems = () => {
+    if (props.fixedNumber > 0) {
+        items.value = Array.from({ length: props.fixedNumber }, () => ({ title: '', imgUrl: '', jumpUrl: '' }));
+    } else {
+        items.value = [{ title: '', imgUrl: '', jumpUrl: '' }];
+    }
+};
+
+initItems();
 
 const handleAdd = () => {
     if (items.value.length < maxItems.value) {
-        items.value.push({ title: '', image: '', link: '' });
+        items.value.push({ title: '', imgUrl: '', jumpUrl: '' });
     }
 };
 
@@ -96,9 +109,78 @@ const handleRemove = (index) => {
     items.value.splice(index, 1);
 };
 
-const handleConfirm = () => {
-    // Save logic here
-    visible.value = false;
+const handleOpen = async () => {
+    if (!props.position) {
+        initItems();
+        return;
+    }
+    
+    try {
+        const res = await request.get('/book/showIndex/getInfoByPosition', {
+            params: { position: props.position }
+        });
+        const list = res.data?.data || [];
+        
+        if (list.length > 0) {
+            // Fill existing data
+            let newItems = list.map(item => ({
+                title: item.title || '',
+                imgUrl: item.imgUrl || '',
+                jumpUrl: item.jumpUrl || '',
+                id: item.id
+            }));
+            
+            // If fixedNumber is set, ensure length matches
+            if (props.fixedNumber > 0) {
+                if (newItems.length < props.fixedNumber) {
+                    const diff = props.fixedNumber - newItems.length;
+                    newItems = [...newItems, ...Array.from({ length: diff }, () => ({ title: '', imgUrl: '', jumpUrl: '' }))];
+                } else if (newItems.length > props.fixedNumber) {
+                    newItems = newItems.slice(0, props.fixedNumber);
+                }
+            }
+            items.value = newItems;
+        } else {
+            initItems();
+        }
+    } catch (e) {
+        console.error(e);
+        initItems();
+    }
+};
+
+const handleConfirm = async () => {
+    if (!props.position) {
+        EleMessage.warning('未配置位置信息');
+        return;
+    }
+
+    loading.value = true;
+    try {
+        const showIndexInfoList = items.value
+            .filter(item => item.imgUrl) // Only save items with images
+            .map((item, index) => ({
+                imgUrl: item.imgUrl,
+                jumpUrl: item.jumpUrl,
+                title: item.title,
+                orderNum: index,
+                showCateId: 0 // Default to 0 as we don't select categories here
+            }));
+
+        await request.post('/book/showIndex/updateIndex', {
+            position: props.position,
+            showIndexInfoList
+        });
+
+        EleMessage.success('保存成功');
+        visible.value = false;
+        emit('success');
+    } catch (e) {
+        console.error(e);
+        EleMessage.error(e.message || '保存失败');
+    } finally {
+        loading.value = false;
+    }
 };
 </script>
 

+ 254 - 45
src/views/salesOps/decoration/components/DiamondEdit.vue

@@ -1,16 +1,25 @@
 <template>
-    <el-dialog v-model="visible" title="金刚区编辑" width="600px" :close-on-click-modal="false">
+    <el-dialog v-model="visible" :title="title" width="600px" :close-on-click-modal="false" @open="handleOpen">
         <div class="diamond-edit">
             <div v-for="(item, index) in items" :key="index"
                 class="flex items-center gap-4 mb-4 p-3 border rounded bg-gray-50 relative group">
-                <ImageUpload v-model="item.image" :limit="1" :isShowTip="false" />
+                <ImageUpload v-model="item.imgUrl" :limit="1" :isShowTip="false" />
 
                 <div class="flex-1">
-                    <el-input v-model="item.title" placeholder="请输入标题 (不超过5个字)" maxlength="5" show-word-limit
-                        class="mb-2" />
-                    <el-input v-model="item.link" placeholder="请输入跳转链接" size="small">
+                    <div class="flex items-center gap-2 mb-2">
+                        <el-select v-model="item.showCateId" filterable remote reserve-keyword placeholder="请搜索并选择书单"
+                            :remote-method="(query) => remoteMethod(query, index)" :loading="item.loading"
+                            @change="(val) => handleSelectChange(val, index)" clearable class="flex-1">
+                            <el-option v-for="opt in options[index] || []" :key="opt.value" :label="opt.label"
+                                :value="opt.value" :disabled="isOptionDisabled(opt.value, index)" />
+                        </el-select>
+                    </div>
+                    <el-input v-model="item.jumpUrl" placeholder="请输入跳转链接">
                         <template #prepend>链接</template>
                     </el-input>
+                    <div v-if="item.showCateId" class="text-xs text-gray-400 mt-1">
+                        已绑定书单ID: {{ item.showCateId }}
+                    </div>
                 </div>
             </div>
         </div>
@@ -18,53 +27,253 @@
         <template #footer>
             <span class="dialog-footer">
                 <el-button @click="visible = false">取消</el-button>
-                <el-button type="primary" @click="handleConfirm">确定</el-button>
+                <el-button type="primary" :loading="loading" @click="handleConfirm">确定</el-button>
             </span>
         </template>
     </el-dialog>
 </template>
 
 <script setup>
-import { computed, ref, watch } from 'vue';
-import { Plus, Delete } from '@element-plus/icons-vue';
-import ImageUpload from '@/components/ImageUpload/index.vue';
-
-const props = defineProps({
-    modelValue: {
-        type: Boolean,
-        default: false
-    }
-});
-
-const emit = defineEmits(['update:modelValue']);
-
-const visible = computed({
-    get: () => props.modelValue,
-    set: (val) => emit('update:modelValue', val)
-});
-
-const items = ref([
-    { title: '', image: '', link: '' },
-    { title: '', image: '', link: '' },
-    { title: '', image: '', link: '' },
-    { title: '', image: '', link: '' },
-    { title: '', image: '', link: '' },
-    { title: '', image: '', link: '' },
-    { title: '', image: '', link: '' },
-    { title: '', image: '', link: '' },
-    { title: '', image: '', link: '' },
-    { title: '', image: '', link: '' }
-]);
-
-const handleConfirm = () => {
-    // Save logic here
-    visible.value = false;
-};
+    import { computed, ref, watch } from 'vue';
+    import { Plus, Delete } from '@element-plus/icons-vue';
+    import ImageUpload from '@/components/ImageUpload/index.vue';
+    import request from '@/utils/request';
+    import { EleMessage } from 'ele-admin-plus/es';
+
+    const props = defineProps({
+        modelValue: {
+            type: Boolean,
+            default: false
+        },
+        title: {
+            type: String,
+            default: '金刚区编辑'
+        },
+        position: {
+            type: String,
+            default: 'diamond_area'
+        },
+        limit: {
+            type: Number,
+            default: 10
+        }
+    });
+
+    const emit = defineEmits(['update:modelValue', 'success']);
+
+    const visible = computed({
+        get: () => props.modelValue,
+        set: (val) => emit('update:modelValue', val)
+    });
+
+    const loading = ref(false);
+    const selectDialogVisible = ref(false);
+    const currentSelectIndex = ref(-1);
+
+    const options = ref(Array(props.limit).fill([]));
+    const defaultOptions = ref([]);
+
+    const items = ref(Array(props.limit).fill(null).map(() => ({
+        title: '',
+        imgUrl: '',
+        jumpUrl: '',
+        showCateId: undefined,
+        loading: false
+    })));
+
+    // 计算选项是否应被禁用
+    const isOptionDisabled = (value, currentIndex) => {
+        // 检查该值是否已被其他项选中
+        return items.value.some((item, index) => {
+            // 跳过当前正在编辑的项
+            if (index === currentIndex) return false;
+            // 如果其他项选中了该值,则禁用
+            return item.showCateId === value;
+        });
+    };
+
+    // 获取默认书单选项(前10条)
+    const fetchDefaultOptions = async () => {
+        try {
+            const res = await request.get('/book/showIndex/list', {
+                params: {
+                    cateType: 2, // 书单
+                    pageNum: 1,
+                    pageSize: 10
+                }
+            });
+            const list = res.data?.rows || res.data || [];
+            defaultOptions.value = list.map(item => ({
+                value: item.id,
+                label: item.showName,
+                raw: item
+            }));
+        } catch (e) {
+            console.error('Failed to fetch default options', e);
+            defaultOptions.value = [];
+        }
+    };
+
+    // 远程搜索书单
+    const remoteMethod = async (query, index) => {
+        if (query) {
+            items.value[index].loading = true;
+            try {
+                const res = await request.get('/book/showIndex/list', {
+                    params: {
+                        showName: query,
+                        cateType: 2, // 书单
+                        pageNum: 1,
+                        pageSize: 20
+                    }
+                });
+                const list = res.data?.rows || res.data || [];
+                options.value[index] = list.map(item => ({
+                    value: item.id,
+                    label: item.showName,
+                    raw: item
+                }));
+            } catch (e) {
+                console.error(e);
+                options.value[index] = [];
+            } finally {
+                items.value[index].loading = false;
+            }
+        } else {
+            // 恢复默认选项,但保留当前选中的项(如果不在默认列表中)
+            const currentSelectedId = items.value[index].showCateId;
+            let currentOption = null;
+
+            // 尝试在当前选项或默认选项中找到当前选中的项
+            if (currentSelectedId) {
+                currentOption = options.value[index].find(opt => opt.value === currentSelectedId) ||
+                    defaultOptions.value.find(opt => opt.value === currentSelectedId);
+
+                // 如果都没找到,检查是否有手动注入的数据(回显时)
+                if (!currentOption && items.value[index].title) {
+                    currentOption = {
+                        value: currentSelectedId,
+                        label: items.value[index].title,
+                        raw: { imgUrl: items.value[index].imgUrl, showName: items.value[index].title }
+                    };
+                }
+            }
+
+            const merged = [...defaultOptions.value];
+            if (currentOption && !merged.some(opt => opt.value === currentOption.value)) {
+                merged.unshift(currentOption);
+            }
+            options.value[index] = merged;
+        }
+    };
+
+    // 处理下拉框选择变化
+    const handleSelectChange = (val, index) => {
+        if (!val) {
+            // 清空选择
+            items.value[index].showCateId = undefined;
+            return;
+        }
+        const selectedOption = options.value[index].find(opt => opt.value === val);
+        if (selectedOption) {
+            const raw = selectedOption.raw;
+            items.value[index].imgUrl = raw.imgUrl;
+            items.value[index].title = raw.showName;
+            items.value[index].showCateId = raw.id;
+            items.value[index].jumpUrl = `pages-sell/pages/recommend?id=${raw.id}`;
+        }
+    };
+
+    // 打开弹窗初始化
+    const handleOpen = async () => {
+        // 先获取默认选项
+        await fetchDefaultOptions();
+
+        try {
+            const res = await request.get('/book/showIndex/getInfoByPosition', {
+                params: { position: props.position }
+            });
+            const list = res.data?.data || [];
+            console.log('list', list);
+            // 填充数据,保持结构一致
+            items.value = Array(props.limit).fill(null).map((_, i) => {
+                const remote = list[i] || {};
+                return {
+                    title: remote.title || '',
+                    imgUrl: remote.imgUrl || '',
+                    jumpUrl: remote.jumpUrl || '',
+                    showCateId: remote.showCateId,
+                    loading: false
+                };
+            });
+
+            // 初始化每个项目的选项(默认选项 + 当前选中项)
+            items.value.forEach((item, index) => {
+                const merged = [...defaultOptions.value];
+
+                if (item.showCateId) {
+                    // 检查当前选中项是否已在默认选项中
+                    const exists = merged.some(opt => opt.value === item.showCateId);
+                    if (!exists && item.title) {
+                        // 如果不在,手动注入当前项以确保回显正确
+                        merged.unshift({
+                            value: item.showCateId,
+                            label: item.title,
+                            raw: { imgUrl: item.imgUrl, showName: item.title }
+                        });
+                    }
+                }
+                options.value[index] = merged;
+            });
+        } catch (e) {
+            console.error(e);
+            // 出错或为空时重置
+            items.value = Array(props.limit).fill(null).map(() => ({
+                title: '',
+                imgUrl: '',
+                jumpUrl: '',
+                showCateId: undefined,
+                loading: false
+            }));
+            options.value = Array(props.limit).fill(defaultOptions.value);
+        }
+    };
+
+    // 保存配置
+    const handleConfirm = async () => {
+        loading.value = true;
+        try {
+            const showIndexInfoList = items.value
+                .filter(item => item.imgUrl) // 仅保存有图片的项
+                .map((item, index) => ({
+                    imgUrl: item.imgUrl,
+                    jumpUrl: item.jumpUrl,
+                    showCateId: item.showCateId || 0,
+                    orderNum: index,
+                    // 添加标题,以便后端支持时保存,或用于前端回显
+                    title: item.title
+                }));
+
+            await request.post('/book/showIndex/updateIndex', {
+                position: props.position,
+                showIndexInfoList
+            });
+
+            EleMessage.success('保存成功');
+            visible.value = false;
+            emit('success');
+        } catch (e) {
+            console.error(e);
+            EleMessage.error(e.message || '保存失败');
+        } finally {
+            loading.value = false;
+        }
+    };
 </script>
 
 <style scoped>
-.diamond-edit {
-    max-height: 60vh;
-    overflow-y: auto;
-}
+    .diamond-edit {
+        max-height: 60vh;
+        overflow-y: auto;
+    }
 </style>

+ 92 - 28
src/views/salesOps/decoration/components/HotSalesEdit.vue

@@ -1,24 +1,23 @@
 <template>
-    <el-dialog
-        v-model="visible"
-        title="热销商品编辑"
-        width="600px"
-        :close-on-click-modal="false"
-    >
+    <el-dialog v-model="visible" title="热销商品编辑" width="600px" :close-on-click-modal="false" @open="handleOpen">
         <div class="hot-sales-edit">
             <el-alert title="请选择 2 个热销商品" type="info" show-icon :closable="false" class="mb-4" />
-            
-            <div v-for="(item, index) in items" :key="index" class="flex items-center gap-4 mb-4 p-3 border rounded bg-gray-50 relative group cursor-pointer hover:border-blue-500 transition-colors" @click="handleOpenSelect(index)">
+
+            <div v-for="(item, index) in items" :key="index"
+                class="flex items-center gap-4 mb-4 p-3 border rounded bg-gray-50 relative group cursor-pointer hover:border-blue-500 transition-colors"
+                @click="handleOpenSelect(index)">
                 <div class="w-16 h-16 bg-gray-200 rounded flex items-center justify-center shrink-0">
-                    <el-icon v-if="!item.id" :size="20" class="text-gray-400"><Plus /></el-icon>
-                    <img v-else :src="item.image" class="w-full h-full object-cover rounded" />
+                    <el-icon v-if="!item.imgUrl" :size="20" class="text-gray-400">
+                        <Plus />
+                    </el-icon>
+                    <img v-else :src="item.imgUrl" class="w-full h-full object-cover rounded" />
                 </div>
-                
+
                 <div class="flex-1">
-                    <div v-if="!item.id" class="text-gray-400">点击选择商品</div>
+                    <div v-if="!item.title" class="text-gray-400">点击选择商品</div>
                     <div v-else>
                         <div class="font-medium truncate">{{ item.title }}</div>
-                        <div class="text-red-500 mt-1">¥{{ item.price }}</div>
+                        <div v-if="item.price" class="text-red-500 mt-1">¥{{ item.price }}</div>
                     </div>
                 </div>
 
@@ -31,14 +30,11 @@
         <template #footer>
             <span class="dialog-footer">
                 <el-button @click="visible = false">取消</el-button>
-                <el-button type="primary" @click="handleConfirm">确定</el-button>
+                <el-button type="primary" :loading="loading" @click="handleConfirm">确定</el-button>
             </span>
         </template>
 
-        <GoodsSelectDialog
-            v-model="selectVisible"
-            @confirm="handleSelectConfirm"
-        />
+        <GoodsSelectDialog ref="goodsSelectRef" v-model="selectVisible" @confirm="handleSelectConfirm" />
     </el-dialog>
 </template>
 
@@ -46,15 +42,21 @@
 import { computed, ref } from 'vue';
 import { Plus } from '@element-plus/icons-vue';
 import GoodsSelectDialog from '../../components/GoodsSelectDialog.vue';
+import request from '@/utils/request';
+import { EleMessage } from 'ele-admin-plus/es';
 
 const props = defineProps({
     modelValue: {
         type: Boolean,
         default: false
+    },
+    position: {
+        type: String,
+        default: 'right_banner'
     }
 });
 
-const emit = defineEmits(['update:modelValue']);
+const emit = defineEmits(['update:modelValue', 'success']);
 
 const visible = computed({
     get: () => props.modelValue,
@@ -63,12 +65,49 @@ const visible = computed({
 
 const selectVisible = ref(false);
 const activeIndex = ref(0);
+const loading = ref(false);
+const goodsSelectRef = ref(null);
+
 // Fixed 2 items
 const items = ref([
-    { id: '', title: '', image: '', price: '' },
-    { id: '', title: '', image: '', price: '' }
+    { id: '', title: '', imgUrl: '', price: '', jumpUrl: '' },
+    { id: '', title: '', imgUrl: '', price: '', jumpUrl: '' }
 ]);
 
+const handleOpen = async () => {
+    // Reset selection in dialog if needed
+    if (goodsSelectRef.value?.reset) {
+        goodsSelectRef.value.reset();
+    }
+    
+    try {
+        const res = await request.get('/book/showIndex/getInfoByPosition', {
+            params: { position: props.position }
+        });
+        const list = res.data?.data || [];
+        
+        if (list.length > 0) {
+            items.value = Array(2).fill(null).map((_, i) => {
+                const remote = list[i] || {};
+                return {
+                    id: remote.showCateId || '', // Using showCateId to store product ID
+                    title: remote.title || '',
+                    imgUrl: remote.imgUrl || '',
+                    jumpUrl: remote.jumpUrl || '',
+                    price: '' // Backend doesn't return price, leave empty
+                };
+            });
+        } else {
+             items.value = [
+                { id: '', title: '', imgUrl: '', price: '', jumpUrl: '' },
+                { id: '', title: '', imgUrl: '', price: '', jumpUrl: '' }
+            ];
+        }
+    } catch (e) {
+        console.error(e);
+    }
+};
+
 const handleOpenSelect = (index) => {
     activeIndex.value = index;
     selectVisible.value = true;
@@ -79,18 +118,43 @@ const handleSelectConfirm = (selected) => {
         // Only take the first selected item for the active slot
         const product = selected[0];
         items.value[activeIndex.value] = {
-            id: product.id,
-            title: product.title,
-            image: product.image,
-            price: product.price
+            id: product.id, // Assuming product object has id
+            title: product.bookName, // GoodsSelectDialog returns bookName
+            imgUrl: product.cover, // GoodsSelectDialog returns cover
+            price: product.price,
+            jumpUrl: `pages-sell/pages/product-detail?id=${product.id}` // Construct jumpUrl
         };
     }
     selectVisible.value = false;
 };
 
-const handleConfirm = () => {
-    // Save logic here
-    visible.value = false;
+const handleConfirm = async () => {
+    loading.value = true;
+    try {
+        const showIndexInfoList = items.value
+            .filter(item => item.imgUrl)
+            .map((item, index) => ({
+                imgUrl: item.imgUrl,
+                jumpUrl: item.jumpUrl,
+                title: item.title,
+                showCateId: item.id || 0, // Store product ID
+                orderNum: index
+            }));
+
+        await request.post('/book/showIndex/updateIndex', {
+            position: props.position,
+            showIndexInfoList
+        });
+
+        EleMessage.success('保存成功');
+        visible.value = false;
+        emit('success');
+    } catch (e) {
+        console.error(e);
+        EleMessage.error(e.message || '保存失败');
+    } finally {
+        loading.value = false;
+    }
 };
 </script>
 

+ 276 - 0
src/views/salesOps/decoration/components/TopicEdit.vue

@@ -0,0 +1,276 @@
+<template>
+    <el-dialog v-model="visible" :title="title" width="600px" :close-on-click-modal="false" @open="handleOpen">
+        <div class="topic-edit">
+            <div v-for="(item, index) in items" :key="index"
+                class="flex items-center gap-4 mb-4 p-3 border rounded bg-gray-50 relative group">
+                <ImageUpload v-model="item.imgUrl" :limit="1" :isShowTip="false" />
+
+                <div class="flex-1">
+                    <div class="flex items-center gap-2 mb-2">
+                        <el-select v-model="item.showCateId" filterable remote reserve-keyword placeholder="请搜索并选择专题"
+                            :remote-method="(query) => remoteMethod(query, index)" :loading="item.loading"
+                            @change="(val) => handleSelectChange(val, index)" clearable class="flex-1">
+                            <el-option v-for="opt in options[index] || []" :key="opt.value" :label="opt.label"
+                                :value="opt.value" :disabled="isOptionDisabled(opt.value, index)" />
+                        </el-select>
+                    </div>
+                    <el-input v-model="item.jumpUrl" placeholder="请输入跳转链接">
+                        <template #prepend>链接</template>
+                    </el-input>
+                    <div v-if="item.showCateId" class="text-xs text-gray-400 mt-1">
+                        已绑定专题ID: {{ item.showCateId }}
+                    </div>
+                </div>
+            </div>
+        </div>
+
+        <template #footer>
+            <span class="dialog-footer">
+                <el-button @click="visible = false">取消</el-button>
+                <el-button type="primary" :loading="loading" @click="handleConfirm">确定</el-button>
+            </span>
+        </template>
+    </el-dialog>
+</template>
+
+<script setup>
+import { computed, ref } from 'vue';
+import ImageUpload from '@/components/ImageUpload/index.vue';
+import request from '@/utils/request';
+import { EleMessage } from 'ele-admin-plus/es';
+
+const props = defineProps({
+    modelValue: {
+        type: Boolean,
+        default: false
+    },
+    title: {
+        type: String,
+        default: '专题编辑'
+    },
+    position: {
+        type: String,
+        default: 'double_row_topic'
+    },
+    limit: {
+        type: Number,
+        default: 10
+    }
+});
+
+const emit = defineEmits(['update:modelValue', 'success']);
+
+const visible = computed({
+    get: () => props.modelValue,
+    set: (val) => emit('update:modelValue', val)
+});
+
+const loading = ref(false);
+
+const options = ref(Array(props.limit).fill([]));
+const defaultOptions = ref([]);
+
+const items = ref(Array(props.limit).fill(null).map(() => ({
+    title: '',
+    imgUrl: '',
+    jumpUrl: '',
+    showCateId: undefined,
+    loading: false
+})));
+
+// 计算选项是否应被禁用
+const isOptionDisabled = (value, currentIndex) => {
+    // 检查该值是否已被其他项选中
+    return items.value.some((item, index) => {
+        // 跳过当前正在编辑的项
+        if (index === currentIndex) return false;
+        // 如果其他项选中了该值,则禁用
+        return item.showCateId === value;
+    });
+};
+
+// 获取默认专题选项(前10条)
+const fetchDefaultOptions = async () => {
+    try {
+        const res = await request.get('/book/showIndex/list', {
+            params: {
+                cateType: 1, // 专题
+                pageNum: 1,
+                pageSize: 10
+            }
+        });
+        const list = res.data?.rows || res.data || [];
+        defaultOptions.value = list.map(item => ({
+            value: item.id,
+            label: item.showName,
+            raw: item
+        }));
+    } catch (e) {
+        console.error('Failed to fetch default options', e);
+        defaultOptions.value = [];
+    }
+};
+
+// 远程搜索专题
+const remoteMethod = async (query, index) => {
+    if (query) {
+        items.value[index].loading = true;
+        try {
+            const res = await request.get('/book/showIndex/list', {
+                params: {
+                    showName: query,
+                    cateType: 1, // 专题
+                    pageNum: 1,
+                    pageSize: 20
+                }
+            });
+            const list = res.data?.rows || res.data || [];
+            options.value[index] = list.map(item => ({
+                value: item.id,
+                label: item.showName,
+                raw: item
+            }));
+        } catch (e) {
+            console.error(e);
+            options.value[index] = [];
+        } finally {
+            items.value[index].loading = false;
+        }
+    } else {
+        // 恢复默认选项,但保留当前选中的项(如果不在默认列表中)
+        const currentSelectedId = items.value[index].showCateId;
+        let currentOption = null;
+
+        // 尝试在当前选项或默认选项中找到当前选中的项
+        if (currentSelectedId) {
+            currentOption = options.value[index].find(opt => opt.value === currentSelectedId) ||
+                defaultOptions.value.find(opt => opt.value === currentSelectedId);
+
+            // 如果都没找到,检查是否有手动注入的数据(回显时)
+            if (!currentOption && items.value[index].title) {
+                currentOption = {
+                    value: currentSelectedId,
+                    label: items.value[index].title,
+                    raw: { imgUrl: items.value[index].imgUrl, showName: items.value[index].title }
+                };
+            }
+        }
+
+        const merged = [...defaultOptions.value];
+        if (currentOption && !merged.some(opt => opt.value === currentOption.value)) {
+            merged.unshift(currentOption);
+        }
+        options.value[index] = merged;
+    }
+};
+
+// 处理下拉框选择变化
+const handleSelectChange = (val, index) => {
+    if (!val) {
+        // 清空选择
+        items.value[index].showCateId = undefined;
+        return;
+    }
+    const selectedOption = options.value[index].find(opt => opt.value === val);
+    if (selectedOption) {
+        const raw = selectedOption.raw;
+        items.value[index].imgUrl = raw.imgUrl;
+        items.value[index].title = raw.showName;
+        items.value[index].showCateId = raw.id;
+        items.value[index].jumpUrl = `pages-sell/pages/topic?id=${raw.id}`;
+    }
+};
+
+// 打开弹窗初始化
+const handleOpen = async () => {
+    // 先获取默认选项
+    await fetchDefaultOptions();
+
+    try {
+        const res = await request.get('/book/showIndex/getInfoByPosition', {
+            params: { position: props.position }
+        });
+        const list = res.data?.data || [];
+        
+        // 填充数据,保持结构一致
+        items.value = Array(props.limit).fill(null).map((_, i) => {
+            const remote = list[i] || {};
+            return {
+                title: remote.title || '',
+                imgUrl: remote.imgUrl || '',
+                jumpUrl: remote.jumpUrl || '',
+                showCateId: remote.showCateId,
+                loading: false
+            };
+        });
+
+        // 初始化每个项目的选项(默认选项 + 当前选中项)
+        items.value.forEach((item, index) => {
+            const merged = [...defaultOptions.value];
+
+            if (item.showCateId) {
+                // 检查当前选中项是否已在默认选项中
+                const exists = merged.some(opt => opt.value === item.showCateId);
+                if (!exists && item.title) {
+                    // 如果不在,手动注入当前项以确保回显正确
+                    merged.unshift({
+                        value: item.showCateId,
+                        label: item.title,
+                        raw: { imgUrl: item.imgUrl, showName: item.title }
+                    });
+                }
+            }
+            options.value[index] = merged;
+        });
+    } catch (e) {
+        console.error(e);
+        // 出错或为空时重置
+        items.value = Array(props.limit).fill(null).map(() => ({
+            title: '',
+            imgUrl: '',
+            jumpUrl: '',
+            showCateId: undefined,
+            loading: false
+        }));
+        options.value = Array(props.limit).fill(defaultOptions.value);
+    }
+};
+
+// 保存配置
+const handleConfirm = async () => {
+    loading.value = true;
+    try {
+        const showIndexInfoList = items.value
+            .filter(item => item.imgUrl) // 仅保存有图片的项
+            .map((item, index) => ({
+                imgUrl: item.imgUrl,
+                jumpUrl: item.jumpUrl,
+                showCateId: item.showCateId || 0,
+                orderNum: index,
+                // 添加标题,以便后端支持时保存,或用于前端回显
+                title: item.title
+            }));
+
+        await request.post('/book/showIndex/updateIndex', {
+            position: props.position,
+            showIndexInfoList
+        });
+
+        EleMessage.success('保存成功');
+        visible.value = false;
+        emit('success');
+    } catch (e) {
+        console.error(e);
+        EleMessage.error(e.message || '保存失败');
+    } finally {
+        loading.value = false;
+    }
+};
+</script>
+
+<style scoped>
+.topic-edit {
+    max-height: 60vh;
+    overflow-y: auto;
+}
+</style>

+ 281 - 72
src/views/salesOps/decoration/index.vue

@@ -5,7 +5,8 @@
             <!-- 1. Search Bar -->
             <div class="section-item cursor-not-allowed opacity-70">
                 <div class="border rounded p-3 text-center text-gray-400 bg-gray-50 relative">
-                    <div class="absolute top-0 left-0 bg-yellow-100 text-xs px-2 py-1 rounded-tl rounded-br text-black">1</div>
+                    <div class="absolute top-0 left-0 bg-yellow-100 text-xs px-2 py-1 rounded-tl rounded-br text-black">
+                        1</div>
                     搜索框,不可编辑
                 </div>
             </div>
@@ -13,7 +14,8 @@
             <!-- 2. One Book One Experience -->
             <div class="section-item cursor-not-allowed opacity-70">
                 <div class="border rounded p-3 text-center text-gray-500 bg-white relative">
-                    <div class="absolute top-0 left-0 bg-yellow-100 text-xs px-2 py-1 rounded-tl rounded-br text-black">2</div>
+                    <div class="absolute top-0 left-0 bg-yellow-100 text-xs px-2 py-1 rounded-tl rounded-br text-black">
+                        2</div>
                     一书一验 不可编辑
                 </div>
             </div>
@@ -22,7 +24,8 @@
             <div class="section-item cursor-pointer hover:border-blue-500 transition-colors"
                 @click="handleEdit('diamond')">
                 <div class="border rounded p-3 flex flex-col items-center justify-center h-20 bg-white relative">
-                    <div class="absolute top-0 left-0 bg-yellow-100 text-xs px-2 py-1 rounded-tl rounded-br text-black">3</div>
+                    <div class="absolute top-0 left-0 bg-yellow-100 text-xs px-2 py-1 rounded-tl rounded-br text-black">
+                        3</div>
                     <span>金刚区</span>
                 </div>
             </div>
@@ -31,7 +34,8 @@
             <div class="section-item cursor-pointer hover:border-blue-500 transition-colors"
                 @click="handleEdit('leftCarousel')">
                 <div class="border rounded p-3 flex h-24 bg-white relative">
-                    <div class="absolute top-0 left-0 bg-yellow-100 text-xs px-2 py-1 rounded-tl rounded-br text-black">4</div>
+                    <div class="absolute top-0 left-0 bg-yellow-100 text-xs px-2 py-1 rounded-tl rounded-br text-black">
+                        4</div>
                     <div class="w-1/2 border-r flex items-center justify-center text-sm">左轮播</div>
                     <div class="w-1/2 flex items-center justify-center text-sm" @click.stop="handleEdit('hotSales')">
                         右热销商品</div>
@@ -41,7 +45,8 @@
             <!-- 5. Left Booklist / Right Booklist -->
             <div class="section-item cursor-pointer hover:border-blue-500 transition-colors">
                 <div class="border rounded p-3 flex h-20 bg-white relative">
-                    <div class="absolute top-0 left-0 bg-yellow-100 text-xs px-2 py-1 rounded-tl rounded-br text-black">5</div>
+                    <div class="absolute top-0 left-0 bg-yellow-100 text-xs px-2 py-1 rounded-tl rounded-br text-black">
+                        5</div>
                     <div class="w-1/2 border-r flex items-center justify-center text-sm"
                         @click="handleEdit('leftBooklist')">左书单</div>
                     <div class="w-1/2 flex items-center justify-center text-sm" @click="handleEdit('rightBooklist')">右书单
@@ -53,7 +58,8 @@
             <div class="section-item cursor-pointer hover:border-blue-500 transition-colors"
                 @click="handleEdit('topicDouble')">
                 <div class="border rounded p-3 flex items-center justify-center h-20 bg-white relative">
-                    <div class="absolute top-0 left-0 bg-yellow-100 text-xs px-2 py-1 rounded-tl rounded-br text-black">6</div>
+                    <div class="absolute top-0 left-0 bg-yellow-100 text-xs px-2 py-1 rounded-tl rounded-br text-black">
+                        6</div>
                     专题双排
                 </div>
             </div>
@@ -62,7 +68,8 @@
             <div class="section-item cursor-pointer hover:border-blue-500 transition-colors"
                 @click="handleEdit('topicSingle')">
                 <div class="border rounded p-3 flex items-center justify-center h-20 bg-white relative">
-                    <div class="absolute top-0 left-0 bg-yellow-100 text-xs px-2 py-1 rounded-tl rounded-br text-black">7</div>
+                    <div class="absolute top-0 left-0 bg-yellow-100 text-xs px-2 py-1 rounded-tl rounded-br text-black">
+                        7</div>
                     专题单排
                 </div>
             </div>
@@ -71,7 +78,8 @@
             <div class="section-item mt-auto cursor-pointer" @click="handleAddTopic">
                 <div
                     class="border-2 border-dashed border-gray-300 rounded p-4 flex items-center justify-center text-gray-500 hover:border-blue-500 hover:text-blue-500 transition-colors relative">
-                    <div class="absolute top-0 left-0 bg-yellow-100 text-xs px-2 py-1 rounded-tl rounded-br text-black">13</div>
+                    <div class="absolute top-0 left-0 bg-yellow-100 text-xs px-2 py-1 rounded-tl rounded-br text-black">
+                        13</div>
                     <el-icon :size="24">
                         <Plus />
                     </el-icon>
@@ -113,100 +121,301 @@
 
                     <!-- 3. Diamond Zone -->
                     <div class="bg-white p-3 mt-2 grid grid-cols-5 gap-2">
-                        <div v-for="i in 5" :key="i" class="flex flex-col items-center">
-                            <div class="w-10 h-10 bg-gray-200 rounded-full mb-1"></div>
-                            <span class="text-[10px] text-gray-500">金刚区</span>
-                        </div>
+                        <template v-if="hasDiamondData">
+                            <div v-for="(item, index) in diamondData" :key="index" class="flex flex-col items-center">
+                                <el-image :src="item.imgUrl" class="w-10 h-10 rounded-full mb-1 object-cover">
+                                    <template #error>
+                                        <div class="w-10 h-10 bg-gray-200 rounded-full"></div>
+                                    </template>
+                                </el-image>
+                                <span class="text-[10px] text-gray-500 truncate w-full text-center">{{ item.showCateName
+                                    || '金刚区' }}</span>
+                            </div>
+                        </template>
+                        <template v-else>
+                            <div v-for="i in 5" :key="i" class="flex flex-col items-center">
+                                <div class="w-10 h-10 bg-gray-200 rounded-full mb-1"></div>
+                                <span class="text-[10px] text-gray-500">金刚区</span>
+                            </div>
+                        </template>
                     </div>
 
                     <!-- 4. Carousel & Hot Sales -->
                     <div class="flex mt-2 px-2 gap-2">
-                        <div class="w-1/2 bg-white rounded p-2 h-32 flex items-center justify-center bg-gray-200">
-                            左轮播
+                        <div
+                            class="w-1/2 bg-white rounded p-2 h-32 flex items-center justify-center bg-gray-200 overflow-hidden relative">
+                            <template v-if="hasLeftBannerData">
+                                <el-carousel indicator-position="none" arrow="never" :interval="3000"
+                                    class="w-full h-full">
+                                    <el-carousel-item v-for="(item, index) in leftBannerData" :key="index">
+                                        <el-image :src="item.imgUrl" class="w-full h-full object-cover rounded">
+                                            <template #error>
+                                                <div
+                                                    class="w-full h-full bg-gray-300 flex items-center justify-center text-xs text-gray-500">
+                                                    图裂了
+                                                </div>
+                                            </template>
+                                        </el-image>
+                                    </el-carousel-item>
+                                </el-carousel>
+                            </template>
+                            <span v-else class="text-gray-500">左轮播</span>
                         </div>
-                        <div class="w-1/2 bg-white rounded p-2 h-32 flex items-center justify-center bg-gray-200">
-                            右热销
+                        <div
+                            class="w-1/2 bg-white rounded p-2 h-32 flex items-center justify-center bg-gray-200 overflow-hidden relative">
+                            <template v-if="hasRightBannerData">
+                                <div
+                                    class="w-full h-full bg-gradient-to-b from-[#FFE0D7] to-[#FFF5F2] rounded p-2 flex flex-col">
+                                    <div class="text-center font-bold text-gray-800 text-sm mb-2">🔥 热销商品 🔥</div>
+                                    <div class="grid grid-cols-2 gap-2 flex-1">
+                                        <div v-for="(item, index) in rightBannerData" :key="index"
+                                            class="relative rounded-lg overflow-hidden h-full">
+                                            <el-image :src="item.imgUrl" class="w-full h-full object-cover rounded-lg">
+                                                <template #error>
+                                                    <div
+                                                        class="w-full h-full bg-gray-200 flex items-center justify-center text-xs text-gray-400">
+                                                        <el-icon>
+                                                            <Picture />
+                                                        </el-icon>
+                                                    </div>
+                                                </template>
+                                            </el-image>
+                                        </div>
+                                    </div>
+                                </div>
+                            </template>
+                            <span v-else class="text-gray-500">右热销</span>
                         </div>
                     </div>
 
                     <!-- 5. Booklists -->
                     <div class="flex mt-2 px-2 gap-2">
-                        <div class="w-1/2 bg-white rounded p-2 h-24 flex items-center justify-center bg-gray-200">
-                            左书单
+                        <div
+                            class="w-1/2 bg-white rounded h-24 flex items-center justify-center bg-gray-200 overflow-hidden relative">
+                            <template v-if="hasLeftBooklistData">
+                                <el-carousel indicator-position="none" arrow="never" :interval="3000"
+                                    class="w-full h-full">
+                                    <el-carousel-item v-for="(item, index) in leftBooklistData" :key="index">
+                                        <div class="w-full h-full relative bg-[#FFF8E1] rounded p-2 overflow-hidden">
+                                            <div
+                                                class="text-sm font-bold text-[#B8741A] truncate z-10 relative leading-tight">
+                                                {{ item.showCateName || '书单标题' }}
+                                            </div>
+                                            <el-image :src="item.imgUrl"
+                                                class="absolute -right-2 -bottom-2 w-14 h-14 object-contain z-0 opacity-90"
+                                                fit="contain">
+                                                <template #error>
+                                                    <div
+                                                        class="w-full h-full flex items-center justify-center text-xs text-gray-300">
+                                                        <el-icon>
+                                                            <Picture />
+                                                        </el-icon>
+                                                    </div>
+                                                </template>
+                                            </el-image>
+                                        </div>
+                                    </el-carousel-item>
+                                </el-carousel>
+                            </template>
+                            <span v-else class="text-gray-500">左书单</span>
                         </div>
-                        <div class="w-1/2 bg-white rounded p-2 h-24 flex items-center justify-center bg-gray-200">
-                            右书单
+                        <div
+                            class="w-1/2 bg-white rounded h-24 flex items-center justify-center bg-gray-200 overflow-hidden relative">
+                            <template v-if="hasRightBooklistData">
+                                <el-carousel indicator-position="none" arrow="never" :interval="3000"
+                                    class="w-full h-full">
+                                    <el-carousel-item v-for="(item, index) in rightBooklistData" :key="index">
+                                        <div class="w-full h-full relative bg-[#E8F5E9] rounded overflow-hidden p-2">
+                                            <div
+                                                class="text-sm font-bold text-[#2E7D32] truncate z-10 relative leading-tight">
+                                                {{ item.showCateName || '书单标题' }}
+                                            </div>
+                                            <el-image :src="item.imgUrl"
+                                                class="absolute -right-2 -bottom-2 w-14 h-14 object-contain z-0 opacity-90"
+                                                fit="contain">
+                                                <template #error>
+                                                    <div
+                                                        class="w-full h-full flex items-center justify-center text-xs text-gray-300">
+                                                        <el-icon>
+                                                            <Picture />
+                                                        </el-icon>
+                                                    </div>
+                                                </template>
+                                            </el-image>
+                                        </div>
+                                    </el-carousel-item>
+                                </el-carousel>
+                            </template>
+                            <span v-else class="text-gray-500">右书单</span>
                         </div>
                     </div>
 
                     <!-- 6 & 7 Topics -->
                     <div class="mt-2 px-2">
-                        <div class="bg-white rounded h-24 mb-2 flex items-center justify-center bg-gray-200">专题双排</div>
-                        <div class="bg-white rounded h-24 flex items-center justify-center bg-gray-200">专题单排</div>
+                        <!-- Topic Double (Double Row) -->
+                        <template v-if="hasTopicDoubleData">
+                            <div class="bg-white rounded p-3 mb-2">
+                                <div class="flex justify-between items-center mb-2">
+                                    <span class="font-bold text-sm">{{ topicDoubleData[0]?.title || '专题双排' }}</span>
+                                    <span class="text-xs text-gray-400">查看全部 ></span>
+                                </div>
+                                <div class="grid grid-cols-3 gap-2">
+                                    <div v-for="i in 6" :key="i" class="flex flex-col">
+                                        <div class="bg-gray-200 rounded h-20 w-full mb-1 overflow-hidden relative">
+                                            <!-- Use topic cover as first item if available, else placeholder -->
+                                            <el-image v-if="i === 1 && topicDoubleData[0]?.imgUrl"
+                                                :src="topicDoubleData[0].imgUrl" class="w-full h-full object-cover">
+                                                <template #error>
+                                                    <div class="w-full h-full bg-gray-200"></div>
+                                                </template>
+                                            </el-image>
+                                        </div>
+                                        <div class="h-3 bg-gray-100 rounded w-3/4 mb-1"></div>
+                                        <div class="h-3 bg-gray-100 rounded w-1/2"></div>
+                                    </div>
+                                </div>
+                            </div>
+                        </template>
+                        <div v-else class="bg-white rounded h-24 mb-2 flex items-center justify-center bg-gray-200">专题双排
+                        </div>
+
+                        <!-- Topic Single (Single Row) -->
+                        <template v-if="hasTopicSingleData">
+                            <div class="bg-white rounded p-3">
+                                <div class="flex justify-between items-center mb-2">
+                                    <span class="font-bold text-sm">{{ topicSingleData[0]?.title || '专题单排' }}</span>
+                                    <span class="text-xs text-gray-400">查看全部 ></span>
+                                </div>
+                                <div class="grid grid-cols-3 gap-2">
+                                    <div v-for="i in 3" :key="i" class="flex flex-col">
+                                        <div class="bg-gray-200 rounded h-20 w-full mb-1 overflow-hidden relative">
+                                            <el-image v-if="i === 1 && topicSingleData[0]?.imgUrl"
+                                                :src="topicSingleData[0].imgUrl" class="w-full h-full object-cover">
+                                                <template #error>
+                                                    <div class="w-full h-full bg-gray-200"></div>
+                                                </template>
+                                            </el-image>
+                                        </div>
+                                        <div class="h-3 bg-gray-100 rounded w-3/4 mb-1"></div>
+                                        <div class="h-3 bg-gray-100 rounded w-1/2"></div>
+                                    </div>
+                                </div>
+                            </div>
+                        </template>
+                        <div v-else class="bg-white rounded h-24 flex items-center justify-center bg-gray-200">专题单排
+                        </div>
                     </div>
                 </div>
             </div>
         </div>
 
         <!-- Modals -->
-        <DiamondEdit v-model="modals.diamond" />
-        <CarouselEdit 
-            v-model="modals.leftCarousel" 
-            title="左轮播编辑" 
-            type="carousel"
-            :fixedNumber="3"
-        />
-        <HotSalesEdit v-model="modals.hotSales" />
-        <CarouselEdit 
-            v-model="modals.leftBooklist" 
-            title="左书单编辑" 
-            type="banner"
-            :fixedNumber="4"
-        />
-        <CarouselEdit 
-            v-model="modals.rightBooklist" 
-            title="右书单编辑" 
-            type="banner"
-            :fixedNumber="4"
-        />
+        <DiamondEdit v-model="modals.diamond" @success="handleSuccess" />
+        <CarouselEdit v-model="modals.leftCarousel" title="左轮播编辑" type="carousel" :fixedNumber="3"
+            position="left_banner" @success="handleSuccess" />
+        <HotSalesEdit v-model="modals.hotSales" position="right_banner" @success="handleSuccess" />
+        <DiamondEdit v-model="modals.leftBooklist" title="左书单编辑" position="left_book_list" :limit="4"
+            @success="handleSuccess" />
+        <DiamondEdit v-model="modals.rightBooklist" title="右书单编辑" position="right_book_list" :limit="4"
+            @success="handleSuccess" />
 
         <!-- Reusing CarouselEdit for Topics as they are likely banner images linking to topics -->
-        <CarouselEdit v-model="modals.topicDouble" title="专题双排编辑" type="banner" />
-        <CarouselEdit v-model="modals.topicSingle" title="专题单排编辑" type="banner" />
+        <TopicEdit v-model="modals.topicDouble" title="专题双排编辑" position="double_row_topic" :limit="1"
+            @success="handleSuccess" />
+        <TopicEdit v-model="modals.topicSingle" title="专题单排编辑" position="single_row_topic" :limit="1"
+            @success="handleSuccess" />
     </div>
 </template>
 
 <script setup>
-import { ref, reactive } from 'vue';
-import { Search, Plus } from '@element-plus/icons-vue';
-import DiamondEdit from './components/DiamondEdit.vue';
-import CarouselEdit from './components/CarouselEdit.vue';
-import HotSalesEdit from './components/HotSalesEdit.vue';
-
-const modals = reactive({
-    diamond: false,
-    leftCarousel: false,
-    hotSales: false,
-    leftBooklist: false,
-    rightBooklist: false,
-    topicDouble: false,
-    topicSingle: false
-});
-
-const handleEdit = (type) => {
-    if (modals[type] !== undefined) {
-        modals[type] = true;
-    }
-};
+    import { ref, reactive, onMounted } from 'vue';
+    import { Search, Plus, Picture } from '@element-plus/icons-vue';
+    import DiamondEdit from './components/DiamondEdit.vue';
+    import CarouselEdit from './components/CarouselEdit.vue';
+    import HotSalesEdit from './components/HotSalesEdit.vue';
+    import TopicEdit from './components/TopicEdit.vue';
+    import request from '@/utils/request';
+
+    const modals = reactive({
+        diamond: false,
+        leftCarousel: false,
+        hotSales: false,
+        leftBooklist: false,
+        rightBooklist: false,
+        topicDouble: false,
+        topicSingle: false
+    });
+
+    const diamondData = ref([]);
+    const hasDiamondData = ref(false);
+    const leftBooklistData = ref([]);
+    const hasLeftBooklistData = ref(false);
+    const rightBooklistData = ref([]);
+    const hasRightBooklistData = ref(false);
+    const leftBannerData = ref([]);
+    const hasLeftBannerData = ref(false);
+    const rightBannerData = ref([]);
+    const hasRightBannerData = ref(false);
+    const topicDoubleData = ref([]);
+    const hasTopicDoubleData = ref(false);
+    const topicSingleData = ref([]);
+    const hasTopicSingleData = ref(false);
+
+    const fetchDecorationData = async () => {
+        const positions = [
+            { key: 'diamond_area', data: diamondData, has: hasDiamondData },
+            { key: 'left_book_list', data: leftBooklistData, has: hasLeftBooklistData },
+            { key: 'right_book_list', data: rightBooklistData, has: hasRightBooklistData },
+            { key: 'left_banner', data: leftBannerData, has: hasLeftBannerData },
+            { key: 'right_banner', data: rightBannerData, has: hasRightBannerData },
+            { key: 'double_row_topic', data: topicDoubleData, has: hasTopicDoubleData },
+            { key: 'single_row_topic', data: topicSingleData, has: hasTopicSingleData }
+        ];
 
-const handleAddTopic = () => {
-    // Logic to add topic
-    console.log('Add topic');
-};
+        for (const pos of positions) {
+            request.get('/book/showIndex/getInfoByPosition', {
+                params: { position: pos.key }
+            }).then(res => {
+                const list = res.data.data || [];
+                if (list.length > 0) {
+                    pos.data.value = list;
+                    pos.has.value = true;
+                } else {
+                    pos.data.value = [];
+                    pos.has.value = false;
+                }
+            }).catch(e => {
+                console.error(`Failed to fetch ${pos.key}`, e);
+            });
+        }
+    };
+
+    const handleEdit = (type) => {
+        if (modals[type] !== undefined) {
+            modals[type] = true;
+        }
+    };
+
+    const handleSuccess = () => {
+        fetchDecorationData();
+    };
+
+    const handleAddTopic = () => {
+        // Logic to add topic
+        console.log('Add topic');
+    };
+
+    onMounted(() => {
+        fetchDecorationData();
+    });
 </script>
 
 <style scoped>
-.section-item {
-    position: relative;
-}
+    .section-item {
+        position: relative;
+    }
+
+    .phone-content {
+        max-height: calc(100vh - 210px);
+    }
 </style>

+ 95 - 0
src/views/salesOps/topics/components/topics-bind.vue

@@ -0,0 +1,95 @@
+<template>
+    <goods-select-dialog ref="goodsSelectRef" v-model="visible" :default-selected="selectedBooks" title="绑定图书"
+        @confirm="handleConfirm" />
+</template>
+
+<script setup>
+import { ref } from 'vue';
+import GoodsSelectDialog from '../../components/GoodsSelectDialog.vue';
+import request from '@/utils/request';
+import { EleMessage } from 'ele-admin-plus/es';
+
+const emit = defineEmits(['success']);
+
+const visible = ref(false);
+const selectedBooks = ref([]);
+const originalIsbns = ref([]);
+const currentId = ref(null);
+const goodsSelectRef = ref(null);
+
+const handleOpen = async (row) => {
+    currentId.value = row.id;
+    selectedBooks.value = [];
+    originalIsbns.value = [];
+
+    // Reset dialog state
+    if (goodsSelectRef.value?.reset) {
+        goodsSelectRef.value.reset();
+    }
+
+    await loadBoundBooks(row.id);
+    visible.value = true;
+};
+
+const loadBoundBooks = async (id) => {
+    try {
+        const res = await request.get('/book/showIndex/queryBindBook', {
+            params: { showCateId: id, type: 1, pageNum: 1, pageSize: 1000 }
+        });
+        const list = res.data?.rows || res.data || [];
+        
+        // Map bookIsbn to isbn to match GoodsSelectDialog rowKey
+        const mappedList = (Array.isArray(list) ? list : []).map(item => ({
+            ...item,
+            isbn: item.bookIsbn || item.isbn
+        }));
+        
+        selectedBooks.value = mappedList;
+        originalIsbns.value = mappedList.map(b => b.isbn);
+    } catch (e) {
+    }
+};
+
+const handleConfirm = async (rows) => {
+    if (!currentId.value) return;
+    
+    const loading = EleMessage.loading('正在保存...');
+    try {
+        const currentIsbns = rows.map(b => b.isbn);
+        
+        const toAdd = currentIsbns.filter(isbn => !originalIsbns.value.includes(isbn));
+        const toRemove = originalIsbns.value.filter(isbn => !currentIsbns.includes(isbn));
+        
+        const promises = [];
+        
+        if (toAdd.length > 0) {
+            promises.push(request.post('/book/showIndex/bindBook', {
+                showCateId: currentId.value,
+                bookIsbnList: toAdd
+            }));
+        }
+        
+        if (toRemove.length > 0) {
+            promises.push(request.post('/book/showIndex/unBindBook', {
+                showCateId: currentId.value,
+                bookIsbnList: toRemove
+            }));
+        }
+        
+        await Promise.all(promises);
+        
+        loading.close();
+        EleMessage.success('绑定成功');
+        visible.value = false;
+        emit('success');
+    } catch (e) {
+        loading.close();
+        console.error(e);
+        EleMessage.error(e.message || '操作失败');
+    }
+};
+
+defineExpose({
+    handleOpen
+});
+</script>

+ 90 - 91
src/views/salesOps/topics/components/topics-edit.vue

@@ -1,115 +1,114 @@
 <template>
-    <simple-form-modal ref="modalRef" :items="formItems" :baseUrl="baseUrl" @success="handleSuccess" :title="title"
-        width="600px" labelWidth="100px">
+    <ele-modal :width="600" v-model="visible" :title="title" @confirm="handleConfirm" @cancel="handleCancel"
+        :confirm-loading="loading">
+        <el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
+            <el-form-item label="专题名称" prop="showName">
+                <el-input v-model="form.showName" placeholder="请输入专题名称" />
+            </el-form-item>
 
-        <template #relatedItems="{ model }">
-            <div class="flex items-center space-x-4">
-                <el-checkbox v-model="isSpecified" label="指定商品" />
-                <div v-if="isSpecified" class="flex items-center text-gray-600">
-                    <span>已选择 <span class="text-red-500 font-bold">{{ (model.relatedItems || []).length }}</span> 款商品,</span>
-                    <el-button link type="primary" @click="openGoodsDialog(model)">选择商品 ></el-button>
+            <el-form-item label="是否上线" prop="showStatus">
+                <el-switch v-model="form.showStatus" :active-value="1" :inactive-value="2" />
+            </el-form-item>
+
+            <el-form-item label="专题Banner" prop="imgUrl">
+                <div class="flex flex-col">
+                    <image-upload v-model="form.imgUrl" :limit="1" :fileSize="2" fileType="jpg" />
+                    <span class="text-gray-400 text-xs mt-1">建议尺寸:750*350</span>
                 </div>
-            </div>
-        </template>
+            </el-form-item>
 
-        <template #style="{ model }">
-            <div class="flex flex-col">
-                <image-upload v-model="model.style" :limit="1" :fileSize="2" fileType="jpg" />
-                <span class="text-gray-400 text-xs mt-1">只支持.jpg 格式</span>
-            </div>
-        </template>
+            <el-form-item label="专题描述" prop="remark">
+                <el-input v-model="form.remark" type="textarea" placeholder="请输入专题描述" />
+            </el-form-item>
+        </el-form>
 
-        <template #status="{ model }">
-            <el-switch v-model="model.status" active-value="online" inactive-value="offline" />
+        <template #footer>
+            <div class="flex justify-end items-center">
+                <el-button @click="handleCancel">取消</el-button>
+                <el-button type="primary" :loading="loading" @click="handleConfirm">确定</el-button>
+            </div>
         </template>
-
-    </simple-form-modal>
-
-    <goods-select-dialog v-model="goodsDialogVisible" :default-selected="selectedGoods" @confirm="handleGoodsConfirm" />
+    </ele-modal>
 </template>
 
 <script setup>
-import { ref, computed } from 'vue';
-import SimpleFormModal from '@/components/CommonPage/SimpleFormModal.vue';
-import GoodsSelectDialog from '../../components/GoodsSelectDialog.vue';
+import { ref, reactive, computed } from 'vue';
 import ImageUpload from '@/components/ImageUpload/index.vue';
+import request from '@/utils/request';
+import { EleMessage } from 'ele-admin-plus/es';
 
 const emit = defineEmits(['success']);
-const modalRef = ref(null);
-const title = ref('添加专题');
-const goodsDialogVisible = ref(false);
-const selectedGoods = ref([]);
-const isSpecified = ref(true); // Default checked as per image
-const currentFormModel = ref(null);
 
-const baseUrl = {
-    add: '/salesOps/topics/add',
-    update: '/salesOps/topics/update'
-};
+const visible = ref(false);
+const loading = ref(false);
+const formRef = ref(null);
 
-const formItems = computed(() => [
-    {
-        label: '专题名称',
-        prop: 'name',
-        type: 'input',
-        required: true,
-        props: { placeholder: '请输入专题名称' }
-    },
-    {
-        label: '关联商品',
-        prop: 'relatedItems',
-        type: 'relatedItems',
-        slot: 'relatedItems',
-        required: true,
-    },
-    {
-        label: '是否上线',
-        prop: 'status',
-        type: 'status',
-        slot: 'status',
-        required: true,
-    },
-    {
-        label: '专题样式',
-        prop: 'style',
-        type: 'style',
-        slot: 'style',
-        required: true,
-    }
-]);
+const form = reactive({
+    id: undefined,
+    showName: '',
+    imgUrl: '',
+    showStatus: 1,
+    remark: '',
+    cateType: 1  // 专题
+});
 
-const handleOpen = (data) => {
-    title.value = data && data.id ? '编辑专题' : '添加专题';
-    // Initialize selected goods from data if available
-    selectedGoods.value = data && data.relatedGoods ? data.relatedGoods : [];
-    // Mocking 10 selected items if it's a new or existing one for demo
-    if (selectedGoods.value.length === 0 && data) {
-        // selectedGoods.value = ...
-    }
-    modalRef.value.handleOpen(data);
+const rules = {
+    showName: [{ required: true, message: '请输入专题名称', trigger: 'blur' }],
+    imgUrl: [{ required: true, message: '请上传Banner', trigger: 'change' }],
+    showStatus: [{ required: true, message: '请选择状态', trigger: 'change' }]
 };
 
-const handleSuccess = (data) => {
-    emit('success', data);
-};
+const title = computed(() => form.id ? '编辑专题' : '添加专题');
 
-const openGoodsDialog = (model) => {
-    currentFormModel.value = model;
-    // ensure relatedItems is initialized
-    if (!model.relatedItems) {
-        model.relatedItems = [];
+const handleOpen = async (row) => {
+    visible.value = true;
+    
+    if (row) {
+        Object.assign(form, {
+            id: row.id,
+            showName: row.showName,
+            imgUrl: row.imgUrl,
+            showStatus: row.showStatus,
+            remark: row.remark,
+            cateType: 1
+        });
+    } else {
+        Object.assign(form, {
+            id: undefined,
+            showName: '',
+            imgUrl: '',
+            showStatus: 1,
+            remark: '',
+            cateType: 1
+        });
     }
-    // Use the model's data for the dialog
-    selectedGoods.value = model.relatedItems;
-    goodsDialogVisible.value = true;
 };
 
-const handleGoodsConfirm = (rows) => {
-    selectedGoods.value = rows;
-    if (currentFormModel.value) {
-        currentFormModel.value.relatedItems = rows;
-    }
-    goodsDialogVisible.value = false;
+const handleConfirm = () => {
+    formRef.value?.validate(async (valid) => {
+        if (!valid) return;
+        loading.value = true;
+        try {
+            const params = { ...form };
+            if (!params.id) {
+                delete params.id;
+            }
+            await request.post('/book/showIndex/addSpecial', params);
+            
+            EleMessage.success(title.value + '成功');
+            visible.value = false;
+            emit('success');
+        } catch (e) {
+            console.error(e);
+            EleMessage.error(e.message || '操作失败');
+        } finally {
+            loading.value = false;
+        }
+    });
+};
+
+const handleCancel = () => {
+    visible.value = false;
 };
 
 defineExpose({

+ 53 - 33
src/views/salesOps/topics/index.vue

@@ -3,27 +3,31 @@
         <!-- Search Bar -->
         <div class="bg-white p-4 mb-4 rounded shadow-sm flex items-center justify-between">
             <div class="flex items-center space-x-2">
-                <el-input v-model="searchQuery" placeholder="请输入专题名称" style="width: 240px" clearable />
+                <el-input v-model="searchForm.name" placeholder="请输入专题名称" style="width: 240px" clearable />
                 <el-button type="primary" @click="handleSearch">查询</el-button>
                 <el-button @click="handleReset">重置</el-button>
             </div>
             <el-button type="primary" @click="handleAdd">添加专题</el-button>
         </div>
 
-        <common-table ref="tableRef" :pageConfig="pageConfig" :columns="columns" :tools="false"
-            :datasource="mockDatasource">
+        <common-table ref="tableRef" :pageConfig="pageConfig" :columns="columns" :tools="false">
+            <!-- 封面 -->
+            <template #imgUrl="{ row }">
+                <el-image :src="row.imgUrl" class="w-10 h-10 object-cover" :preview-src-list="[row.imgUrl]"
+                    preview-teleported fit="cover" />
+            </template>
 
             <!-- 状态 -->
-            <template #status="{ row }">
-                <span :class="row.status === 'online' ? 'text-green-500' : 'text-gray-500'">
-                    {{ row.status === 'online' ? '已上架' : '已下架' }}
+            <template #showStatus="{ row }">
+                <span :class="row.showStatus === 1 ? 'text-green-500' : 'text-gray-500'">
+                    {{ row.showStatus === 1 ? '已上架' : '已下架' }}
                 </span>
             </template>
 
             <!-- 操作 -->
             <template #action="{ row }">
                 <div class="flex items-center space-x-2">
-                    <el-button v-if="row.status === 'online'" size="small" type="primary" plain
+                    <el-button v-if="row.showStatus === 1" size="small" type="primary" plain
                         @click="handleStatusToggle(row)">
                         下架
                     </el-button>
@@ -32,13 +36,14 @@
                     </el-button>
 
                     <el-button link type="primary" @click="handleEdit(row)">编辑</el-button>
-                    <el-button link type="primary" @click="handleDetail(row)">详情</el-button>
-                    <el-button link type="danger" @click="handleDelete(row)">删除</el-button>
+                    <el-button link type="primary" @click="handleBind(row)">绑定图书</el-button>
+                    <!-- <el-button link type="danger" @click="handleDelete(row)">删除</el-button> -->
                 </div>
             </template>
         </common-table>
 
         <topics-edit ref="editDialogRef" @success="handleSuccess" />
+        <topics-bind ref="bindDialogRef" @success="handleSuccess" />
     </ele-page>
 </template>
 
@@ -46,42 +51,46 @@
     import { ref, reactive } from 'vue';
     import CommonTable from '@/components/CommonPage/CommonTable.vue';
     import TopicsEdit from './components/topics-edit.vue';
+    import TopicsBind from './components/topics-bind.vue';
     import { EleMessage } from 'ele-admin-plus/es';
 
     defineOptions({ name: 'TopicsList' });
 
     const tableRef = ref(null);
     const editDialogRef = ref(null);
-    const searchQuery = ref('');
+    const bindDialogRef = ref(null);
+    const searchForm = reactive({
+        name: ''
+    });
 
     const pageConfig = reactive({
-        pageUrl: '/book/showIndex/queryBindBook',
+        pageUrl: '/book/showIndex/list',
         fileName: '专题列表',
         cacheKey: 'topics-list',
         params: {
-            type: 1
+            cateType: 1  // 专题
         }
     });
 
     const columns = ref([
         { type: 'selection', width: 50, align: 'center' },
-        { label: '编号', prop: 'code', width: 100, align: 'center' },
-        { label: '专题名称', prop: 'name', align: 'center' },
-        { label: '发布时间', prop: 'publishTime', align: 'center', width: 180 },
-        { label: '专题类型', prop: 'typeLabel', align: 'center' },
-        { label: '相关单品', prop: 'relatedItems', align: 'center' },
-        { label: '状态', prop: 'status', slot: 'status', align: 'center' },
-        { label: '排序', prop: 'sort', align: 'center', width: 80 },
-        { label: '操作', prop: 'action', slot: 'action', align: 'center', width: 250 }
+        { label: '序号', type: 'index', width: 80, align: 'center' },
+        { label: '专题名称', prop: 'showName', align: 'center', minWidth: 150 },
+        { label: '封面', prop: 'imgUrl', slot: 'imgUrl', align: 'center', width: 120 },
+        { label: '相关单品', prop: 'bookCount', align: 'center', width: 100, formatter: (row) => row.bookCount || 0 },
+        { label: '备注', prop: 'remark', align: 'center', minWidth: 150, showOverflowTooltip: true },
+        { label: '状态', prop: 'showStatus', slot: 'showStatus', align: 'center', width: 100 },
+        { label: '创建时间', prop: 'createTime', align: 'center', width: 180 },
+        { label: '操作', prop: 'action', slot: 'action', align: 'center', width: 200 }
     ]);
 
     const handleSearch = () => {
-        reload();
+        tableRef.value?.reload(searchForm);
     };
 
     const handleReset = () => {
-        searchQuery.value = '';
-        reload();
+        searchForm.name = '';
+        handleSearch();
     };
 
     const reload = () => {
@@ -96,25 +105,36 @@
         editDialogRef.value?.handleOpen(row);
     };
 
-    const handleDetail = (row) => {
-        EleMessage.info(`查看 ${row.name} 详情`);
+    const handleBind = (row) => {
+        bindDialogRef.value?.handleOpen(row);
     };
 
     const handleDelete = (row) => {
-        EleMessage.confirm(`确定要删除 ${row.name} 吗?`)
+        EleMessage.confirm(`确定要删除 ${row.showName} 吗?`)
             .then(() => {
-                // TODO: Call API to delete
-                // tableRef.value?.operatBatch({ method: 'delete', row, url: '/salesOps/topics/delete' });
-                EleMessage.success('删除成功');
-                reload();
+                // TODO: Delete API if available
+                EleMessage.warning('暂无删除接口');
             })
             .catch(() => { });
     };
 
     const handleStatusToggle = (row) => {
-        // TODO: Call API to update status
-        // tableRef.value?.operatBatch({ method: 'post', row, url: '/salesOps/topics/updateStatus' });
-        EleMessage.success(`${row.name} 已${row.status === 'online' ? '上架' : '下架'}`);
+        const newStatus = row.showStatus === 1 ? 2 : 1;
+        const actionText = newStatus === 1 ? '上架' : '下架';
+
+        tableRef.value?.operatBatch({
+            url: '/book/showIndex/updateShowStatus',
+            method: 'post',
+            data: {
+                id: row.id,
+                showStatus: newStatus
+            },
+            title: `确定要${actionText}吗?`,
+            success: () => {
+                EleMessage.success(`${actionText}成功`);
+                reload();
+            }
+        });
     };
 
     const handleSuccess = () => {