Просмотр исходного кода

feat(goods): 重构商品列表页面并更新组件逻辑

ylong 1 месяц назад
Родитель
Сommit
af61ff879f

+ 2 - 0
.trae/rules/book-hi-admin.md

@@ -47,6 +47,7 @@ All list pages must follow this standard structure:
 ### B. Table Configuration (`index.vue`)
 *   **`pageConfig` Object**:
     *   `pageUrl`: Backend API endpoint for pagination (Required).
+    *   `exportUrl`: Backend API endpoint for export (Optional).
     *   `rowKey`: Unique identifier (usually `'id'`).
     *   `fileName`: Default filename for exports.
     *   `cacheKey`: Unique key for column caching.
@@ -66,6 +67,7 @@ All list pages must follow this standard structure:
 
 ### C. Interaction Logic
 *   **Refresh**: `pageRef.value?.reload(where)`.
+*   **Export**: `pageRef.value?.exportData('FileName')`.
 *   **Confirmations**: Use built-in table methods:
     ```javascript
     pageRef.value?.messageBoxConfirm({

+ 23 - 45
src/components/CommonPage/CommonExportLog.vue

@@ -1,47 +1,18 @@
 <!-- 编辑弹窗 -->
 <template>
-    <ele-modal
-        form
-        :width="1160"
-        v-model="visible"
-        title="导出记录"
-        @open="handleOpen"
-        :body-style="{ padding: '0 20px' }"
-    >
-        <common-table
-            ref="pageRef"
-            :pageConfig="pageConfig"
-            :columns="columns"
-            :tools="false"
-        >
+    <ele-modal form :width="1160" v-model="visible" title="导出记录" @open="handleOpen" :body-style="{ padding: '0 20px' }">
+        <common-table ref="pageRef" :pageConfig="pageConfig" :columns="columns" :tools="false">
             <template #toolbar>
-                <ProDatePicker
-                    start-placeholder="申请时间(开始时间)"
-                    end-placeholder="申请时间(结束时间)"
-                    v-model="searchData"
-                />
-                <el-button type="primary" @click="reload" class="ml-4"
-                    >查询</el-button
-                >
+                <ProDatePicker start-placeholder="申请时间(开始时间)" end-placeholder="申请时间(结束时间)" v-model="searchData" />
+                <el-button type="primary" @click="reload" class="ml-4">查询</el-button>
                 <el-button type="info" @click="handleReset">重置</el-button>
             </template>
 
             <template #action="{ row }">
-                <el-button
-                    type="primary"
-                    link
-                    :disabled="row.exportStatus != 3 || row.cleanStatus == 1"
-                    @click="handleDownload(row)"
-                    class="ml-4"
-                    >[下载]</el-button
-                >
-                <el-button
-                    type="danger"
-                    link
-                    @click="handleDelete(row)"
-                    :disabled="row.exportStatus == 1"
-                    >[删除]</el-button
-                >
+                <el-button type="primary" link :disabled="row.exportStatus != 3 || row.cleanStatus == 1"
+                    :loading="row.loading" @click="handleDownload(row)" class="ml-4">[下载]</el-button>
+                <el-button type="danger" link @click="handleDelete(row)"
+                    :disabled="row.exportStatus == 1">[删除]</el-button>
             </template>
         </common-table>
         <template #footer>
@@ -107,7 +78,7 @@
         {
             columnKey: 'action',
             label: '操作',
-            width: 150,
+            width: 180,
             align: 'center',
             slot: 'action'
         }
@@ -121,13 +92,20 @@
     });
 
     async function handleDownload(row) {
-        const res = await request({
-            url: '/common/exportrecord/downLoadFile?id=' + row.id,
-            method: 'get',
-            responseType: 'blob'
-        });
-        await checkDownloadRes(res);
-        download(res.data, row.fileName);
+        row.loading = true;
+        try {
+            const res = await request({
+                url: '/common/exportrecord/downLoadFile?id=' + row.id,
+                method: 'get',
+                responseType: 'blob'
+            });
+            await checkDownloadRes(res);
+            download(res.data, row.fileName);
+        } catch (e) {
+            console.error(e);
+        } finally {
+            row.loading = false;
+        }
     }
 
     //删除

+ 13 - 26
src/views/goods/list/components/common-import-modal.vue

@@ -1,18 +1,8 @@
 <template>
-    <ele-modal
-        :width="460"
-        :title="title"
-        :body-style="{ paddingTop: '8px' }"
-        v-model="visible"
-    >
+    <ele-modal :width="460" :title="title" :body-style="{ paddingTop: '8px' }" v-model="visible">
         <div class="flex mb-2">
             <div class="text-sm">请先下载'{{ templateName }}':</div>
-            <el-button
-                @click="downloadTemplate"
-                link
-                type="primary"
-                >下载{{ templateName }}</el-button
-            >
+            <el-button @click="downloadTemplate" link type="primary">下载{{ templateName }}</el-button>
         </div>
 
         <div class="flex flex-col mb-2">
@@ -22,16 +12,8 @@
             </div>
         </div>
         <div v-loading="loading" class="user-import-upload">
-            <el-upload
-                v-model:file-list="fileList"
-                ref="uploadRef"
-                action=""
-                accept=".xls,.xlsx"
-                :before-upload="doUpload"
-                :auto-upload="false"
-                :limit="1"
-                :on-exceed="handleExceed"
-            >
+            <el-upload v-model:file-list="fileList" ref="uploadRef" action="" accept=".xls,.xlsx"
+                :before-upload="doUpload" :auto-upload="false" :limit="1" :on-exceed="handleExceed">
                 <el-button type="primary" class="mr-2">选择文件</el-button>
                 <ele-text type="placeholder">只能上传 xls、xlsx 文件</ele-text>
             </el-upload>
@@ -39,9 +21,7 @@
 
         <template #footer>
             <el-button @click="visible = false">关闭</el-button>
-            <el-button type="primary" @click="handleSumbit" :loading="loading"
-                >导入</el-button
-            >
+            <el-button type="primary" @click="handleSumbit" :loading="loading">导入</el-button>
         </template>
     </ele-modal>
 </template>
@@ -72,7 +52,8 @@
     const emit = defineEmits(['done']);
 
     /** 弹窗是否打开 */
-    const visible = defineModel({ type: Boolean });
+    // const visible = defineModel({ type: Boolean });
+    const visible = ref(false);
 
     /** 导入请求状态 */
     const loading = ref(false);
@@ -140,4 +121,10 @@
         }
         return false;
     };
+
+    const open = () => {
+        visible.value = true;
+    };
+
+    defineExpose({ open });
 </script>

+ 43 - 47
src/views/goods/list/components/edit-price-modal.vue

@@ -1,28 +1,25 @@
 <template>
     <ele-modal
-        :width="800"
+        :width="500"
         title="编辑价格"
         v-model="visible"
         @closed="handleClosed"
     >
-        <div class="mb-4">
-            <div class="font-bold mb-2">编辑SKU</div>
-            <div class="text-sm">商品标题:{{ rowData?.title || '-' }}</div>
-        </div>
-
-        <ele-table :data="skuList" border style="width: 100%">
-            <el-table-column prop="skuName" label="SKU" min-width="120" />
-            <el-table-column label="价格" min-width="150">
-                <template #default="{ row }">
-                    <el-input v-model="row.price" placeholder="请输入价格" />
-                </template>
-            </el-table-column>
-            <el-table-column prop="skuCode" label="SKU商家编码" min-width="150" />
-        </ele-table>
+        <el-form ref="formRef" :model="form" :rules="rules" label-width="80px">
+            <el-form-item label="商品标题">
+                <span>{{ rowData?.bookName || '-' }}</span>
+            </el-form-item>
+            <el-form-item label="ISBN">
+                <span>{{ rowData?.isbn || '-' }}</span>
+            </el-form-item>
+            <el-form-item label="一般品售价" prop="productPrice">
+                <el-input-number v-model="form.productPrice" :min="0" :precision="2" :step="0.1" style="width: 100%" />
+            </el-form-item>
+        </el-form>
 
         <template #footer>
             <el-button @click="visible = false">取消</el-button>
-            <el-button type="success" @click="handleSubmit" :loading="loading">提交</el-button>
+            <el-button type="primary" @click="handleSubmit" :loading="loading">提交</el-button>
         </template>
     </ele-modal>
 </template>
@@ -33,54 +30,53 @@ import { EleMessage } from 'ele-admin-plus/es';
 import request from '@/utils/request';
 
 const emit = defineEmits(['done']);
-const visible = defineModel({ type: Boolean });
+// const visible = defineModel({ type: Boolean });
+const visible = ref(false);
 const loading = ref(false);
 const rowData = ref({});
-const skuList = ref([]);
+const formRef = ref(null);
+
+const form = reactive({
+    productPrice: 0
+});
+
+const rules = {
+    productPrice: [{ required: true, message: '请输入售价', trigger: 'blur' }]
+};
 
-// Open modal and load data
+// 打开弹窗
 const open = (row) => {
     rowData.value = row;
+    form.productPrice = Number(row.productPrice) || 0;
     visible.value = true;
-    // Mock data for now, replace with API call
-    loadSkuData(row.id);
-};
-
-const loadSkuData = async (id) => {
-    // TODO: Fetch SKU list from API
-    // const res = await request.get(`/goods/sku/list/${id}`);
-    // skuList.value = res.data;
-    
-    // Mock
-    skuList.value = [
-        { id: 1, skuName: '一般', price: rowData.value.price || '0', skuCode: rowData.value.isbn + '-1' },
-        { id: 2, skuName: '良好', price: (Number(rowData.value.price) + 1).toString() || '0', skuCode: rowData.value.isbn + '-2' },
-        { id: 3, skuName: '全新', price: (Number(rowData.value.price) + 2).toString() || '0', skuCode: rowData.value.isbn + '-3' },
-        { id: 4, skuName: '次品', price: (Number(rowData.value.price) - 1).toString() || '0', skuCode: rowData.value.isbn + '-4' }
-    ];
 };
 
-const handleSubmit = async () => {
-    loading.value = true;
-    try {
-        // TODO: Submit changes
-        // await request.post('/goods/sku/update', { skus: skuList.value });
+const handleSubmit = () => {
+    formRef.value?.validate(async (valid) => {
+        if (!valid) return;
         
-        // Mock
-        setTimeout(() => {
+        loading.value = true;
+        try {
+            await request.post('/shop/shopbook/setInfo', {
+                isbn: rowData.value.isbn,
+                productPrice: form.productPrice
+            });
+            
             EleMessage.success('价格更新成功');
             visible.value = false;
             emit('done');
+        } catch (e) {
+            EleMessage.error(e.message || '更新失败');
+        } finally {
             loading.value = false;
-        }, 1000);
-    } catch (e) {
-        loading.value = false;
-    }
+        }
+    });
 };
 
 const handleClosed = () => {
-    skuList.value = [];
     rowData.value = {};
+    form.productPrice = 0;
+    formRef.value?.resetFields();
 };
 
 defineExpose({ open });

+ 122 - 92
src/views/goods/list/components/goods-search.vue

@@ -6,108 +6,138 @@
 </template>
 
 <script setup>
-import { reactive, ref, defineEmits, getCurrentInstance } from 'vue';
-import ProSearch from '@/components/CommonPage/ProSearch2.vue';
+    import { reactive, ref, defineEmits, getCurrentInstance } from 'vue';
+    import ProSearch from '@/components/CommonPage/ProSearch2.vue';
 
-const emit = defineEmits(['search']);
+    const emit = defineEmits(['search']);
 
-const formItems = reactive([
-    { type: 'input', label: '书名', prop: 'bookName' },
-    { type: 'input', label: '作者', prop: 'author' },
-    { type: 'input', label: '出版社', prop: 'publish' },
-    {
-        type: 'inputNumberRange',
-        prop: 'priceRange',
-        keys: ['minPrice', 'maxPrice'],
-        label: '售价',
-        props: {
+    const formItems = reactive([
+        { type: 'input', label: '书名', prop: 'bookName' },
+        { type: 'input', label: '作者', prop: 'author' },
+        { type: 'input', label: '出版社', prop: 'publish' },
+        {
+            type: 'inputNumberRange',
+            prop: 'priceRange',
+            keys: ['productPriceMin', 'productPriceMax'],
             label: '售价',
-            onChange: (val) => {
-                searchRef.value?.setData({
-                    minPrice: val.min,
-                    maxPrice: val.max
-                });
+            props: {
+                label: '售价',
+                onChange: (val) => {
+                    searchRef.value?.setData({
+                        productPriceMin: val.min,
+                        productPriceMax: val.max
+                    });
+                }
             }
-        }
-    },
-    {
-        type: 'inputNumberRange',
-        prop: 'stockRange',
-        keys: ['minStock', 'maxStock'],
-        label: '库存',
-        props: {
+        },
+        {
+            type: 'inputNumberRange',
+            prop: 'stockRange',
+            keys: ['stockMin', 'stockMax'],
             label: '库存',
-            onChange: (val) => {
-                searchRef.value?.setData({
-                    minStock: val.min,
-                    maxStock: val.max
-                });
+            props: {
+                label: '库存',
+                onChange: (val) => {
+                    searchRef.value?.setData({
+                        stockMin: val.min,
+                        stockMax: val.max
+                    });
+                }
             }
-        }
-    },
-    {
-        type: 'daterange',
-        label: '价格变动',
-        prop: 'priceChangeDate',
-        keys: ['priceChangeStartTime', 'priceChangeEndTime'],
-        props: {
-            format: 'YYYY-MM-DD',
-            valueFormat: 'YYYY-MM-DD',
-            startPlaceholder: '开始时间',
-            endPlaceholder: '结束时间',
-            onChange: (val) => {
-                searchRef.value?.setData({
-                    priceChangeStartTime: val && val.length > 0 ? val[0] : '',
-                    priceChangeEndTime: val && val.length > 0 ? val[1] : ''
-                });
+        },
+        {
+            type: 'daterange',
+            label: '价格变动',
+            prop: 'updateTimeRange',
+            keys: ['updateTimeStart', 'updateTimeEnd'],
+            props: {
+                format: 'YYYY-MM-DD',
+                valueFormat: 'YYYY-MM-DD',
+                startPlaceholder: '开始时间',
+                endPlaceholder: '结束时间',
+                onChange: (val) => {
+                    searchRef.value?.setData({
+                        updateTimeStart: val && val.length > 0 ? val[0] : '',
+                        updateTimeEnd: val && val.length > 0 ? val[1] : ''
+                    });
+                }
+            }
+        },
+        {
+            type: 'daterange',
+            label: '上架时间',
+            prop: 'onSaleTimeRange',
+            keys: ['onSaleTimeStart', 'onSaleTimeEnd'],
+            props: {
+                format: 'YYYY-MM-DD',
+                valueFormat: 'YYYY-MM-DD',
+                startPlaceholder: '开始时间',
+                endPlaceholder: '结束时间',
+                onChange: (val) => {
+                    searchRef.value?.setData({
+                        onSaleTimeStart: val && val.length > 0 ? val[0] : '',
+                        onSaleTimeEnd: val && val.length > 0 ? val[1] : ''
+                    });
+                }
             }
+        },
+        { type: 'input', label: 'ISBN', prop: 'isbnStr', props: { placeholder: 'ISBN (英文逗号分割)', }, colProps: { span: 8 } },
+        {
+            type: 'select',
+            label: '销售状态',
+            prop: 'sellStatus',
+            options: [
+                { label: '全部', value: '' },
+                { label: '待上架', value: '0' },
+                { label: '出售中', value: '1' },
+                { label: '已下架(仓库中)', value: '2' },
+                { label: '黑名单', value: '3' }
+            ]
         }
-    },
-    { type: 'input', label: 'ISBN', prop: 'isbn', placeholder: 'ISBN (中英文逗号、空格、换行分割)', style: { width: '300px' } },
-    {
-        type: 'select',
-        label: '商品类型',
-        prop: 'productType',
-        options: [
-            { label: '全部商品类型', value: '' },
-            { label: '图书商品', value: '1' },
-            { label: '其他商品', value: '2' }
-        ],
-        defaultValue: ''
-    }
-]);
+    ]);
 
-const initKeys = reactive({
-    bookName: '',
-    author: '',
-    publish: '',
-    minPrice: '',
-    maxPrice: '',
-    minStock: '',
-    maxStock: '',
-    priceChangeStartTime: '',
-    priceChangeEndTime: '',
-    priceChangeDate: [],
-    isbn: '',
-    productType: ''
-});
+    const initKeys = reactive({
+        bookName: '',
+        author: '',
+        publish: '',
+        productPriceMin: '',
+        productPriceMax: '',
+        stockMin: '',
+        stockMax: '',
+        updateTimeStart: '',
+        updateTimeEnd: '',
+        updateTimeRange: [],
+        onSaleTimeStart: '',
+        onSaleTimeEnd: '',
+        onSaleTimeRange: [],
+        isbnStr: '',
+        sellStatus: ''
+    });
+
+    const searchRef = ref(null);
+    /** 搜索 */
+    const search = (data) => {
+        let params = JSON.parse(JSON.stringify(data));
+        delete params.updateTimeRange;
+        delete params.onSaleTimeRange;
+        delete params.priceRange;
+        delete params.stockRange;
+
+        // 处理 ISBN
+        if (params.isbnStr) {
+            // 按逗号(中英文)、空格、换行符分割,然后用英文逗号连接
+            params.isbnStr = params.isbnStr.split(/[,,\s\n]+/).filter(item => item.trim() !== '').join(',');
+        }
 
-const searchRef = ref(null);
-/** 搜索 */
-const search = (data) => {
-    let params = JSON.parse(JSON.stringify(data));
-    delete params.priceChangeDate;
-    delete params.priceRange;
-    delete params.stockRange;
-    emit('search', params);
-};
+        emit('search', params);
+    };
 
-// 暴露 reset 方法给父组件
-const reset = () => {
-    searchRef.value?.reset();
-};
+    // 暴露 reset 方法给父组件
+    const reset = () => {
+        searchRef.value?.reset();
+    };
 
-defineExpose({
-    reset
-});
+    defineExpose({
+        reset
+    });
 </script>

+ 10 - 3
src/views/goods/list/components/new-book-modal.vue

@@ -33,10 +33,17 @@ import { EleMessage } from 'ele-admin-plus/es';
 import request from '@/utils/request';
 
 const emit = defineEmits(['done']);
-const visible = defineModel({ type: Boolean });
+// const visible = defineModel({ type: Boolean });
+const visible = ref(false);
 const loading = ref(false);
 const formRef = ref(null);
 
+const open = () => {
+    visible.value = true;
+};
+
+defineExpose({ open });
+
 const form = reactive({
     isbn: '',
     price: ''
@@ -53,7 +60,7 @@ const handleSubmit = async () => {
         if (valid) {
             loading.value = true;
             try {
-                // TODO: Replace with actual API endpoint
+                // TODO: 替换为实际 API 接口
                 // const res = await request.post('/goods/book/add', form);
                 // if (res.data.code === 200) {
                 //     EleMessage.success('新建成功');
@@ -61,7 +68,7 @@ const handleSubmit = async () => {
                 //     emit('done');
                 // }
                 
-                // Mock success for now
+                // 暂时模拟成功
                 setTimeout(() => {
                     EleMessage.success('新建成功 (Mock)');
                     visible.value = false;

+ 52 - 59
src/views/goods/list/components/operation-log-modal.vue

@@ -1,16 +1,16 @@
 <!-- 操作日志弹窗 -->
 <template>
-    <ele-modal v-model="visible" title="操作日志" :width="1200" @open="handleDialogOpen">
-        <!-- Search Bar -->
+    <ele-modal v-model="visible" title="操作日志" :width="1200" @open="handleDialogOpen" >
+        <!-- 搜索栏 -->
         <operation-log-search ref="searchRef" @search="handleSearch" />
 
         <common-table ref="tableRef" :pageConfig="pageConfig" :columns="columns" :tools="false"
-            :datasource="mockDatasource" :bodyStyle="{padding:0}">
+            :bodyStyle="{padding:0}" height="50vh">
             <template #actions="{ row }">
                 <el-button link type="primary" @click="handleDownloadSuccess(row)"
-                    v-if="row.successCount > 0">下载成功数据</el-button>
+                    v-if="row.successNum > 0">下载成功数据</el-button>
                 <el-button link type="danger" @click="handleDownloadFail(row)"
-                    v-if="row.failCount > 0">下载失败数据</el-button>
+                    v-if="row.failNum > 0">下载失败数据</el-button>
                 <el-button link type="primary" @click="handleDownloadSource(row)">下载源文件</el-button>
             </template>
         </common-table>
@@ -30,9 +30,10 @@ import { ElMessage } from 'element-plus';
 defineOptions({ name: 'OperationLogModal' });
 
 // 弹窗可见性
-const visible = defineModel({ type: Boolean });
+// const visible = defineModel({ type: Boolean });
+const visible = ref(false);
 
-// Search Component Ref
+// 搜索组件引用
 const searchRef = ref(null);
 
 // 表格组件实例
@@ -40,67 +41,47 @@ const tableRef = ref(null);
 
 // 表格列配置
 const columns = ref([
-    { label: '任务ID', prop: 'taskId', align: 'center', width: 100 },
-    { label: '类型', prop: 'taskTypeStr', align: 'center', width: 100 },
+    { label: '任务ID', prop: 'id', align: 'center', width: 100 },
+    { 
+        label: '类型', 
+        prop: 'taskType', 
+        align: 'center', 
+        width: 100,
+        formatter: (row) => {
+            const map = { 1: '更新价格', 2: '上下架' };
+            return map[row.taskType] || '未知';
+        }
+    },
     { label: '文件名称', prop: 'fileName', align: 'center', minWidth: 200, showOverflowTooltip: true },
-    { label: '商品总数', prop: 'totalItems', align: 'center' },
-    { label: '成功数', prop: 'successCount', align: 'center' },
-    { label: '失败数', prop: 'failCount', align: 'center' },
-    { label: '状态', prop: 'statusStr', align: 'center' },
+    { label: '商品总数', prop: 'shopTotalNum', align: 'center' },
+    { label: '成功数', prop: 'successNum', align: 'center' },
+    { label: '失败数', prop: 'failNum', align: 'center' },
+    { 
+        label: '状态', 
+        prop: 'taskStatus', 
+        align: 'center',
+        formatter: (row) => {
+            const map = { 0: '进行中', 1: '已完成', 2: '失败' };
+            return map[row.taskStatus] || '未知';
+        }
+    },
     { label: '时间', prop: 'createTime', align: 'center', width: 160 },
-    { label: '操作人', prop: 'operator', align: 'center' },
+    { label: '操作人', prop: 'createName', align: 'center' },
     { label: '操作', prop: 'actions', slot: 'actions', align: 'center', width: 150, fixed: 'right' }
 ]);
 
 // 页面配置
 const pageConfig = reactive({
-    pageUrl: '/book/goods/operation/log', // Mock URL
+    pageUrl: '/shop/shopbook/operationLog',
     fileName: '操作日志',
     cacheKey: 'operation-log-data',
     params: {}
 });
 
-// Mock Datasource
-const mockDatasource = ({ page, limit, where }) => {
-    return new Promise((resolve) => {
-        setTimeout(() => {
-            const list = [
-                {
-                    id: 1,
-                    taskId: '0001',
-                    taskTypeStr: '更新价格',
-                    fileName: '【11.04】书籍价格更新6.5.xlsx',
-                    totalItems: 10923,
-                    successCount: 10900,
-                    failCount: 23,
-                    statusStr: '已完成',
-                    createTime: '2024-10-14 16:48',
-                    operator: '琳达'
-                },
-                {
-                    id: 2,
-                    taskId: '0002',
-                    taskTypeStr: '批量上架',
-                    fileName: '20241014_listing.xlsx',
-                    totalItems: 500,
-                    successCount: 400,
-                    failCount: 100,
-                    statusStr: '进行中',
-                    createTime: '2024-10-15 09:30',
-                    operator: 'Admin'
-                }
-            ];
-            resolve({
-                code: 0,
-                msg: 'success',
-                count: list.length,
-                data: list
-            });
-        }, 300);
-    });
-};
+// 移除 Mock 数据源,直接使用 CommonTable
+const mockDatasource = null;
 
-// Search
+// 搜索
 function handleSearch(params) {
     tableRef.value?.reload(params);
 }
@@ -108,7 +89,7 @@ function handleSearch(params) {
 // 弹窗打开处理函数
 function handleDialogOpen() {
     nextTick(() => {
-        // Reset search form which should trigger a search/reload with default params
+        // 重置搜索表单,应使用默认参数触发搜索/重新加载
         if (searchRef.value) {
             searchRef.value.reset();
         } else {
@@ -118,15 +99,27 @@ function handleDialogOpen() {
 }
 
 function handleDownloadSuccess(row) {
-    ElMessage.success(`下载任务 ${row.taskId} 的成功数据`);
+    if (row.successField) {
+        window.open(row.successField);
+    } else {
+        ElMessage.warning('无成功文件');
+    }
 }
 
 function handleDownloadFail(row) {
-    ElMessage.warning(`下载任务 ${row.taskId} 的失败数据`);
+    if (row.failField) {
+        window.open(row.failField);
+    } else {
+        ElMessage.warning('无失败文件');
+    }
 }
 
 function handleDownloadSource(row) {
-    ElMessage.success(`下载任务 ${row.taskId} 的源文件`);
+    if (row.fileName) {
+        window.open(row.fileName);
+    } else {
+        ElMessage.warning('无源文件');
+    }
 }
 
 // 打开弹窗

+ 93 - 53
src/views/goods/list/components/operation-log-search.vue

@@ -4,65 +4,105 @@
 </template>
 
 <script setup>
-import { reactive, ref, defineEmits } from 'vue';
-import ProSearch from '@/components/CommonPage/ProSearch2.vue';
+    import { reactive, ref, defineEmits } from 'vue';
+    import ProSearch from '@/components/CommonPage/ProSearch2.vue';
 
-const emit = defineEmits(['search']);
+    const emit = defineEmits(['search']);
 
-const formItems = reactive([
-    {
-        type: 'select',
-        label: '任务类型',
-        prop: 'taskType',
-        options: [
-            { label: '更新价格', value: 'update_price' },
-            { label: '批量上架', value: 'batch_listing' },
-            { label: '批量下架', value: 'batch_delisting' }
-        ]
-    },
-    {
-        type: 'select',
-        label: '任务状态',
-        prop: 'taskStatus',
-        options: [
-            { label: '已完成', value: 'completed' },
-            { label: '进行中', value: 'processing' },
-            { label: '失败', value: 'failed' }
-        ]
-    },
-    {
-        type: 'date',
-        label: '执行时间',
-        prop: 'execTime',
-        props: {
-            valueFormat: 'YYYY-MM-DD',
-            placeholder: '选择执行时间'
+    const formItems = reactive([
+        { type: 'input', label: '任务ID', prop: 'id', colProps: { span: 6 } },
+        {
+            type: 'select',
+            label: '任务类型',
+            prop: 'taskType',
+            colProps: { span: 6 },
+            options: [
+                { label: '更新价格', value: 1 },
+                { label: '上下架', value: 2 }
+            ]
+        },
+        {
+            type: 'select',
+            label: '任务状态',
+            prop: 'taskStatus',
+            colProps: { span: 6 },
+            options: [
+                { label: '进行中', value: 0 },
+                { label: '已完成', value: 1 },
+                { label: '失败', value: 2 }
+            ]
+        },
+        {
+            type: 'input', label: '操作人', prop: 'createName',
+            colProps: { span: 6 },
+
+        },
+        {
+            type: 'datetimerange',
+            label: '创建时间',
+            prop: 'createTimeRange',
+            colProps: { span: 8 },
+            keys: ['createTimeStart', 'createTimeEnd'],
+            props: {
+                valueFormat: 'YYYY-MM-DD HH:mm:ss',
+                startPlaceholder: '开始时间',
+                endPlaceholder: '结束时间',
+                onChange: (val) => {
+                    searchRef.value?.setData({
+                        createTimeStart: val && val.length > 0 ? val[0] : '',
+                        createTimeEnd: val && val.length > 0 ? val[1] : ''
+                    });
+                }
+            }
+        },
+        {
+            type: 'datetimerange',
+            label: '完成时间',
+            prop: 'finishTimeRange',
+            colProps: { span: 8 },
+            keys: ['finishTimeStart', 'finishTimeEnd'],
+            props: {
+                valueFormat: 'YYYY-MM-DD HH:mm:ss',
+                startPlaceholder: '开始时间',
+                endPlaceholder: '结束时间',
+                onChange: (val) => {
+                    searchRef.value?.setData({
+                        finishTimeStart: val && val.length > 0 ? val[0] : '',
+                        finishTimeEnd: val && val.length > 0 ? val[1] : ''
+                    });
+                }
+            }
         }
-    },
-    { type: 'input', label: '操作人', prop: 'operator' },
-    { type: 'input', label: '任务ID', prop: 'taskId' }
-]);
+    ]);
 
-const initKeys = reactive({
-    taskType: '',
-    taskStatus: '',
-    execTime: '',
-    operator: '',
-    taskId: ''
-});
+    const initKeys = reactive({
+        id: '',
+        taskType: '',
+        taskStatus: '',
+        createName: '',
+        createTimeRange: [],
+        createTimeStart: '',
+        createTimeEnd: '',
+        finishTimeRange: [],
+        finishTimeStart: '',
+        finishTimeEnd: ''
+    });
 
-const searchRef = ref(null);
+    const searchRef = ref(null);
 
-const search = (data) => {
-    emit('search', data);
-};
+    const search = (data) => {
+        let params = JSON.parse(JSON.stringify(data));
+        delete params.createTimeRange;
+        delete params.finishTimeRange;
+        emit('search', params);
+    };
 
-const reset = () => {
-    searchRef.value?.reset();
-};
+    const reset = () => {
+        searchRef.value?.reset();
+    };
 
-defineExpose({
-    reset,
-    setData: (data) => searchRef.value?.setData(data)
-});
+    defineExpose({
+        reset,
+        setData: (data) => searchRef.value?.setData(data)
+    });
 </script>

+ 0 - 163
src/views/goods/list/components/recycle-detail-modal.vue

@@ -1,163 +0,0 @@
-<!-- 回收明细弹窗 -->
-<template>
-    <ele-modal
-        v-model="visible"
-        title="回收明细"
-        :width="1200"
-        @open="handleDialogOpen"
-    >
-        <!-- Search Bar -->
-        <recycle-detail-search ref="searchRef" @search="handleSearch" />
-
-        <common-table
-            ref="tableRef"
-            :pageConfig="pageConfig"
-            :columns="columns"
-            :tools="false"
-            :datasource="mockDatasource"
-        >
-        </common-table>
-
-        <template #footer>
-            <el-button @click="visible = false">关闭</el-button>
-        </template>
-    </ele-modal>
-</template>
-
-<script setup>
-    import { ref, reactive, defineExpose, nextTick } from 'vue';
-    import CommonTable from '@/components/CommonPage/CommonTable.vue';
-    import RecycleDetailSearch from './recycle-detail-search.vue';
-
-    defineOptions({ name: 'RecycleDetailModal' });
-
-    // 弹窗可见性
-    const visible = defineModel({ type: Boolean });
-    // 当前查看的图书信息
-    const currentBook = ref(null);
-    // 查询参数
-    const queryParams = reactive({
-        bookId: ''
-    });
-
-    // Search Component Ref
-    const searchRef = ref(null);
-
-    // 表格组件实例
-    const tableRef = ref(null);
-
-    // 表格列配置
-    const columns = ref([
-        { label: '订单编号', prop: 'orderNo', align: 'center', minWidth: 120 },
-        { label: '用户名', prop: 'userName', align: 'center' },
-        {
-            label: '预估单价',
-            prop: 'estPrice',
-            align: 'center',
-            formatter: (row) => '¥' + (row?.estPrice || 0)
-        },
-        { label: '预估本数', prop: 'estCount', align: 'center' },
-        { label: '回收本数', prop: 'recycleCount', align: 'center' },
-        { 
-            label: '审核金额', 
-            prop: 'auditAmount', 
-            align: 'center',
-            formatter: (row) => '¥' + (row?.auditAmount || 0)
-        },
-        { label: '发货人', prop: 'senderName', align: 'center' },
-        { label: '手机号', prop: 'senderPhone', align: 'center', minWidth: 110 },
-        { label: '发货地址', prop: 'senderAddress', align: 'center', minWidth: 150, showOverflowTooltip: true },
-        { label: '收货仓库', prop: 'warehouse', align: 'center' },
-        { label: '订单状态', prop: 'orderStatus', align: 'center' },
-        { label: '提交时间', prop: 'createTime', align: 'center', width: 160 }
-    ]);
-
-    // 页面配置
-    const pageConfig = reactive({
-        pageUrl: '/book/recycle/detail/pagelist', // Mock URL
-        fileName: '回收明细',
-        cacheKey: 'recycle-detail-data',
-        params: {
-            bookId: ''
-        }
-    });
-
-    // Mock Datasource
-    const mockDatasource = ({ page, limit, where }) => {
-        return new Promise((resolve) => {
-            setTimeout(() => {
-                const list = [
-                    {
-                        id: 1,
-                        orderNo: 'REC202310270001',
-                        userName: '张三',
-                        estPrice: 15.5,
-                        estCount: 2,
-                        recycleCount: 2,
-                        auditAmount: 31.0,
-                        senderName: '李四',
-                        senderPhone: '13800138000',
-                        senderAddress: '北京市朝阳区某街道',
-                        warehouse: '北京仓',
-                        orderStatus: '已完成',
-                        createTime: '2023-10-27 10:00:00'
-                    },
-                    {
-                        id: 2,
-                        orderNo: 'REC202310270002',
-                        userName: '王五',
-                        estPrice: 10.0,
-                        estCount: 1,
-                        recycleCount: 0,
-                        auditAmount: 0,
-                        senderName: '赵六',
-                        senderPhone: '13900139000',
-                        senderAddress: '上海市浦东新区',
-                        warehouse: '上海仓',
-                        orderStatus: '审核失败',
-                        createTime: '2023-10-26 14:30:00'
-                    }
-                ];
-                resolve({
-                    code: 0,
-                    msg: 'success',
-                    count: list.length,
-                    data: list
-                });
-            }, 300);
-        });
-    };
-
-    // Search
-    function handleSearch(params) {
-        const finalParams = { ...params, bookId: queryParams.bookId };
-        tableRef.value?.reload(finalParams);
-    }
-
-    // 弹窗打开处理函数
-    function handleDialogOpen() {
-        nextTick(() => {
-            // Reset search form which should trigger a search/reload with default params
-            if (searchRef.value) {
-                searchRef.value.reset();
-            } else {
-                tableRef.value?.reload({ bookId: queryParams.bookId });
-            }
-        });
-    }
-
-    // 打开弹窗
-    function open(row) {
-        if (!row) return;
-        currentBook.value = row;
-        queryParams.bookId = row.id;
-        visible.value = true;
-    }
-
-    defineExpose({
-        open
-    });
-</script>
-
-<style scoped>
-</style>

+ 0 - 68
src/views/goods/list/components/recycle-detail-search.vue

@@ -1,68 +0,0 @@
-<!-- 回收明细搜索表单 -->
-<template>
-    <ele-card :body-style="{ paddingBottom: '8px' }">
-        <ProSearch
-            :items="formItems"
-            ref="searchRef"
-            @search="search"
-            :initKeys="initKeys"
-        />
-    </ele-card>
-</template>
-
-<script setup>
-    import { reactive, ref, defineEmits } from 'vue';
-    import ProSearch from '@/components/CommonPage/ProSearch2.vue';
-
-    const emit = defineEmits(['search']);
-
-    const formItems = reactive([
-        { type: 'input', label: '发货人名称', prop: 'senderName' },
-        { type: 'input', label: '发货人电话', prop: 'senderPhone' },
-        { type: 'input', label: '发货人地址', prop: 'senderAddress' },
-        {
-            type: 'daterange',
-            label: '时间',
-            prop: 'dateRange',
-            keys: ['startTime', 'endTime'],
-            props: {
-                format: 'YYYY-MM-DD',
-                valueFormat: 'YYYY-MM-DD',
-                startPlaceholder: '开始时间',
-                endPlaceholder: '结束时间',
-                onChange: (val) => {
-                    searchRef.value?.setData({
-                        startTime: val && val.length > 0 ? val[0] : '',
-                        endTime: val && val.length > 0 ? val[1] : ''
-                    });
-                }
-            }
-        }
-    ]);
-
-    const initKeys = reactive({
-        senderName: '',
-        senderPhone: '',
-        senderAddress: '',
-        startTime: '',
-        endTime: '',
-        dateRange: []
-    });
-
-    const searchRef = ref(null);
-    
-    const search = (data) => {
-        let params = JSON.parse(JSON.stringify(data));
-        delete params.dateRange;
-        emit('search', params);
-    };
-    
-    const reset = () => {
-        searchRef.value?.reset();
-    };
-
-    defineExpose({
-        reset,
-        setData: (data) => searchRef.value?.setData(data)
-    });
-</script>

+ 0 - 112
src/views/goods/list/components/sales-detail-modal.vue

@@ -1,112 +0,0 @@
-<!-- 销售明细弹窗 -->
-<template>
-    <ele-modal
-        v-model="visible"
-        title="销售明细"
-        :width="1200"
-        @open="handleDialogOpen"
-    >
-        <!-- Search Bar -->
-        <sales-detail-search ref="searchRef" @search="handleSearch" />
-
-        <common-table
-            ref="tableRef"
-            :pageConfig="pageConfig"
-            :columns="columns"
-            :tools="false"
-        >
-        </common-table>
-
-        <template #footer>
-            <el-button @click="visible = false">关闭</el-button>
-        </template>
-    </ele-modal>
-</template>
-
-<script setup>
-    import { ref, reactive, defineExpose, nextTick } from 'vue';
-    import CommonTable from '@/components/CommonPage/CommonTable.vue';
-    import SalesDetailSearch from './sales-detail-search.vue';
-
-    defineOptions({ name: 'SalesDetailModal' });
-
-    // 弹窗可见性
-    const visible = defineModel({ type: Boolean });
-
-    // 当前查看的图书信息
-    const currentBook = ref(null);
-    const title = ref('销售明细');
-
-    // 查询参数
-    const queryParams = reactive({
-        bookId: ''
-    });
-
-    // Search Component Ref
-    const searchRef = ref(null);
-
-    // 表格组件实例
-    const tableRef = ref(null);
-
-    // 表格列配置
-    const columns = ref([
-        { label: '订单编号', prop: 'orderNo', align: 'center' },
-        { label: '用户名', prop: 'userName', align: 'center' },
-        {
-            label: '单价',
-            prop: 'salePrice',
-            align: 'center',
-            formatter: (row) => '¥' + (row?.salePrice || 0)
-        },
-        { label: '数量', prop: 'saleCount', align: 'center' },
-        { label: '收货人', prop: 'receiverName', align: 'center' },
-        { label: '手机号', prop: 'receiverPhone', align: 'center' },
-        { label: '收货地址', prop: 'receiverAddress', align: 'center', minWidth: 150, showOverflowTooltip: true },
-        { label: '订单状态', prop: 'orderStatus', align: 'center' },
-        { label: '提交时间', prop: 'createTime', align: 'center', width: 160 }
-    ]);
-
-    // 页面配置
-    const pageConfig = reactive({
-        pageUrl: '/book/sale/detail/pagelist',
-        fileName: '销售明细',
-        cacheKey: 'sale-detail-data',
-        params: {
-            bookId: ''
-        }
-    });
-
-    // Search
-    function handleSearch(params) {
-        const finalParams = { ...params, bookId: queryParams.bookId };
-        tableRef.value?.reload(finalParams);
-    }
-
-    // 弹窗打开处理函数
-    function handleDialogOpen() {
-        nextTick(() => {
-            // Reset search form which should trigger a search/reload with default params
-            if (searchRef.value) {
-                searchRef.value.reset();
-            } else {
-                // Fallback if searchRef is not ready (though nextTick should help)
-                tableRef.value?.reload({ bookId: queryParams.bookId });
-            }
-        });
-    }
-
-    // 打开弹窗
-    function open(row) {
-        if (!row) return;
-        currentBook.value = row;
-        queryParams.bookId = row.id;
-        visible.value = true;
-    }
-
-    defineExpose({
-        open
-    });
-</script>
-
-<style scoped>
-</style>

+ 0 - 68
src/views/goods/list/components/sales-detail-search.vue

@@ -1,68 +0,0 @@
-<!-- 销售明细搜索表单 -->
-<template>
-    <ele-card :body-style="{ paddingBottom: '8px' }">
-        <ProSearch
-            :items="formItems"
-            ref="searchRef"
-            @search="search"
-            :initKeys="initKeys"
-        />
-    </ele-card>
-</template>
-
-<script setup>
-    import { reactive, ref, defineEmits } from 'vue';
-    import ProSearch from '@/components/CommonPage/ProSearch2.vue';
-
-    const emit = defineEmits(['search']);
-
-    const formItems = reactive([
-        { type: 'input', label: '收货人名称', prop: 'receiverName' },
-        { type: 'input', label: '收货人电话', prop: 'receiverPhone' },
-        { type: 'input', label: '收货人地址', prop: 'receiverAddress' },
-        {
-            type: 'daterange',
-            label: '时间',
-            prop: 'dateRange',
-            keys: ['startTime', 'endTime'],
-            props: {
-                format: 'YYYY-MM-DD',
-                valueFormat: 'YYYY-MM-DD',
-                startPlaceholder: '开始时间',
-                endPlaceholder: '结束时间',
-                onChange: (val) => {
-                    searchRef.value?.setData({
-                        startTime: val && val.length > 0 ? val[0] : '',
-                        endTime: val && val.length > 0 ? val[1] : ''
-                    });
-                }
-            }
-        }
-    ]);
-
-    const initKeys = reactive({
-        receiverName: '',
-        receiverPhone: '',
-        receiverAddress: '',
-        startTime: '',
-        endTime: '',
-        dateRange: []
-    });
-
-    const searchRef = ref(null);
-    
-    const search = (data) => {
-        let params = JSON.parse(JSON.stringify(data));
-        delete params.dateRange;
-        emit('search', params);
-    };
-    
-    const reset = () => {
-        searchRef.value?.reset();
-    };
-
-    defineExpose({
-        reset,
-        setData: (data) => searchRef.value?.setData(data)
-    });
-</script>

+ 269 - 327
src/views/goods/list/index.vue

@@ -1,386 +1,328 @@
 <template>
     <ele-page flex-table :bodyStyle="{ padding: '0 20px' }">
-        <goods-search ref="searchRef" @search="reload" />
+        <goods-search ref="searchRef" @search="handleSearch" />
 
         <div class="px-4 py-2 bg-white">
-            <el-tabs v-model="activeTab" @tab-change="handleTabChange" :head-style="{marginBottom: '0'}">
-                <el-tab-pane label="全部" name="all" />
-                <el-tab-pane label="出售中" name="on_sale" />
-                <el-tab-pane label="仓库中" name="in_warehouse" />
-                <el-tab-pane label="销售黑名单" name="blacklist" />
-                <el-tab-pane label="待上架列表" name="pending_listing" />
+            <el-tabs v-model="activeTab" type="card" @tab-change="handleTabChange" :head-style="{ marginBottom: '0' }">
+                <el-tab-pane label="全部" name="" />
+                <el-tab-pane label="出售中" name="1" />
+                <el-tab-pane label="已下架(仓库中)" name="2" />
+                <el-tab-pane label="销售黑名单" name="3" />
+                <el-tab-pane label="待上架列表" name="0" />
             </el-tabs>
         </div>
 
-        <common-table ref="tableRef" :pageConfig="pageConfig" :columns="columns" :tools="false" :datasource="mockDatasource" :bodyStyle="{paddingTop:0}">
+        <common-table ref="tableRef" :pageConfig="pageConfig" :columns="columns" :tools="false"
+            :bodyStyle="{ paddingTop: 0 }">
             <template #toolbar>
                 <div class="flex gap-2">
                     <el-button type="primary" plain @click="openNewBookModal">新建图书商品</el-button>
                     <el-button type="primary" plain @click="openNewOtherModal">新建其他商品</el-button>
                     <el-button type="danger" plain @click="openUpdatePriceModal">更新价格</el-button>
                     <el-button type="warning" plain @click="openListDelistModal">上架/下架</el-button>
-                    <el-button type="primary" plain v-if="activeTab === 'pending_listing'"
-                        @click="handleOneClickListing">一键上架</el-button>
                     <el-button color="#bd3124" plain @click="handleExport">导出</el-button>
                     <el-button type="warning" plain @click="handleOperationLog">操作日志</el-button>
                 </div>
             </template>
 
-            <!-- Columns Slots -->
+            <!-- 列插槽 -->
             <template #cover="{ row }">
-                <el-image style="width: 80px; height: 80px; border-radius: 4px" fit="cover"
-                    :src="row.cover || row.image" :preview-src-list="[row.cover || row.image]" preview-teleported />
+                <el-image style="width: 60px; height: 80px; border-radius: 4px" fit="cover" :src="row.cover"
+                    :preview-src-list="[row.cover]" preview-teleported>
+                    <template #error>
+                        <div class="flex items-center justify-center w-full h-full bg-gray-100 text-gray-400">
+                            <el-icon>
+                                <Picture />
+                            </el-icon>
+                        </div>
+                    </template>
+                </el-image>
             </template>
 
             <template #info="{ row }">
                 <goods-info :row="row" />
             </template>
 
+            <template #stock="{ row }">
+                <div class="text-left">
+                    <div>中等: {{ row.mediumNum || 0 }}</div>
+                    <div>良好: {{ row.goodNum || 0 }}</div>
+                    <div>次品: {{ row.badNum || 0 }}</div>
+                    <div>合计: {{ (row.mediumNum || 0) + (row.goodNum || 0) + (row.badNum || 0) }}</div>
+                </div>
+            </template>
+
             <template #price="{ row }">
                 <div class="flex items-center justify-center">
-                    <span>¥{{ row.price }}</span>
-                    <el-icon class="ml-1 cursor-pointer text-blue-500" @click="handleEditPrice(row)">
+                    <span>¥{{ row.productPrice }}</span>
+                    <el-icon class="ml-1 cursor-pointer" style="color:#409eef" @click="handleEditPrice(row)">
                         <EditPen />
                     </el-icon>
                 </div>
-                <div v-if="row.schedule" class="text-xs text-green-500 mt-1">
-                    谢程婧: {{ row.schedule }}
-                </div>
             </template>
 
-            <template #stock="{ row }">
-                <div class="text-xs text-left">
-                    <div>中等: {{ row.stockMedium || 0 }}</div>
-                    <div>良好: {{ row.stockGood || 0 }}</div>
-                    <div>次品: {{ row.stockDefective || 0 }}</div>
-                    <div>合计: {{ row.stockTotal || 0 }}</div>
-                </div>
+            <template #status="{ row }">
+                <el-tag v-if="row.sellStatus == '1'" type="success">出售中</el-tag>
+                <el-tag v-else-if="row.sellStatus == '0'" type="warning">待上架</el-tag>
+                <el-tag v-else-if="row.sellStatus == '2'" type="info">已下架</el-tag>
+                <el-tag v-else-if="row.sellStatus == '3'" type="danger">黑名单</el-tag>
+                <el-tag v-else type="warning">-</el-tag>
             </template>
 
             <template #action="{ row }">
-                <div class="flex flex-wrap gap-1 button-group">
-                    <!-- Common Actions -->
-                    <el-button type="success" size="small" @click="handleModify(row)">修改</el-button>
-
-                    <!-- Specific Actions based on Tab/Status -->
-                    <template v-if="activeTab === 'pending_listing'">
-                        <el-button type="primary" size="small" @click="handleListing(row)">上架</el-button>
-                        <el-button color="#e99d42" size="small" @click="handleRecycleLog(row)">回收日志</el-button>
-                        <el-button type="primary" size="small" @click="handlePriceLog(row)">售价日志</el-button>
-
-                        <el-button color="#f37607" size="small" @click="handleViewTaobao(row)">查看淘宝</el-button>
-                        <el-button color="#951d1d" size="small" @click="handleViewKongfz(row)">查看孔网</el-button>
-                        <el-button type="primary" size="small"
-                            @click="handleToggleRecycleList(row)">移除/加入回收书单</el-button>
-                        <el-button type="danger" size="small" @click="handleToggleRecycle(row)">暂停/开启回收</el-button>
-
-                        <el-button color="#7728f5" size="small"
-                            @click="handleSetIndependentParams(row)">设置独立参数</el-button>
-                        <el-button color="#333333" size="small" @click="handleToggleBlacklist(row)">加入/移除黑名单</el-button>
+                <div class="flex flex-wrap gap-2 button-group">
+                    <!-- 其他状态保持原有逻辑 -->
+                    <el-button type="warning" @click="handleSalesDetail(row)">销售明细</el-button>
+                    <el-button type="primary" plain @click="handleRecycleDetail(row)">回收明细</el-button>
+
+                    <template v-if="row.sellStatus == '1'">
+                        <el-button type="warning" @click="handleDelist(row)">下架</el-button>
                     </template>
 
-                    <template v-else>
-                        <el-button type="danger" size="small" @click="handlePriceLog(row)">售价日志</el-button>
-                        <el-button type="warning" size="small" @click="handleDelist(row)">下架</el-button>
-                        <el-button type="warning" size="small" @click="handleSalesDetail(row)">销售明细</el-button>
-                        <el-button type="danger" size="small" @click="handleRecycleDetail(row)">回收明细</el-button>
+                    <template v-else-if="row.sellStatus == '2'">
+                        <el-button type="success" @click="handleListing(row)">上架</el-button>
                     </template>
+
+                    <el-button v-if="row.sellStatus == '3'" type="danger"
+                        @click="handleToggleBlacklist(row, false)">移出黑名单</el-button>
+                    <el-button v-else color="#606066" @click="handleToggleBlacklist(row, true)">加入黑名单</el-button>
+                </div>
+            </template>
+
+            <template #others="{ row }">
+                <div class="flex flex-wrap gap-2">
+                    <!-- 待上架状态显示特定按钮组 -->
+                    <el-button color="#e99d42" @click="handleRecycleLog(row)"
+                        v-permission="'recycle:bookStat:recycleLog'">回收日志</el-button>
+                    <el-button color="#0f7dc7" @click="handleSalesLog(row)"
+                        v-permission="'recycle:bookStat:salesLog'">售价日志</el-button>
+                    <el-button color="#f27606" @click="handleViewUrl(row, 'tb')">查看淘宝</el-button>
+                    <el-button color="#951d1d" @click="handleViewUrl(row, 'kw')">查看孔网</el-button>
+
+                    <!-- 移除/加入回收书单 -->
+                    <el-button color="#4095e5" @click="handleOptBooklist('remove', row)"
+                        v-permission="'recycle:bookStat:removeBooklist'"
+                        v-if="row.recycleStatus && row.recycleStatus > 0">
+                        移除回收书单
+                    </el-button>
+                    <el-button color="#bd3124" @click="handleOptBooklist('add', row)"
+                        v-permission="'recycle:bookStat:addBooklist'" v-else>
+                        加入回收书单
+                    </el-button>
+
+                    <!-- 暂停/开启回收 -->
+                    <template v-if="row.recycleStatus && row.recycleStatus > 0">
+                        <el-button v-if="row.recycleStatus == 1" color="#bd3124" @click="handleOptRecycle('pause', row)"
+                            v-permission="'recycle:bookStat:pauseRecycle'">
+                            暂停回收
+                        </el-button>
+                        <el-button v-else color="#bd3124" @click="handleOptRecycle('start', row)"
+                            v-permission="'recycle:bookStat:startRecycle'">
+                            开启回收
+                        </el-button>
+                    </template>
+
+                    <el-button color="#7728f5" @click="handleSetParams(row)">设置独立参数</el-button>
                 </div>
             </template>
         </common-table>
 
-        <!-- Modals -->
-        <new-book-modal v-model="newBookVisible" @done="reload" />
+        <!-- 弹窗 -->
+        <new-book-modal ref="newBookModalRef" @done="reload" />
+        <edit-price-modal ref="editPriceModalRef" @done="reload" />
+        <sales-detail-modal ref="salesDetailModalRef" />
+        <recycle-detail-modal ref="recycleDetailModalRef" />
+        <operation-log-modal ref="operationLogModalRef" />
 
-        <common-import-modal v-model="updatePriceVisible" title="更新价格" template-name="更新价格模板"
-            template-url="https://example.com/price_template.xlsx" upload-url="/goods/price/import"
-            instruction-title="更新规则" :instructions="[
+        <common-import-modal ref="updatePriceModalRef" title="更新价格" template-name="更新价格模板"
+            template-url="https://shuhi.oss-cn-qingdao.aliyuncs.com/default/shop_book_price_import.xlsx"
+            upload-url="/shop/shopbook/importPrice" instruction-title="更新规则" :instructions="[
                 '文件ISBN不存在或者错误,会自动过滤掉',
                 '导入文件第一行需与模版完全一致'
             ]" @done="reload" />
 
-        <common-import-modal v-model="listDelistVisible" title="上架/下架" template-name="上架/下架模板"
-            template-url="https://example.com/list_delist_template.xlsx" upload-url="/goods/status/import"
+        <common-import-modal ref="listDelistModalRef" title="批量上/下架" template-name="上架/下架模板"
+            template-url="https://example.com/list_delist_template.xlsx" upload-url="/shop/shopbook/batchShelf"
             instruction-title="上架/下架规则" :instructions="[
                 '文件ISBN不存在或者错误,会自动过滤掉',
                 '导入文件第一行需与模版完全一致'
             ]" @done="reload" />
 
-        <edit-price-modal ref="editPriceModalRef" v-model="editPriceVisible" @done="reload" />
-
-        <sales-detail-modal ref="salesDetailModalRef" v-model="salesDetailVisible" />
-        
-        <recycle-detail-modal ref="recycleDetailModalRef" v-model="recycleDetailVisible" />
-
-        <operation-log-modal ref="operationLogModalRef" v-model="operationLogVisible" />
-
+        <books-edit ref="booksEditRef" @success="reload" />
+        <set-params ref="paramsRef" @refresh="reload" />
+        <order-recycle-log ref="recycleLogRef" />
+        <order-sales-log ref="salesLogRef" />
     </ele-page>
 </template>
 
 <script setup>
-import { ref, reactive, computed } from 'vue';
-import CommonTable from '@/components/CommonPage/CommonTable.vue';
-import GoodsSearch from './components/goods-search.vue';
-import GoodsInfo from './components/goods-info.vue';
-import NewBookModal from './components/new-book-modal.vue';
-import CommonImportModal from './components/common-import-modal.vue';
-import EditPriceModal from './components/edit-price-modal.vue';
-import SalesDetailModal from './components/sales-detail-modal.vue';
-import RecycleDetailModal from './components/recycle-detail-modal.vue';
-import OperationLogModal from './components/operation-log-modal.vue';
-import { EditPen } from '@element-plus/icons-vue';
-import { EleMessage } from 'ele-admin-plus/es';
-
-defineOptions({ name: 'GoodsList' });
-
-// State
-const activeTab = ref('all');
-const tableRef = ref(null);
-const searchRef = ref(null);
-
-// Modals State
-const newBookVisible = ref(false);
-const updatePriceVisible = ref(false);
-const listDelistVisible = ref(false);
-const editPriceVisible = ref(false);
-const salesDetailVisible = ref(false);
-const recycleDetailVisible = ref(false);
-const operationLogVisible = ref(false);
-
-const editPriceModalRef = ref(null);
-const salesDetailModalRef = ref(null);
-const recycleDetailModalRef = ref(null);
-const operationLogModalRef = ref(null);
-
-// Page Config
-const pageConfig = reactive({
-    pageUrl: '/goods/list', // Mock URL
-    fileName: '商品列表',
-    cacheKey: 'goods-list-data',
-    params: {
-        status: 'all'
-    }
-});
-
-// Mock Datasource
-const mockDatasource = ({ page, limit, where }) => {
-    // Simulate API delay
-    return new Promise((resolve) => {
-        setTimeout(() => {
-            const list = [
-                {
-                    id: 1,
-                    cover: 'https://img3.doubanio.com/view/subject/s/public/s34049753.jpg',
-                    title: 'Vue.js设计与实现',
-                    author: '霍春阳',
-                    isbn: '9787115583648',
-                    publisher: '人民邮电出版社',
-                    productType: '图书',
-                    price: 89.00,
-                    stock: 100,
-                    stockMedium: 50,
-                    stockGood: 30,
-                    stockDefective: 20,
-                    stockTotal: 100,
-                    salesVolume: 500,
-                    listingTime: '2023-01-01',
-                    status: 'on_sale'
-                },
-                {
-                    id: 2,
-                    cover: 'https://img9.doubanio.com/view/subject/s/public/s29653655.jpg',
-                    title: '深入浅出Node.js',
-                    author: '朴灵',
-                    isbn: '9787115323562',
-                    publisher: '人民邮电出版社',
-                    productType: '图书',
-                    price: 69.00,
-                    stock: 0,
-                    stockMedium: 0,
-                    stockGood: 0,
-                    stockDefective: 0,
-                    stockTotal: 0,
-                    salesVolume: 1200,
-                    listingTime: '2022-05-20',
-                    status: 'in_warehouse'
-                },
-                {
-                    id: 3,
-                    cover: 'https://img1.doubanio.com/view/subject/s/public/s28359307.jpg',
-                    title: 'JavaScript高级程序设计',
-                    author: '马特·弗里斯比',
-                    isbn: '9787115545381',
-                    publisher: '人民邮电出版社',
-                    productType: '图书',
-                    price: 99.00,
-                    stock: 50,
-                    stockMedium: 20,
-                    stockGood: 20,
-                    stockDefective: 10,
-                    stockTotal: 50,
-                    salesVolume: 300,
-                    listingTime: '2023-06-01',
-                    status: 'pending_listing'
-                },
-                {
-                    id: 4,
-                    cover: 'https://img2.doubanio.com/view/subject/s/public/s34049753.jpg',
-                    title: 'CSS世界',
-                    author: '张鑫旭',
-                    isbn: '9787115472199',
-                    publisher: '人民邮电出版社',
-                    productType: '图书',
-                    price: 59.00,
-                    stock: 20,
-                    stockMedium: 10,
-                    stockGood: 5,
-                    stockDefective: 5,
-                    stockTotal: 20,
-                    salesVolume: 150,
-                    listingTime: '2021-12-12',
-                    status: 'blacklist'
-                }
-            ];
-
-            // Filter by tab status (simulated)
-            let filteredList = list;
-            if (activeTab.value !== 'all') {
-                if (activeTab.value === 'pending_listing') {
-                     // For demo purposes, let's make sure we have data for pending_listing
-                     // Or just filter by status if it matches
-                     filteredList = list.filter(item => item.status === activeTab.value);
-                     // If empty for demo, just show item 3
-                     if (filteredList.length === 0) filteredList = [list[2]];
-                } else {
-                     filteredList = list.filter(item => item.status === activeTab.value);
-                }
-            }
-
-            resolve({
-                code: 0,
-                msg: 'success',
-                count: list.length,
-                data: filteredList
-            });
-        }, 500);
+    import { ref, reactive } from 'vue';
+    import CommonTable from '@/components/CommonPage/CommonTable.vue';
+    import CommonImportModal from './components/common-import-modal.vue';
+    import GoodsSearch from './components/goods-search.vue';
+    import GoodsInfo from './components/goods-info.vue';
+    import NewBookModal from './components/new-book-modal.vue';
+    import EditPriceModal from './components/edit-price-modal.vue';
+    import SalesDetailModal from '@/views/recycle/inventory/components/sale-detail.vue';
+    import RecycleDetailModal from '@/views/recycle/inventory/components/recycle-detail.vue';
+    import OperationLogModal from './components/operation-log-modal.vue';
+    import BooksEdit from '@/views/data/books/components/books-edit.vue';
+    import SetParams from '@/views/recycle/components/set-params.vue';
+    import OrderRecycleLog from '@/views/recycleOrder/detail/order-recycle-log.vue';
+    import OrderSalesLog from '@/views/recycleOrder/detail/order-sales-log.vue';
+    import { EditPen, Picture } from '@element-plus/icons-vue';
+    import { EleMessage } from 'ele-admin-plus/es';
+    import request from '@/utils/request';
+    import { useBookOperation } from '@/utils/use-book-operation';
+
+    defineOptions({ name: 'GoodsList' });
+
+    // 状态
+    const activeTab = ref('');
+    const tableRef = ref(null);
+    const searchRef = ref(null);
+    const searchParams = ref({});
+
+    // 弹窗状态
+    const newBookModalRef = ref(null);
+    const editPriceModalRef = ref(null);
+    const salesDetailModalRef = ref(null);
+    const recycleDetailModalRef = ref(null);
+    const operationLogModalRef = ref(null);
+    const updatePriceModalRef = ref(null);
+    const listDelistModalRef = ref(null);
+    const booksEditRef = ref(null);
+    // const setParamsRef = ref(null);
+    const recycleLogRef = ref(null);
+    const salesLogRef = ref(null);
+
+    // Page Config
+    const pageConfig = reactive({
+        pageUrl: '/shop/shopbook/shopPageList',
+        exportUrl: '/shop/shopbook/export',
+        fileName: '商品列表',
+        cacheKey: 'goods-list-data',
+        params: {
+            sellStatus: ''
+        }
     });
-};
-
-// Columns
-const columns = computed(() => [
-    { type: 'selection', width: 50, align: 'center', fixed: 'left' },
-    { label: '商品图', prop: 'cover', width: 100, slot: 'cover', align: 'center' },
-    { label: '商品名称', prop: 'info', minWidth: 200, slot: 'info' },
-    { label: '商品类型', prop: 'productType', width: 100, align: 'center' },
-    { label: '售价', prop: 'price', width: 120, slot: 'price', align: 'center', sortable: 'custom' },
-    { label: '库存', prop: 'stock', width: 150, slot: 'stock', align: 'center', sortable: 'custom' },
-    { label: '销量', prop: 'salesVolume', width: 100, align: 'center', sortable: 'custom' },
-    { label: '上架时间', prop: 'listingTime', width: 160, align: 'center', sortable: 'custom' },
-    { label: '操作', prop: 'action', width: 280, slot: 'action', align: 'center', fixed: 'right' }
-]);
-
-// Methods
-const reload = (params = {}) => {
-    tableRef.value?.reload(params);
-};
-
-const handleTabChange = (name) => {
-    pageConfig.params.status = name;
-    reload();
-};
-
-// Modal Openers
-const openNewBookModal = () => {
-    newBookVisible.value = true;
-};
-
-const openNewOtherModal = () => {
-    EleMessage.info('功能开发中...');
-};
-
-const openUpdatePriceModal = () => {
-    updatePriceVisible.value = true;
-};
-
-const openListDelistModal = () => {
-    listDelistVisible.value = true;
-};
-
-const handleEditPrice = (row) => {
-    editPriceVisible.value = true;
-    editPriceModalRef.value?.open(row);
-};
-
-// Actions
-const handleOneClickListing = () => {
-    EleMessage.success('一键上架指令已发送');
-};
-
-const handleExport = () => {
-    EleMessage.success('开始导出...');
-};
-
-const handleOperationLog = () => {
-    operationLogVisible.value = true;
-    operationLogModalRef.value?.open();
-};
-
-const handleModify = (row) => {
-    EleMessage.info(`修改商品: ${row.title}`);
-};
-
-const handlePriceLog = (row) => {
-    EleMessage.info(`查看售价日志: ${row.title}`);
-};
-
-const handleDelist = (row) => {
-    EleMessage.warning(`下架商品: ${row.title}`);
-};
-
-const handleListing = (row) => {
-    EleMessage.success(`上架商品: ${row.title}`);
-};
-
-const handleSalesDetail = (row) => {
-    salesDetailVisible.value = true;
-    salesDetailModalRef.value?.open(row);
-};
-
-const handleRecycleDetail = (row) => {
-    recycleDetailVisible.value = true;
-    recycleDetailModalRef.value?.open(row);
-};
-
-const handleRecycleLog = (row) => {
-    EleMessage.info(`回收日志: ${row.title}`);
-};
-
-const handleViewTaobao = (row) => {
-    window.open(`https://s.taobao.com/search?q=${row.isbn}`, '_blank');
-};
-
-const handleViewKongfz = (row) => {
-    window.open(`https://search.kongfz.com/product_result/?key=${row.isbn}`, '_blank');
-};
-
-const handleToggleRecycleList = (row) => {
-    EleMessage.info('切换回收书单状态');
-};
-
-const handleToggleRecycle = (row) => {
-    EleMessage.info('切换回收状态');
-};
-
-const handleSetIndependentParams = (row) => {
-    EleMessage.info('设置独立参数');
-};
-
-const handleToggleBlacklist = (row) => {
-    EleMessage.info('切换黑名单状态');
-};
 
