ソースを参照

激活码功能开发和接口对接

ylong 4 ヶ月 前
コミット
a5da632ec7

+ 4 - 1
src/components/CommonPage/CommonTable.vue

@@ -29,7 +29,7 @@ let props = defineProps({
             rowKey: "id",
         }),
     },
-    pageUrl: { type: String, default: "/system/post/list" },
+    pageUrl: { type: String, default: "" },
     columns: { type: Array, default: () => [] },
     bodyStyle: { type: Object, default: () => ({}) },
     flexTable: { type: Boolean, default: true },
@@ -44,6 +44,9 @@ const selections = ref([]);
 
 async function queryPage(params) {
     let url = props.pageConfig.pageUrl || props.pageUrl;
+    if (!url) {
+        return Promise.reject(new Error("请配置页面URL"));
+    }
     const res = await proxy.$http.get(url, { params });
     if (res.data.code === 200) {
         return res.data;

+ 173 - 0
src/views/code/list/components/activation-out-stock-modal.vue

@@ -0,0 +1,173 @@
+<!-- 激活码出库单弹窗 -->
+<template>
+    <ele-modal form :width="600" v-model="visible" title="激活码出库单" @closed="handleClosed">
+        <div class="out-stock-info">
+            <div class="info-row">
+                <span class="label">ISBN:</span>
+                <span class="value">{{ outStockData.isbn || '-' }}</span>
+            </div>
+            
+            <div class="info-row">
+                <span class="label">书名:</span>
+                <span class="value">{{ outStockData.bookName || '-' }}</span>
+            </div>
+            
+            <div class="info-row">
+                <span class="label">数量:</span>
+                <span class="value">{{ outStockData.num || '-' }}</span>
+            </div>
+            
+            <div class="info-row">
+                <span class="label">链接:</span>
+                <span class="value link-text">{{ outStockData.urlLink || '-' }}</span>
+            </div>
+            
+            <div class="info-row">
+                <span class="label">提取码:</span>
+                <span class="value">{{ outStockData.extCode || '-' }}</span>
+            </div>
+            
+            <div class="info-row">
+                <span class="label">备注:</span>
+                <span class="value">{{ outStockData.remark || '-' }}</span>
+            </div>
+        </div>
+
+        <template #footer>
+            <el-button @click="handleCancel">取消</el-button>
+            <el-button type="primary" @click="handleCopyAll">
+                复制全部
+            </el-button>
+        </template>
+    </ele-modal>
+</template>
+
+<script setup>
+    import { ref, reactive } from 'vue';
+    import { EleMessage } from 'ele-admin-plus/es';
+
+    /** 弹窗是否打开 */
+    const visible = defineModel({ type: Boolean });
+
+    /** 出库数据 */
+    const outStockData = reactive({
+        isbn: '',
+        bookName: '',
+        num: '',
+        urlLink: '',
+        extCode: '',
+        remark: ''
+    });
+
+    /** 关闭弹窗 */
+    const handleCancel = () => {
+        visible.value = false;
+    };
+
+    /** 弹窗关闭事件 */
+    const handleClosed = () => {
+        // 重置数据
+        Object.assign(outStockData, {
+            isbn: '',
+            bookName: '',
+            num: '',
+            urlLink: '',
+            extCode: '',
+            remark: ''
+        });
+    };
+
+    /** 复制全部信息 */
+    const handleCopyAll = async () => {
+        try {
+            // 格式化复制内容
+            const copyText = `ISBN: ${outStockData.isbn || '-'}
+书名: ${outStockData.bookName || '-'}
+数量: ${outStockData.num || '-'}
+链接: ${outStockData.urlLink || '-'}
+提取码: ${outStockData.extCode || '-'}
+备注: ${outStockData.remark || '-'}`;
+
+            // 使用 Clipboard API 复制到剪贴板
+            if (navigator.clipboard && window.isSecureContext) {
+                await navigator.clipboard.writeText(copyText);
+            } else {
+                // 降级方案:使用 document.execCommand
+                const textArea = document.createElement('textarea');
+                textArea.value = copyText;
+                textArea.style.position = 'fixed';
+                textArea.style.left = '-999999px';
+                textArea.style.top = '-999999px';
+                document.body.appendChild(textArea);
+                textArea.focus();
+                textArea.select();
+                document.execCommand('copy');
+                textArea.remove();
+            }
+            
+            EleMessage.success('复制成功');
+        } catch (error) {
+            console.error('复制失败:', error);
+            EleMessage.error('复制失败,请手动复制');
+        }
+    };
+
+    /** 弹窗打开事件 */
+    const handleOpen = (data) => {
+        if (data) {
+            Object.assign(outStockData, {
+                isbn: data.isbn || '',
+                bookName: data.bookName || '',
+                num: data.num || '',
+                urlLink: data.urlLink || '',
+                extCode: data.extCode || '',
+                remark: data.remark || ''
+            });
+            visible.value = true;
+        }
+    };
+
+    defineExpose({
+        handleOpen
+    });
+</script>
+
+<style scoped>
+.out-stock-info {
+    padding: 20px 0;
+    padding-left: 50px;
+}
+
+.info-row {
+    display: flex;
+    align-items: flex-start;
+    margin-bottom: 16px;
+    line-height: 1.5;
+}
+
+.info-row:last-child {
+    margin-bottom: 0;
+}
+
+.label {
+    width: 80px;
+    font-weight: 500;
+    color: #333;
+    flex-shrink: 0;
+}
+
+.value {
+    flex: 1;
+    color: #666;
+    word-break: break-all;
+}
+
+.link-text {
+    color: #1890ff;
+    cursor: pointer;
+}
+
+.link-text:hover {
+    text-decoration: underline;
+}
+</style>

+ 37 - 0
src/views/code/list/components/code-search.vue

@@ -0,0 +1,37 @@
+<!-- 搜索表单 -->
+<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, getCurrentInstance, computed } from 'vue';
+    import ProSearch from '@/components/CommonPage/ProSearch2.vue';
+
+    const { proxy } = getCurrentInstance();
+    const emit = defineEmits(['search']);
+
+    const formItems = computed(() => {
+        return [
+            { type: 'input', label: 'ISBN', prop: 'isbn' },
+            { type: 'input', label: '书名', prop: 'bookName' }
+        ];
+    });
+
+    const initKeys = reactive({
+        isbn: '',
+        bookName: ''
+    });
+
+    const searchRef = ref(null);
+    /** 搜索 */
+    const search = (data) => {
+        emit('search', { ...data });
+    };
+</script>

+ 92 - 0
src/views/code/list/components/in-detail-modal.vue

@@ -0,0 +1,92 @@
+<!-- 入库明细弹窗 -->
+<template>
+    <ele-modal form :width="1200" v-model="visible" title="入库明细">
+        <common-table
+            ref="tableRef"
+            :pageConfig="pageConfig"
+            :columns="columns"
+            :tools="false"
+            :bodyStyle="{ padding: 0 }"
+        />
+
+        <template #footer>
+            <el-button @click="handleCancel">关闭</el-button>
+        </template>
+    </ele-modal>
+</template>
+
+<script setup>
+    import { ref, reactive, watch, nextTick } from 'vue';
+    import CommonTable from '@/components/CommonPage/CommonTable.vue';
+
+    /** 弹窗是否打开 */
+    const visible = defineModel({ type: Boolean });
+
+    /** 当前ISBN */
+    const currentIsbn = ref('');
+
+    /** 页面组件实例 */
+    const tableRef = ref(null);
+
+    /** 页面配置 */
+    const pageConfig = reactive({
+        pageUrl: '',
+        fileName: '入库明细',
+        cacheKey: 'inDetailTable',
+    });
+
+    /** 表格列配置 */
+    const columns = ref([
+        { label: 'ISBN', prop: 'isbn', width: 140, align: 'center' },
+        { label: '书名', prop: 'bookName', minWidth: 200, align: 'left' },
+        { label: '数量', prop: 'num', width: 80, align: 'center', formatter: (row) => 1 },
+        { 
+            label: '状态', 
+            prop: 'status', 
+            width: 100, 
+            align: 'center',
+            formatter: (row) => {
+                const statusMap = {
+                    1: '已入库',
+                    2: '已作废'
+                };
+                return statusMap[row.status] || '未知';
+            }
+        },
+        { label: '操作人', prop: 'operatorName', width: 120, align: 'center' },
+        { label: '描述', prop: 'remark', minWidth: 150, align: 'left' },
+        { label: '操作时间', prop: 'operatorTime', width: 160, align: 'center' }
+    ]);
+
+    /** 关闭弹窗 */
+    const handleCancel = () => {
+        visible.value = false;
+    };
+
+    /** 弹窗打开事件 */
+    const handleOpen = (isbn) => {
+        if (isbn) {
+            currentIsbn.value = isbn;
+            visible.value = true;
+            // 设置页面URL参数
+            pageConfig.pageUrl = `/activation/bookActivationInfo/pageInLogs/${isbn}`;
+            
+            nextTick(() => {
+                // 重新加载数据
+                tableRef.value?.reload();
+            });
+        }
+    };
+
+    /** 监听弹窗关闭,清理数据 */
+    watch(visible, (newVal) => {
+        if (!newVal) {
+            currentIsbn.value = '';
+            pageConfig.params = {};
+        }
+    });
+
+    defineExpose({
+        handleOpen
+    });
+</script>

+ 96 - 0
src/views/code/list/components/out-detail-modal.vue

@@ -0,0 +1,96 @@
+<!-- 出库明细弹窗 -->
+<template>
+    <ele-modal form :width="1200" v-model="visible" title="出库明细">
+        <common-table ref="tableRef" :pageConfig="pageConfig" :columns="columns" :tools="false"
+            :bodyStyle="{ padding: 0 }" />
+
+        <template #footer>
+            <el-button @click="handleCancel">关闭</el-button>
+        </template>
+    </ele-modal>
+</template>
+
+<script setup>
+import { ref, reactive, watch } from 'vue';
+import CommonTable from '@/components/CommonPage/CommonTable.vue';
+
+/** 弹窗是否打开 */
+const visible = defineModel({ type: Boolean });
+
+/** 当前ISBN */
+const currentIsbn = ref('');
+
+/** 页面组件实例 */
+const tableRef = ref(null);
+
+/** 页面配置 */
+const pageConfig = reactive({
+    pageUrl: '/activation/bookActivationInfo/pageOutLogs',
+    fileName: '出库明细',
+    cacheKey: 'outDetailTable',
+    params: {}
+});
+
+/** 表格列配置 */
+const columns = ref([
+    { label: 'ISBN', prop: 'isbn', width: 140, align: 'center' },
+    { label: '书名', prop: 'bookName', minWidth: 200, align: 'left' },
+    { label: '数量', prop: 'num', width: 80, align: 'center' },
+    { label: '金额', prop: 'totalPrice', width: 100, align: 'center' },
+    {
+        label: '状态',
+        prop: 'status',
+        width: 100,
+        align: 'center',
+        formatter: (row) => {
+            const statusMap = {
+                1: '已出库',
+                2: '已作废'
+            };
+            return statusMap[row.status] || '未知';
+        }
+    },
+    { label: '操作人', prop: 'operatorName', width: 120, align: 'center' },
+    { label: '描述', prop: 'remark', minWidth: 150, align: 'left' },
+    {
+        label: '使用次数',
+        prop: 'useTimes',
+        width: 100,
+        align: 'center',
+        formatter: (row) => {
+            return row.useTimes === 0 ? '未使用' : row.useTimes;
+        }
+    },
+    { label: '操作时间', prop: 'operatorTime', width: 160, align: 'center' }
+]);
+
+/** 关闭弹窗 */
+const handleCancel = () => {
+    visible.value = false;
+};
+
+/** 弹窗打开事件 */
+const handleOpen = (isbn) => {
+    if (isbn) {
+        pageConfig.pageUrl = '/activation/bookActivationInfo/pageOutLogs/' + isbn;
+        currentIsbn.value = isbn;
+        visible.value = true;
+        // 设置查询参数
+        pageConfig.params = { isbn };
+        // 重新加载数据
+        tableRef.value?.reload();
+    }
+};
+
+/** 监听弹窗关闭,清理数据 */
+watch(visible, (newVal) => {
+    if (!newVal) {
+        currentIsbn.value = '';
+        pageConfig.params = {};
+    }
+});
+
+defineExpose({
+    handleOpen
+});
+</script>

+ 162 - 0
src/views/code/list/components/out-stock-modal.vue

@@ -0,0 +1,162 @@
+<!-- 出库操作弹窗 -->
+<template>
+    <ele-modal form :width="600" v-model="visible" title="出库" @closed="handleClosed">
+        <el-form ref="formRef" :model="formData" :rules="rules" label-width="80px">
+            <el-form-item label="ISBN" prop="isbn">
+                <el-input v-model="formData.isbn" placeholder="请输入ISBN" readonly />
+            </el-form-item>
+
+            <el-form-item label="书名" prop="bookName">
+                <el-input v-model="formData.bookName" placeholder="请输入书名" readonly />
+            </el-form-item>
+
+            <el-form-item label="数量" prop="num">
+                <el-input-number v-model="formData.num" :min="1" :max="9999" placeholder="请输入数量" style="width: 100%" />
+            </el-form-item>
+
+            <el-form-item label="单价" prop="price">
+                <el-input-number v-model="formData.price" :min="0" :precision="2" placeholder="请输入单价"
+                    style="width: 100%" />
+            </el-form-item>
+
+            <el-form-item label="备注" prop="remark">
+                <el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="请输入备注" />
+            </el-form-item>
+        </el-form>
+
+        <template #footer>
+            <el-button @click="handleCancel">取消</el-button>
+            <el-button type="success" :loading="loading" @click="handleGenerateLink">
+                生成链接
+            </el-button>
+        </template>
+    </ele-modal>
+</template>
+
+<script setup>
+import { ref, reactive, watch } from 'vue';
+import { EleMessage } from 'ele-admin-plus/es';
+import request from '@/utils/request';
+
+/** 弹窗是否打开 */
+const visible = defineModel({ type: Boolean });
+
+/** 表单引用 */
+const formRef = ref(null);
+
+/** 加载状态 */
+const loading = ref(false);
+
+/** 表单数据 */
+const formData = reactive({
+    isbn: '',
+    bookName: '',
+    num: 1,
+    price: 0,
+    remark: ''
+});
+
+/** 表单验证规则 */
+const rules = reactive({
+    num: [
+        { required: true, message: '请输入数量', trigger: 'blur' },
+        { type: 'number', min: 1, message: '数量必须大于0', trigger: 'blur' }
+    ],
+    price: [
+        { required: true, message: '请输入单价', trigger: 'blur' },
+        { type: 'number', min: 0, message: '单价不能小于0', trigger: 'blur' }
+    ]
+});
+
+/** 当前书籍信息 */
+const currentBook = ref(null);
+
+/** 出库结果数据 */
+const outStockResult = ref(null);
+
+/** 关闭弹窗 */
+const handleCancel = () => {
+    visible.value = false;
+};
+
+/** 弹窗关闭事件 */
+const handleClosed = () => {
+    // 重置表单
+    formRef.value?.resetFields();
+    Object.assign(formData, {
+        isbn: '',
+        bookName: '',
+        num: 1,
+        price: 0,
+        remark: ''
+    });
+    currentBook.value = null;
+    outStockResult.value = null;
+    loading.value = false;
+};
+
+/** 生成链接 */
+const handleGenerateLink = async () => {
+    try {
+        // 表单验证
+        await formRef.value?.validate();
+
+        loading.value = true;
+
+        // 调用出库接口
+        const response = await request.post('/activation/bookActivationInfo/outStock', {
+            isbn: formData.isbn,
+            num: formData.num,
+            price: formData.price,
+            remark: formData.remark
+        });
+
+        if (response.data.code === 200) {
+            outStockResult.value = response.data.data;
+            EleMessage.success('出库成功');
+
+            // 关闭当前弹窗
+            visible.value = false;
+
+            // 触发打开激活码出库单弹窗
+            emit('openActivationModal', {
+                ...formData,
+                ...response.data.data
+            });
+        } else {
+            EleMessage.error(response.msg || '出库失败');
+        }
+    } catch (error) {
+        console.error('出库失败:', error);
+        EleMessage.error('出库失败,请重试');
+    } finally {
+        loading.value = false;
+    }
+};
+
+/** 弹窗打开事件 */
+const handleOpen = (bookInfo) => {
+    if (bookInfo) {
+        currentBook.value = bookInfo;
+        formData.isbn = bookInfo.isbn || '';
+        formData.bookName = bookInfo.bookName || '';
+        formData.num = 1;
+        formData.price = bookInfo.wholesalePrice || 0;
+        formData.remark = '';
+        visible.value = true;
+    }
+};
+
+/** 定义事件 */
+const emit = defineEmits(['openActivationModal']);
+
+defineExpose({
+    handleOpen
+});
+</script>
+
+<style scoped>
+.el-form-item {
+    margin-bottom: 20px;
+}
+</style>

+ 165 - 0
src/views/code/list/components/price-edit-modal.vue

@@ -0,0 +1,165 @@
+<!-- 价格编辑弹窗 -->
+<template>
+    <ele-modal form :width="500" v-model="visible" :title="modalTitle" @closed="handleClosed">
+        <el-form ref="formRef" :model="form" :rules="rules" label-width="80px">
+            <el-form-item label="ISBN" prop="isbn">
+                <el-input v-model="form.isbn" placeholder="请输入ISBN" disabled />
+            </el-form-item>
+
+            <el-form-item label="书名" prop="bookName">
+                <el-input v-model="form.bookName" placeholder="书名" disabled />
+            </el-form-item>
+
+            <!-- 零售价编辑 -->
+            <el-form-item v-if="priceType === 'retail'" label="零售价" prop="retailPrice">
+                <el-input-number v-model="form.retailPrice" :min="0" :precision="2" :step="0.01" placeholder="请输入零售价"
+                    style="width: 100%" />
+            </el-form-item>
+
+            <!-- 批发价编辑 -->
+            <el-form-item v-if="priceType === 'wholesale'" label="批发价" prop="wholesalePrice">
+                <el-input-number v-model="form.wholesalePrice" :min="0" :precision="2" :step="0.01" placeholder="请输入批发价"
+                    style="width: 100%" />
+            </el-form-item>
+        </el-form>
+
+        <template #footer>
+            <el-button @click="handleCancel">取消</el-button>
+            <el-button type="primary" :loading="loading" @click="handleSubmit">
+                保存
+            </el-button>
+        </template>
+    </ele-modal>
+</template>
+
+<script setup>
+import { ref, reactive, computed } from 'vue';
+import { EleMessage } from 'ele-admin-plus/es';
+import request from '@/utils/request';
+
+/** 弹窗是否打开 */
+const visible = defineModel({ type: Boolean });
+
+/** 表单引用 */
+const formRef = ref(null);
+
+/** 加载状态 */
+const loading = ref(false);
+
+/** 价格类型 */
+const priceType = ref('retail'); // 'retail' 或 'wholesale'
+
+/** 弹窗标题 */
+const modalTitle = computed(() => {
+    return priceType.value === 'retail' ? '零售价编辑' : '批发价编辑';
+});
+
+/** 表单数据 */
+const form = reactive({
+    isbn: '',
+    bookName: '',
+    retailPrice: null,
+    wholesalePrice: null
+});
+
+/** 表单验证规则 */
+const rules = computed(() => {
+    if (priceType.value === 'retail') {
+        return {
+            retailPrice: [
+                { required: true, message: '请输入零售价', trigger: 'blur' },
+                { type: 'number', min: 0, message: '零售价不能小于0', trigger: 'blur' }
+            ]
+        };
+    } else {
+        return {
+            wholesalePrice: [
+                { required: true, message: '请输入批发价', trigger: 'blur' },
+                { type: 'number', min: 0, message: '批发价不能小于0', trigger: 'blur' }
+            ]
+        };
+    }
+});
+
+/** 关闭弹窗 */
+const handleCancel = () => {
+    visible.value = false;
+};
+
+/** 弹窗关闭事件 */
+const handleClosed = () => {
+    formRef.value?.resetFields();
+    Object.assign(form, {
+        isbn: '',
+        bookName: '',
+        retailPrice: null,
+        wholesalePrice: null
+    });
+    loading.value = false;
+};
+
+/** 提交表单 */
+const handleSubmit = async () => {
+    try {
+        await formRef.value?.validate();
+        loading.value = true;
+
+        if (priceType.value === 'retail') {
+            // 更新零售价
+            await request.post('/activation/bookActivationInfo/updateRetailPrice', {
+                isbn: form.isbn,
+                price: form.retailPrice
+            });
+            EleMessage.success('零售价更新成功');
+        } else {
+            // 更新批发价
+            await request.post('/activation/bookActivationInfo/updateWholesalePrice', {
+                isbn: form.isbn,
+                price: form.wholesalePrice
+            });
+            EleMessage.success('批发价更新成功');
+        }
+
+        visible.value = false;
+
+        // 触发刷新事件
+        emit('success');
+    } catch (error) {
+        console.error('价格更新失败:', error);
+        EleMessage.error(error.message || '价格更新失败');
+    } finally {
+        loading.value = false;
+    }
+};
+
+/** 弹窗打开事件 */
+const handleOpen = (data, type = 'retail') => {
+    if (data) {
+        priceType.value = type;
+        Object.assign(form, {
+            isbn: data.isbn || '',
+            bookName: data.bookName || '',
+            retailPrice: data.retailPrice || 0,
+            wholesalePrice: data.wholesalePrice || 0
+        });
+        visible.value = true;
+    }
+};
+
+/** 定义事件 */
+const emit = defineEmits(['success']);
+
+defineExpose({
+    handleOpen
+});
+</script>
+
+<style scoped>
+:deep(.el-input-number) {
+    width: 100%;
+}
+
+:deep(.el-input-number .el-input__inner) {
+    text-align: left;
+}
+</style>

+ 294 - 0
src/views/code/list/index.vue

@@ -0,0 +1,294 @@
+<template>
+    <ele-page flex-table>
+        <code-search @search="reload" />
+
+        <common-table ref="pageRef" :pageConfig="pageConfig" :columns="columns">
+            <template #bookInfo="{ row }">
+                <div class="book-info">
+                    <div class="book-title">{{ row.bookName }}</div>
+                    <div class="book-details">
+                        <div class="book-detail-item">作者:{{ row.author || '-' }}</div>
+                        <div class="book-detail-item">ISBN:{{ row.isbn || '-' }}</div>
+                        <div class="book-detail-item">出版社:{{ row.publish || '-' }}</div>
+                    </div>
+                </div>
+            </template>
+            <template #bookType>
+                <el-tag type="primary">激活码</el-tag>
+            </template>
+
+            <template #cover="{ row }">
+                <div class="book-cover">
+                    <img :src="row.cover" alt="封面" style="width: 40px; height: 50px; object-fit: cover;" />
+                </div>
+            </template>
+
+            <template #retailPrice="{ row }">
+                <div class="price-cell">
+                    <span>{{ row.retailPrice ? `¥${row.retailPrice}` : '-' }}</span>
+                    <el-button type="primary" link size="small" @click="handleEditPrice(row, 'retail')" style="margin-left: 8px;">
+                        <el-icon style="font-size: 18px;"><Edit /></el-icon>
+                    </el-button>
+                </div>
+            </template>
+
+            <template #wholesalePrice="{ row }">
+                <div class="price-cell">
+                    <span>{{ row.wholesalePrice ? `¥${row.wholesalePrice}` : '-' }}</span>
+                    <el-button type="primary" link size="small" @click="handleEditPrice(row, 'wholesale')" style="margin-left: 8px;">
+                        <el-icon style="font-size: 18px;"><Edit /></el-icon>
+                    </el-button>
+                </div>
+            </template>
+
+            <template #action="{ row }">
+                <div>
+                    <el-button type="warning" v-permission="'code:activation:out'" link @click="handleViewDetails(row)">
+                        出库
+                    </el-button>
+                    <el-button type="danger" v-permission="'code:activation:in'" link @click="handleInDetails(row)">
+                        入库明细
+                    </el-button>
+                    <el-button type="success" v-permission="'code:activation:outDetails'" link
+                        @click="handleOutDetails(row)">
+                        出库明细
+                    </el-button>
+                </div>
+            </template>
+        </common-table>
+
+        <!-- 出库明细弹窗 -->
+        <out-detail-modal ref="outDetailModalRef" v-model="outDetailVisible" />
+        
+        <!-- 入库明细弹窗 -->
+        <in-detail-modal ref="inDetailModalRef" v-model="inDetailVisible" />
+        
+        <!-- 出库操作弹窗 -->
+        <out-stock-modal ref="outStockModalRef" v-model="outStockVisible" @openActivationModal="handleOpenActivationModal" />
+        
+        <!-- 激活码出库单弹窗 -->
+        <activation-out-stock-modal ref="activationOutStockModalRef" v-model="activationOutStockVisible" />
+        
+        <!-- 价格编辑弹窗 -->
+        <price-edit-modal ref="priceEditModalRef" v-model="priceEditVisible" @success="handlePriceEditSuccess" />
+    </ele-page>
+</template>
+
+<script setup>
+import { ref, reactive } from 'vue';
+import { useRouter } from 'vue-router';
+import { EleMessage } from 'ele-admin-plus/es';
+import { Edit } from '@element-plus/icons-vue';
+import CodeSearch from './components/code-search.vue';
+import CommonTable from '@/components/CommonPage/CommonTable.vue';
+import OutDetailModal from './components/out-detail-modal.vue';
+import InDetailModal from './components/in-detail-modal.vue';
+import OutStockModal from './components/out-stock-modal.vue';
+import ActivationOutStockModal from './components/activation-out-stock-modal.vue';
+import PriceEditModal from './components/price-edit-modal.vue';
+
+defineOptions({ name: 'CodeList' });
+
+/** 弹窗显示状态 */
+const outDetailVisible = ref(false);
+const inDetailVisible = ref(false);
+const outStockVisible = ref(false);
+const activationOutStockVisible = ref(false);
+const priceEditVisible = ref(false);
+
+/** 弹窗组件引用 */
+const outDetailModalRef = ref(null);
+const inDetailModalRef = ref(null);
+const outStockModalRef = ref(null);
+const activationOutStockModalRef = ref(null);
+const priceEditModalRef = ref(null);
+
+/** 表格列配置 */
+const columns = reactive([
+    {
+        type: 'selection',
+        columnKey: 'selection',
+        width: 50,
+        align: 'center',
+        fixed: 'left'
+    },
+    {
+        label: '商品图',
+        prop: 'cover',
+        align: 'center',
+        slot: 'cover',
+        minWidth: 120
+    },
+    {
+        label: '商品名称',
+        prop: 'bookName',
+        align: 'left',
+        minWidth: 300,
+        slot: 'bookInfo'
+    },
+    {
+        label: '商品类型',
+        prop: 'bookType',
+        align: 'center',
+        slot: 'bookType',
+        width: 120
+    },
+    {
+        label: '零售价',
+        prop: 'retailPrice',
+        align: 'center',
+        width: 120,
+        formatter: (row) => row.retailPrice ? `¥${row.retailPrice}` : '-',
+        slot: 'retailPrice'
+    },
+    {
+        label: '批发价',
+        prop: 'wholesalePrice',
+        align: 'center',
+        width: 120,
+        formatter: (row) => row.wholesalePrice ? `¥${row.wholesalePrice}` : '-',
+        slot: 'wholesalePrice'
+    },
+    {
+        label: '库存',
+        prop: 'stockNum',
+        align: 'center',
+        width: 120
+    },
+    {
+        label: '销量',
+        prop: 'salesNum',
+        align: 'center',
+        width: 120
+    },
+    {
+        columnKey: 'action',
+        label: '操作',
+        width: 120,
+        align: 'center',
+        slot: 'action',
+        fixed: 'right'
+    }
+]);
+
+const router = useRouter();
+/** 页面组件实例 */
+const pageRef = ref(null);
+
+const pageConfig = reactive({
+    pageUrl: '/activation/bookActivationInfo/pageList',
+    exportUrl: '/activation/bookActivationInfo/export',
+    deleteUrl: '/activation/bookActivationInfo/delete'
+});
+
+/** 搜索 */
+function reload(where) {
+    pageRef.value?.reload({ page: 1, where });
+}
+
+/** 批量删除 */
+function handleBatchDelete(row) {
+    if (row) {
+        // 单个删除
+        pageRef.value?.remove([row]);
+    } else {
+        // 批量删除
+        pageRef.value?.removeBatch();
+    }
+}
+
+function handleExportExcel() {
+    pageRef.value?.exportData('激活码记录');
+}
+
+//新增
+function handleAdd() {
+    // TODO: 实现新增功能
+    EleMessage.info('新增功能开发中...');
+}
+
+//查看出库明细
+function handleViewDetails(row) {
+    if (row && row.isbn) {
+        outStockModalRef.value?.handleOpen(row);
+    } else {
+        EleMessage.warning('请选择有效的商品');
+    }
+}
+
+/** 打开激活码出库单弹窗 */
+function handleOpenActivationModal(data) {
+    activationOutStockModalRef.value?.handleOpen(data);
+}
+
+/** 编辑价格 */
+function handleEditPrice(row, type) {
+    if (row && row.isbn) {
+        priceEditModalRef.value?.handleOpen(row, type);
+    } else {
+        EleMessage.warning('请选择有效的商品');
+    }
+}
+
+/** 价格编辑成功回调 */
+function handlePriceEditSuccess() {
+    // 刷新列表数据
+    pageRef.value?.reload();
+}
+
+//查看出库明细
+function handleOutDetails(row) {
+    if (row && row.isbn) {
+        outDetailModalRef.value?.handleOpen(row.isbn);
+    } else {
+        EleMessage.warning('请选择有效的商品');
+    }
+}
+
+//查看入库明细
+function handleInDetails(row) {
+    if (row && row.isbn) {
+        inDetailModalRef.value?.handleOpen(row.isbn);
+    } else {
+        EleMessage.warning('请选择有效的商品');
+    }
+}
+</script>
+
+<style scoped>
+.book-info {
+    text-align: left;
+}
+
+.book-title {
+    font-weight: 500;
+    color: #1890ff;
+    margin-bottom: 8px;
+    font-size: 14px;
+    line-height: 1.4;
+}
+
+.book-details {
+    font-size: 12px;
+    color: #666;
+    line-height: 1.5;
+}
+
+.book-detail-item {
+    margin-bottom: 2px;
+}
+
+.book-detail-item:last-child {
+    margin-bottom: 0;
+}
+
+.price-cell {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+}
+
+.price-cell span {
+    margin-right: 4px;
+}
+</style>

+ 143 - 0
src/views/code/record/components/invalid-modal.vue

@@ -0,0 +1,143 @@
+<template>
+    <ele-modal
+        v-model="visible"
+        title="作废"
+        width="500px"
+        :destroy-on-close="true"
+        @closed="handleClosed"
+    >
+        <div class="invalid-form">
+            <div class="form-item">
+                <label class="form-label">请选择作废原因</label>
+                <el-select
+                    v-model="selectedReason"
+                    placeholder="请选择"
+                    style="width: 100%"
+                    clearable
+                >
+                    <el-option
+                        v-for="item in reasonOptions"
+                        :key="item.value"
+                        :label="item.label"
+                        :value="item.value"
+                    />
+                </el-select>
+            </div>
+        </div>
+
+        <template #footer>
+            <el-button @click="handleCancel">取消</el-button>
+            <el-button type="primary" @click="handleConfirm" :loading="loading">确认</el-button>
+        </template>
+    </ele-modal>
+</template>
+
+<script setup>
+import { ref, reactive } from 'vue';
+import { ElMessage } from 'element-plus';
+
+defineOptions({ name: 'InvalidModal' });
+
+// 弹窗显示状态
+const visible = ref(false);
+const loading = ref(false);
+
+// 当前记录信息
+const currentRecord = ref(null);
+
+// 选中的作废原因
+const selectedReason = ref('');
+
+// 作废原因选项
+const reasonOptions = ref([
+    { label: '系统错误', value: 'system_error' },
+    { label: '用户取消', value: 'user_cancel' },
+    { label: '库存不足', value: 'stock_shortage' },
+    { label: '价格变动', value: 'price_change' },
+    { label: '其他原因', value: 'other' }
+]);
+
+// 打开弹窗
+const handleOpen = (record) => {
+    currentRecord.value = record;
+    visible.value = true;
+    selectedReason.value = '';
+};
+
+// 关闭弹窗
+const handleClosed = () => {
+    currentRecord.value = null;
+    selectedReason.value = '';
+    loading.value = false;
+};
+
+// 取消操作
+const handleCancel = () => {
+    visible.value = false;
+};
+
+// 确认作废
+const handleConfirm = async () => {
+    if (!selectedReason.value) {
+        ElMessage.warning('请选择作废原因');
+        return;
+    }
+
+    loading.value = true;
+    
+    try {
+        // 调用作废API(暂时使用假接口)
+        await mockInvalidApi({
+            id: currentRecord.value.id,
+            reason: selectedReason.value
+        });
+        
+        ElMessage.success('作废成功');
+        visible.value = false;
+        
+        // 触发父组件刷新
+        emit('success');
+    } catch (error) {
+        console.error('作废失败:', error);
+        ElMessage.error('作废失败');
+    } finally {
+        loading.value = false;
+    }
+};
+
+// 模拟作废API
+const mockInvalidApi = (params) => {
+    return new Promise((resolve, reject) => {
+        setTimeout(() => {
+            console.log('作废参数:', params);
+            // 模拟成功
+            resolve({ code: 200, message: '作废成功' });
+        }, 1000);
+    });
+};
+
+// 暴露方法给父组件
+defineExpose({
+    handleOpen
+});
+
+// 定义事件
+const emit = defineEmits(['success']);
+</script>
+
+<style scoped>
+.invalid-form {
+    padding: 20px 0;
+}
+
+.form-item {
+    margin-bottom: 20px;
+}
+
+.form-label {
+    display: block;
+    margin-bottom: 8px;
+    font-weight: 500;
+    color: #333;
+}
+</style>

+ 248 - 0
src/views/code/record/components/out-detail-modal.vue

@@ -0,0 +1,248 @@
+<!-- 出库详情弹窗 -->
+<template>
+    <ele-modal v-model="visible" title="出库详情" :width="800" @closed="handleClosed">
+        <div v-loading="loading" class="detail-container">
+            <!-- 基本信息 -->
+            <div class="info-section">
+                <div class="info-row">
+                    <span class="info-label">链接:</span>
+                    <span class="info-value">{{ detailData.urlLink || '-' }}</span>
+                    <el-button type="primary" size="small" @click="copyLink" style="margin-left: 10px;">
+                        复制全部信息
+                    </el-button>
+                </div>
+                <div class="info-row">
+                    <span class="info-label">提取码:</span>
+                    <span class="info-value">{{ detailData.extCode || '-' }}</span>
+                </div>
+                <div class="info-row">
+                    <span class="info-label">书名:</span>
+                    <span class="info-value">{{ detailData.bookName || '-' }}</span>
+                </div>
+                <div class="info-row">
+                    <span class="info-label">数量:</span>
+                    <span class="info-value">{{ detailData.num || 0 }}个</span>
+                </div>
+            </div>
+
+            <!-- 图片详情 -->
+            <div class="image-section">
+                <h3 class="section-title">图片详情</h3>
+                <div v-if="detailData.imgList && detailData.imgList.length > 0" class="image-grid">
+                    <div 
+                        v-for="(image, index) in detailData.imgList" 
+                        :key="index" 
+                        class="image-item"
+                        @click="previewImage(image, index)"
+                    >
+                        <img :src="image" :alt="`图片${index + 1}`" class="thumbnail" />
+                    </div>
+                </div>
+                <div v-else class="no-images">
+                    暂无图片
+                </div>
+            </div>
+        </div>
+
+        <!-- 图片预览 -->
+        <el-image-viewer
+            v-if="showImageViewer"
+            :url-list="detailData.imgList || []"
+            :initial-index="currentImageIndex"
+            @close="closeImageViewer"
+        />
+
+        <template #footer>
+            <el-button @click="handleClose">关闭</el-button>
+        </template>
+    </ele-modal>
+</template>
+
+<script setup>
+import { ref, reactive } from 'vue';
+import { EleMessage } from 'ele-admin-plus/es';
+import request from '@/utils/request';
+
+/** 弹窗是否打开 */
+const visible = defineModel({ type: Boolean });
+
+/** 加载状态 */
+const loading = ref(false);
+
+/** 详情数据 */
+const detailData = reactive({
+    isbn: '',
+    bookName: '',
+    num: 0,
+    urlLink: '',
+    extCode: '',
+    imgList: []
+});
+
+/** 图片预览相关 */
+const showImageViewer = ref(false);
+const currentImageIndex = ref(0);
+
+/** 关闭弹窗 */
+const handleClose = () => {
+    visible.value = false;
+};
+
+/** 弹窗关闭事件 */
+const handleClosed = () => {
+    // 重置数据
+    Object.assign(detailData, {
+        isbn: '',
+        bookName: '',
+        num: 0,
+        urlLink: '',
+        extCode: '',
+        imgList: []
+    });
+    loading.value = false;
+    showImageViewer.value = false;
+};
+
+/** 复制完整信息 */
+const copyLink = async () => {
+    if (detailData.urlLink) {
+        try {
+            const copyText = `链接:${detailData.urlLink}\n提取码:${detailData.extCode || ''}\n书名:${detailData.bookName || ''}\n数量:${detailData.num || 0}个`;
+            await navigator.clipboard.writeText(copyText);
+            EleMessage.success('信息复制成功');
+        } catch (error) {
+            console.error('复制失败:', error);
+            EleMessage.error('复制失败');
+        }
+    } else {
+        EleMessage.warning('暂无链接可复制');
+    }
+};
+
+/** 预览图片 */
+const previewImage = (image, index) => {
+    currentImageIndex.value = index;
+    showImageViewer.value = true;
+};
+
+/** 关闭图片预览 */
+const closeImageViewer = () => {
+    showImageViewer.value = false;
+};
+
+/** 获取出库详情 */
+const fetchOutDetail = async (id) => {
+    try {
+        loading.value = true;
+        const response = await request.get(`/activation/bookActivationInfo/outDetail/${id}`);
+        
+        if (response.data.code === 200) {
+            const data = response.data.data || {};
+            Object.assign(detailData, {
+                isbn: data.isbn || '',
+                bookName: data.bookName || '',
+                num: data.num || 0,
+                urlLink: data.urlLink || '',
+                extCode: data.extCode || '',
+                imgList: data.imgList || []
+            });
+        } else {
+            EleMessage.error(response.data.msg || '获取详情失败');
+        }
+    } catch (error) {
+        console.error('获取出库详情失败:', error);
+        EleMessage.error('获取详情失败');
+    } finally {
+        loading.value = false;
+    }
+};
+
+/** 打开弹窗 */
+const handleOpen = (id) => {
+    if (id) {
+        visible.value = true;
+        fetchOutDetail(id);
+    } else {
+        EleMessage.warning('缺少必要参数');
+    }
+};
+
+defineExpose({
+    handleOpen
+});
+</script>
+
+<style scoped>
+.detail-container {
+    min-height: 300px;
+}
+
+.info-section {
+    margin-bottom: 24px;
+}
+
+.info-row {
+    display: flex;
+    align-items: center;
+    margin-bottom: 12px;
+    font-size: 14px;
+}
+
+.info-label {
+    font-weight: bold;
+    color: #333;
+    width: 80px;
+    flex-shrink: 0;
+}
+
+.info-value {
+    color: #666;
+    flex: 1;
+    word-break: break-all;
+}
+
+.image-section {
+    border-top: 1px solid #eee;
+    padding-top: 20px;
+}
+
+.section-title {
+    font-size: 16px;
+    font-weight: bold;
+    color: #333;
+    margin-bottom: 16px;
+}
+
+.image-grid {
+    display: grid;
+    grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
+    gap: 12px;
+}
+
+.image-item {
+    cursor: pointer;
+    border: 2px solid transparent;
+    border-radius: 8px;
+    overflow: hidden;
+    transition: all 0.3s ease;
+}
+
+.image-item:hover {
+    border-color: #409eff;
+    transform: scale(1.05);
+}
+
+.thumbnail {
+    width: 100%;
+    height: 120px;
+    object-fit: cover;
+    display: block;
+}
+
+.no-images {
+    text-align: center;
+    color: #999;
+    padding: 40px 0;
+    font-size: 14px;
+}
+</style>

+ 211 - 0
src/views/code/record/index.vue

@@ -0,0 +1,211 @@
+<template>
+    <ele-page flex-table>
+        <page-search @search="reload" />
+
+        <common-table ref="pageRef" :pageConfig="pageConfig" :columns="columns">
+            <template #toolbar>
+                <div class="flex items-center mb-4">
+                    <el-statistic :value="statistics.totalStockNum || 0" title="累计激活码数量" value-style="font-size:30px"
+                        class="mr-20" />
+                    <el-statistic :value="statistics.totalOutNum || 0" title="已出库数量" value-style="font-size:30px"  
+                        class="mr-20" />
+                    <el-statistic :value="statistics.totalOutPrice || 0" title="已出库金额" :precision="2"
+                        value-style="font-size:30px" class="mr-20" />
+                    <el-statistic :value="statistics.stockNum || 0" title="待出库数量" value-style="font-size:30px" 
+                        class="mr-20" />
+                </div>
+            </template>
+
+            <template #bookInfo="{ row }">
+                <div class="book-info">
+                    <div class="book-title">{{ row.bookName }}</div>
+                    <div class="book-details">
+                        <div class="book-detail-item">作者:{{ row.author || '-' }}</div>
+                        <div class="book-detail-item">ISBN:{{ row.isbn || '-' }}</div>
+                        <div class="book-detail-item">出版社:{{ row.publisher || '-' }}</div>
+                    </div>
+                </div>
+            </template>
+
+            <template #type="{ row }">
+                <el-tag :type="row.type == 0 ? 'success' : 'warning'">
+                    {{ row.type == 0 ? '入库' : '出库' }}
+                </el-tag>
+            </template>
+
+            <template #status="{ row }">
+                <el-tag :type="row.status == 1 ? 'success' : row.status == 2 ? 'warning' : 'info'">
+                    {{ getStatusText(row.status) }}
+                </el-tag>
+            </template>
+
+            <template #operatorName="{ row }">
+                {{ row.operatorName || '-' }}
+            </template>
+
+            <template #action="{ row }">
+                <div>
+                    <el-button type="primary" link @click="handleViewDetails(row)">
+                        [查看详情]
+                    </el-button>
+                    <el-button type="danger" link @click="handleInvalid(row)" v-if="row.type == 1">
+                        [作废]
+                    </el-button>
+                </div>
+            </template>
+        </common-table>
+
+        <!-- 出库详情弹窗 -->
+        <out-detail-modal ref="outDetailModalRef" />
+        
+        <!-- 作废弹窗 -->
+        <invalid-modal ref="invalidModalRef" @success="handleInvalidSuccess" />
+    </ele-page>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted } from 'vue';
+import CommonTable from '@/components/CommonPage/CommonTable.vue';
+import pageSearch from './page-search.vue';
+import OutDetailModal from './components/out-detail-modal.vue';
+import InvalidModal from './components/invalid-modal.vue';
+import request from '@/utils/request';
+
+defineOptions({ name: 'CodeRecord' });
+
+// 统计数据的响应式对象
+const statistics = reactive({
+    totalStockNum: 0,
+    totalOutNum: 0,
+    totalOutPrice: 0,
+    stockNum: 0
+});
+
+// 获取统计数据
+async function fetchStatistics() {
+    request.get('/activation/bookActivationInfo/inOutSum', {
+        params: {
+            startTime: '',
+            endTime: ''
+        }
+    }).then(res => {
+        console.log('获取统计数据:', res);
+        if (res.data.code === 200) {
+            Object.assign(statistics, res.data.data || {});
+        }
+    })
+}
+
+onMounted(() => {
+    fetchStatistics();
+});
+
+// 获取状态文本
+function getStatusText(status) {
+    const statusMap = {
+        1: '已完成',
+        2: '进行中',
+        3: '待处理'
+    };
+    return statusMap[status] || '未知';
+}
+
+/** 表格列配置 */
+const columns = ref([
+    { label: 'ISBN', prop: 'isbn', align: 'center', width: 150 },
+    {
+        label: '商品名称',
+        prop: 'bookName',
+        align: 'left',
+        minWidth: 300,
+        slot: 'bookInfo'
+    },
+    { label: '操作类型', prop: 'type', align: 'center', width: 100, slot: 'type' },
+    { label: '操作时间', prop: 'operatorTime', align: 'center', width: 180 },
+    { label: '数量', prop: 'num', align: 'center', width: 100 },
+    { label: '金额', prop: 'totalPrice', align: 'center', width: 120 },
+    { label: '单价', prop: 'price', align: 'center', width: 100 },
+    { label: '操作', prop: 'operatorName', align: 'center', width: 120, slot: 'operatorName' },
+    {
+        columnKey: 'action',
+        label: '操作',
+        width: 120,
+        align: 'center',
+        slot: 'action',
+        fixed: 'right'
+    }
+]);
+
+/** 页面组件实例 */
+const pageRef = ref(null);
+
+/** 出库详情弹窗引用 */
+const outDetailModalRef = ref(null);
+
+/** 作废弹窗引用 */
+const invalidModalRef = ref(null);
+
+const pageConfig = reactive({
+    pageUrl: '/activation/bookActivationInfo/inOutList',
+    fileName: '出入库记录',
+    cacheKey: 'codeRecord',
+    rowKey: 'id',
+});
+
+//刷新表格
+function reload() {
+    pageRef.value?.reload();
+}
+
+// 查看详情
+const handleViewDetails = (row) => {
+    console.log('查看详情:', row);
+    if (row && row.id) {
+        outDetailModalRef.value?.handleOpen(row.id);
+    } else {
+        console.error('缺少记录ID');
+    }
+};
+
+// 作废操作
+const handleInvalid = (row) => {
+    console.log('作废操作:', row);
+    if (row && row.id) {
+        invalidModalRef.value?.handleOpen(row);
+    } else {
+        console.error('缺少记录信息');
+    }
+};
+
+// 作废成功回调
+const handleInvalidSuccess = () => {
+    // 刷新表格数据
+    reload();
+    // 刷新统计数据
+    fetchStatistics();
+};
+
+</script>
+
+<style scoped>
+.book-info {
+    text-align: left;
+}
+
+.book-title {
+    font-size: 14px;
+    font-weight: bold;
+    color: #409eff;
+    margin-bottom: 4px;
+    cursor: pointer;
+}
+
+.book-details {
+    font-size: 12px;
+    color: #666;
+}
+
+.book-detail-item {
+    margin-bottom: 2px;
+}
+</style>

+ 101 - 0
src/views/code/record/page-search.vue

@@ -0,0 +1,101 @@
+<!-- 搜索表单 -->
+<template>
+    <ele-card :body-style="{ paddingBottom: '8px' }">
+        <ProSearch :items="formItems" ref="searchRef" @search="search" :initKeys="initKeys">
+            <template #buttons>
+                <el-button type="primary" @click="searchRef.handleSearch()">查询</el-button>
+                <el-button @click="searchRef.handleReset()">重置</el-button>
+            </template>
+        </ProSearch>
+    </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: 'ISBN', prop: 'isbn', placeholder: '请输入ISBN' },
+    { type: 'input', label: '书名', prop: 'bookName', placeholder: '请输入书名' },
+    {
+        type: 'select',
+        label: '请选择操作类型',
+        prop: 'operationType',
+        props: {
+            placeholder: '请选择操作类型',
+            options: [
+                { label: '全部', value: '' },
+                { label: '入库', value: '0' },
+                { label: '出库', value: '1' }
+            ]
+        }
+    },
+    { type: 'input', label: '请输入备注信息', prop: 'remark', placeholder: '请输入备注信息' },
+    {
+        type: 'datetimerange',
+        label: '时间',
+        prop: 'timeRange',
+        props: {
+            valueFormat: 'YYYY-MM-DD HH:mm:ss',
+            format: 'YYYY-MM-DD HH:mm:ss',
+            startPlaceholder: '开始时间',
+            endPlaceholder: '结束时间',
+            shortcuts: [
+                {
+                    text: '最近7天',
+                    value: () => {
+                        const end = new Date();
+                        const start = new Date();
+                        start.setTime(start.getTime() - 3600 * 1000 * 24 * 7);
+                        return [start, end];
+                    }
+                },
+                {
+                    text: '最近15天',
+                    value: () => {
+                        const end = new Date();
+                        const start = new Date();
+                        start.setTime(start.getTime() - 3600 * 1000 * 24 * 15);
+                        return [start, end];
+                    }
+                },
+                {
+                    text: '最近30天',
+                    value: () => {
+                        const end = new Date();
+                        const start = new Date();
+                        start.setTime(start.getTime() - 3600 * 1000 * 24 * 30);
+                        return [start, end];
+                    }
+                }
+            ],
+            onChange: (value) => {
+                searchRef.value?.setData({
+                    startTime: value ? value[0] : '',
+                    endTime: value ? value[1] : ''
+                });
+            }
+        }
+    }
+]);
+
+const initKeys = reactive({
+    startTime: '',
+    endTime: '',
+    timeRange: [],
+    isbn: '',
+    bookName: '',
+    operationType: '',
+    remark: ''
+});
+
+const searchRef = ref(null);
+/** 搜索 */
+const search = (data) => {
+    let params = { ...data };
+    delete params.timeRange;
+    emit('search', params);
+};
+</script>

+ 1 - 1
src/views/data/universities/index.vue

@@ -1,4 +1,4 @@
-<template>
+f<template>
     <ele-page flex-table>
         <universities-search @search="reload" />