+    const columns = ref([
+        { label: '封面', prop: 'cover', slot: 'cover', width: 100, align: 'center' },
+        { label: '商品信息', prop: 'info', slot: 'info', minWidth: 200 },
+        { label: '库存', prop: 'stock', slot: 'stock', width: 150, align: 'center' },
+        { label: '售价', prop: 'productPrice', slot: 'price', width: 120, align: 'center' },
+        { label: '销量', prop: 'salesNum', width: 100, align: 'center' },
+        { label: '状态', prop: 'sellStatus', slot: 'status', width: 100, align: 'center' },
+        { label: '上架时间', prop: 'onSaleTime', width: 160, align: 'center' },
+        { columnKey: 'action', label: '操作', width: 220, align: 'center', slot: 'action', fixed: 'right' },
+        { columnKey: 'others', label: '其他操作', minWidth: 160, align: 'center', slot: 'others', fixed: 'right' }
+    ]);
+
+    // 方法
+    const reload = (where) => {
+        tableRef.value?.reload(where);
+    };
+
+    // 图书操作 Hook
+    const {
+        paramsRef,
+        handleOptBooklist,
+        handleOptRecycle,
+        handleSetParams,
+        handleViewUrl
+    } = useBookOperation(tableRef);
+
+    const handleRecycleLog = (row) => {
+        recycleLogRef.value?.handleOpen(row);
+    };
+
+    const handleSalesLog = (row) => {
+        salesLogRef.value?.handleOpen(row);
+    };
+
+    const handleSearch = (where) => {
+        searchParams.value = where;
+        reload(where);
+    };
+
+    const handleTabChange = (name) => {
+        let status = name;
+        // 直接更新参数
+        pageConfig.params.sellStatus = status;
+        reload();
+    };
+
+    // 操作
+    const openNewBookModal = () => {
+        newBookModalRef.value?.open();
+    };
+
+    const openUpdatePriceModal = () => {
+        updatePriceModalRef.value?.open();
+    };
+
+    const openListDelistModal = () => {
+        listDelistModalRef.value?.open();
+    };
+
+    const openNewOtherModal = () => {
+        EleMessage.info('开发中');
+    };
+
+    const handleEditPrice = (row) => {
+        editPriceModalRef.value?.open(row);
+    };
+
+    const setShelfStatus = async (row, status, successMsg) => {
+        try {
+            await request.post('/shop/shopbook/setShelfStatus', {
+                isbn: row.isbn,
+                sellStatus: status
+            });
+            EleMessage.success(successMsg);
+            reload();
+        } catch (e) {
+            EleMessage.error(e.message || '操作失败');
+        }
+    };
+
+    const handleDelist = (row) => {
+        tableRef.value?.messageBoxConfirm({
+            message: '确认下架该商品吗?',
+            fetch: () => request.post('/shop/shopbook/setShelfStatus', { isbn: row.isbn, sellStatus: '2' }),
+            successMessage: '下架成功'
+        });
+    };
+
+    const handleListing = (row) => {
+        setShelfStatus(row, '1', '上架成功');
+    };
+
+    const handleToggleBlacklist = (row, isAdd) => {
+        const status = isAdd ? '3' : '2'; // Remove from blacklist -> In Warehouse
+        const msg = isAdd ? '确认加入黑名单?' : '确认移出黑名单?';
+
+        tableRef.value?.messageBoxConfirm({
+            message: msg,
+            fetch: () => request.post('/shop/shopbook/setShelfStatus', { isbn: row.isbn, sellStatus: status }),
+            successMessage: '操作成功'
+        });
+    };
+
+    const handleExport = () => {
+        tableRef.value?.exportData('商品列表');
+    };
+
+    const handleOperationLog = () => {
+        operationLogModalRef.value?.open();
+    };
+
+    const handleSalesDetail = (row) => {
+        salesDetailModalRef.value?.handleOpen(row);
+    };
+
+    const handleRecycleDetail = (row) => {
+        recycleDetailModalRef.value?.handleOpen(row);
+    };
 </script>
-
-<style scoped>
-/* Custom styles if needed */
-</style>