Pārlūkot izejas kodu

feat: 新增搜索反馈、易投诉书单和到货统计页面组件

新增搜索反馈管理功能模块,包含搜索表单、处理弹窗和列表展示
添加易投诉书单管理功能,支持图书增删改查和导入导出
实现到货统计页面,包含复杂表格操作和多种状态管理
ylong 2 dienas atpakaļ
vecāks
revīzija
1e968db67d

+ 48 - 0
src/views/data/arrivalStats/components/page-search.vue

@@ -0,0 +1,48 @@
+<!-- 搜索表单 -->
+<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: 'bookName' },
+    { type: 'input', label: 'ISBN', prop: 'isbn' },
+    {
+        type: 'inputNumberRange',
+        label: '库存',
+        prop: 'stock',
+        keys: ['minStock', 'maxStock'],
+        props: {
+            minAttrs: { min: 0 },
+            maxAttrs: { max: 10 },
+            label: '库存',
+            onChange: (val) => {
+                searchRef.value?.setData({
+                    minStock: val.min,
+                    maxStock: val.max
+                });
+            }
+        }
+    },
+]);
+
+const initKeys = reactive({
+    bookName: '',
+    isbn: '',
+    minStock: void 0,
+    maxStock: void 0,
+});
+
+const searchRef = ref(null);
+/** 搜索 */
+const search = (data) => {
+    emit('search', data);
+};
+</script>

+ 382 - 0
src/views/data/arrivalStats/index.vue

@@ -0,0 +1,382 @@
+<template>
+    <ele-page flex-table>
+        <book-search @search="reload" />
+
+        <common-table ref="pageRef" :pageConfig="pageConfig" :columns="columns" :tools="false">
+            <template #toolbar>
+                <el-radio-group @change="handleStatusChange" v-model="searchType">
+                    <el-radio-button label="全部" value="0" />
+                    <el-radio-button label="已加入回收书单(正在回收)" value="1" />
+                    <el-radio-button label="已加入回收书单(暂停回收)" value="2" />
+                </el-radio-group>
+
+                <span class="ml-8"></span>
+                <el-button type="danger" plain v-permission="'recycle:independent:batchAddBlacklist'"
+                    @click="handleOptBlacklist()">
+                    加黑名单
+                </el-button>
+                <el-button type="warning" plain v-permission="'recycle:independent:batchPauseRecycle'"
+                    @click="handleOptRecycle(null, 2)">
+                    暂停回收
+                </el-button>
+                <el-button type="warning" plain v-permission="'recycle:independent:batchStartRecycle'"
+                    @click="handleOptRecycle(null, 1)">
+                    开启回收
+                </el-button>
+                <el-button type="success" plain v-permission="'recycle:independent:batchAddSocial'"
+                    @click="handleOptType(1)">
+                    加社科库
+                </el-button>
+                <el-button type="primary" plain v-permission="'recycle:independent:batchAddTeach'"
+                    @click="handleOptType(2)">
+                    加教材库
+                </el-button>
+            </template>
+
+            <template #cover="{ row }">
+                <el-image style="width: 90px; height: 120px; border-radius: 4px" fit="cover" :src="row.cover"
+                    :preview-src-list="[row.cover]" :initial-index="0" preview-teleported />
+            </template>
+            <template #baseInfo="{ row }">
+                <book-info :row="row" @refresh="reload" />
+            </template>
+            <template #stock="{ row }">
+                <book-stock :row="row" />
+            </template>
+            <template #view="{ row }">
+                <div class="grid grid-cols-2 gap-2 book-btns">
+                    <el-button color="#951d1d" @click="handleViewUrl(row, 'kw')"
+                        v-permission="'recycle:independent:viewUrl'">
+                        查看孔网
+                    </el-button>
+                    <el-button color="#e99d42" @click="handleRecycleLog(row)"
+                        v-permission="'recycle:independent:viewRecycleLog'">
+                        回收日志
+                    </el-button>
+                    <el-button color="#f27606" @click="handleViewUrl(row, 'tb')"
+                        v-permission="'recycle:independent:viewUrl'">
+                        查看淘宝
+                    </el-button>
+                    <el-button color="#0f7dc7" @click="handleSalesLog(row)"
+                        v-permission="'recycle:independent:viewSalesLog'">
+                        售价日志
+                    </el-button>
+                    <el-button color="#399420" @click="handleViewUrl(row, 'db')"
+                        v-permission="'recycle:independent:viewUrl'">
+                        查看豆瓣
+                    </el-button>
+                    <el-button color="#a4adb3" @click="handleViewUrl(row, 'dd')"
+                        v-permission="'recycle:independent:viewUrl'">
+                        查看当当
+                    </el-button>
+                </div>
+            </template>
+
+            <template #action="{ row }">
+                <div class="grid grid-cols-2 gap-2 book-btns">
+                    <el-button color="#7728f5" v-permission="'recycle:independent:updateDiscount'"
+                        @click="handleModifyDiscount(row)">
+                        修改回收折扣
+                    </el-button>
+                    <el-button color="#333333" v-permission="'recycle:independent:addBlacklist'"
+                        @click="handleOptBlacklist(row)">
+                        加入黑名单
+                    </el-button>
+                    <el-button color="#3ab54a" v-permission="'recycle:independent:updateMaxRecycle'"
+                        @click="handleModifyMaxRecycle(row)">
+                        修改最大回收量
+                    </el-button>
+                    <el-button color="#bd3124" v-permission="'recycle:independent:pauseRecycle'"
+                        @click="handleOptRecycle(row, 2)" v-if="row.recycleStatus == 1">
+                        暂停回收
+                    </el-button>
+                    <el-button color="#bd3124" v-permission="'recycle:independent:startRecycle'"
+                        @click="handleOptRecycle(row, 1)" v-else>
+                        开启回收
+                    </el-button>
+                    <el-button color="#e99d42" v-permission="'recycle:independent:updateOrderRecycle'"
+                        @click="handleModifyOrderRecycle(row)">
+                        修改订单回收量
+                    </el-button>
+                    <el-button color="#4095e5" v-permission="'recycle:independent:removeBooklist'"
+                        @click="handleOptBooklist(row)">
+                        移除回收书单
+                    </el-button>
+                </div>
+            </template>
+        </common-table>
+
+        <set-params ref="paramsRef" @refresh="reload" />
+        <add-discount ref="discountRef" @refresh="reload" />
+        <orderRecycleLog ref="recycleLogRef" />
+        <orderSalesLog ref="salesLogRef" />
+        <modifyOrderRecycle ref="orderRecycleRef" @refresh="reload" />
+        <modifyMaxRecycle ref="maxRecycleRef" @refresh="reload" />
+        <modifyDiscount ref="modifyDiscountRef" @refresh="reload" />
+        <orderBlacklist ref="blacklistRef" @refresh="reload" />
+    </ele-page>
+</template>
+
+<script setup>
+import { ref, reactive } from 'vue';
+import CommonTable from '@/components/CommonPage/CommonTable.vue';
+import bookSearch from '@/views/data/arrivalStats/components/page-search.vue';
+import bookInfo from '@/views/recycle/components/book-info.vue';
+import bookStock from '@/views/recycle/components/book-stock.vue';
+import setParams from '@/views/recycle/components/set-params.vue';
+import addDiscount from '@/views/recycle/components/add-discount.vue';
+import orderRecycleLog from '@/views/recycleOrder/detail/order-recycle-log.vue';
+import orderSalesLog from '@/views/recycleOrder/detail/order-sales-log.vue';
+import modifyOrderRecycle from '@/views/recycle/components/modify-order-recycle.vue';
+import modifyMaxRecycle from '@/views/recycle/components/modify-max-recycle.vue';
+import modifyDiscount from '@/views/recycle/components/modify-discount.vue';
+import orderBlacklist from '@/views/recycleOrder/detail/order-blacklist.vue';
+
+defineOptions({ name: 'IndependentParameter' });
+
+const searchType = ref('0');
+const blacklistRef = ref(null);
+function handleStatusChange(value) {
+    searchType.value = value;
+    pageConfig.params.searchType = value;
+    pageRef.value.reload();
+}
+
+/** 表格列配置 */
+const columns = ref([
+    {
+        type: 'selection',
+        columnKey: 'selection',
+        width: 50,
+        align: 'center',
+        fixed: 'left'
+    },
+    {
+        label: '图片',
+        prop: 'cover',
+        width: 120,
+        slot: 'cover'
+    },
+    {
+        label: '信息',
+        prop: 'baseInfo',
+        width: 500,
+        slot: 'baseInfo'
+    },
+    {
+        label: '订阅次数',
+        prop: 'recyclePrice',
+        sortable: true,
+        columnKey: '5',
+        minWidth: 100
+    },
+    {
+        label: '最后一次订阅时间',
+        minWidth: 120,
+        prop: 'recycleMax',
+        sortable: true,
+        columnKey: '6'
+    },
+    {
+        label: '查看',
+        prop: 'view',
+        slot: 'view',
+        width: 234,
+        fixed: 'right'
+    },
+    {
+        columnKey: 'action',
+        label: '操作',
+        width: 234,
+        slot: 'action',
+        fixed: 'right'
+    }
+]);
+
+/** 页面组件实例 */
+const pageRef = ref(null);
+
+const pageConfig = reactive({
+    pageUrl: '/book/bookRecycleInfo/sepSet/pageList',
+    fileName: '到货提醒统计',
+    cacheKey: 'arrivalStatsTable',
+    rowKey: 'isbn',
+    params: {
+        searchType: '0'
+    }
+});
+
+//刷新表格
+function reload(where) {
+    pageRef.value?.reload(where);
+}
+
+//设置参数
+const paramsRef = ref(null);
+function handleSetParams(row) {
+    paramsRef.value?.handleOpen(row);
+}
+
+//黑名单操作
+function handleOptBlacklist(row) {
+    let selections = row ? [row] : pageRef.value?.getSelections();
+    if (!selections || selections.length === 0) {
+        ElMessage.warning('请选择要操作的图书');
+        return;
+    }
+
+    const isbnList = selections.map((item) => item.isbn);
+    // 加入黑名单,打开选择原因弹窗
+    blacklistRef.value?.handleOpen(isbnList);
+}
+
+//回收操作
+function handleOptRecycle(row, status) {
+    let selections = row ? [row] : pageRef.value?.getSelections();
+    if (!selections || selections.length === 0) {
+        ElMessage.warning('请选择要操作的图书');
+        return;
+    }
+
+    const isbnList = selections.map((item) => item.isbn);
+    const isStop = status === 2;
+
+    const url = isStop
+        ? '/book/bookRecycleInfo/stopRecycle'
+        : '/book/bookRecycleInfo/openRecycle';
+
+    const title = isStop ? '确认暂停回收?' : '确认开启回收?';
+
+    pageRef.value?.operatBatch({
+        title,
+        method: 'post',
+        url,
+        data: { isbnList },
+        row,
+        success: () => {
+            reload();
+        }
+    });
+}
+
+//类型操作
+function handleOptType(type) {
+    let selections = pageRef.value?.getSelections();
+    if (!selections || selections.length === 0) {
+        ElMessage.warning('请选择要操作的图书');
+        return;
+    }
+
+    const isbnList = selections.map((item) => item.isbn);
+    const url =
+        type == 1
+            ? '/book/bookRecycleInfo/changeBookTag2'
+            : '/book/bookRecycleInfo/changeBookTag1';
+    const title = type == 1 ? '确认加社科库?' : '确认加教材库?';
+
+    pageRef.value?.operatBatch({
+        title,
+        method: 'post',
+        url,
+        data: { isbnList },
+        success: () => {
+            reload();
+        }
+    });
+}
+
+//指定折扣
+const discountRef = ref(null);
+function handleAddDiscount() {
+    let selections = pageRef.value?.getSelections();
+    if (!selections || selections.length === 0) {
+        ElMessage.warning('请选择要操作的图书');
+        return;
+    }
+
+    const isbnList = selections.map((item) => item.isbn);
+    discountRef.value?.handleOpen({ isbnList });
+}
+
+//查看当当、淘宝、豆瓣链接
+const handleViewUrl = (row, type) => {
+    let url = '';
+    if (type == 'dd') {
+        url = `https://search.dangdang.com/?key=${row.isbn}&act=input`;
+    } else if (type == 'tb') {
+        url = `https://s.taobao.com/search?page=1&q=${row.isbn}&sort=sale-desc&tab=all`;
+    } else if (type == 'db') {
+        url = `https://search.douban.com/book/subject_search?search_text=${row.isbn}`;
+    } else if (type == 'kw') {
+        url = `https://search.kongfz.com/product_result/?key=${row.isbn}&status=0&_stpmt=eyJzZWFyY2hfdHlwZSI6ImFjdGl2ZSJ9`;
+    }
+    window.open(url, '_blank');
+};
+
+//查看回收日志
+const recycleLogRef = ref();
+const handleRecycleLog = (row) => {
+    recycleLogRef.value?.handleOpen(row);
+};
+
+//移除回收书单操作
+function handleOptBooklist(row) {
+    let selections = row ? [row] : pageRef.value?.getSelections();
+    if (!selections || selections.length === 0) {
+        ElMessage.warning('请选择要操作的图书');
+        return;
+    }
+
+    const isbnList = selections.map((item) => item.isbn);
+    const url = '/book/bookRecycleInfo/removeOut';
+    const title = '确认移除回收书单?';
+
+    pageRef.value?.operatBatch({
+        title,
+        method: 'post',
+        url,
+        data: { isbnList },
+        row,
+        success: () => {
+            reload();
+        }
+    });
+}
+
+//查看售价日志
+const salesLogRef = ref();
+const handleSalesLog = (row) => {
+    salesLogRef.value?.handleOpen(row);
+};
+
+//修改订单回收量
+const orderRecycleRef = ref();
+const handleModifyOrderRecycle = (row) => {
+    orderRecycleRef.value?.handleOpen(row);
+};
+
+//修改最大回收量
+const maxRecycleRef = ref();
+const handleModifyMaxRecycle = (row) => {
+    maxRecycleRef.value?.handleOpen(row);
+};
+
+//修改回收折扣
+const modifyDiscountRef = ref();
+const handleModifyDiscount = (row) => {
+    modifyDiscountRef.value?.handleOpen(row);
+};
+</script>
+<style lang="scss" scoped>
+.book-btns {
+    gap: 6px;
+
+    .el-button {
+        min-width: 100px;
+        font-size: 12px;
+        color: #ffffff;
+        padding: 4px 6px;
+        margin-right: 0;
+    }
+}
+</style>

+ 88 - 0
src/views/data/searchFeedback/components/handle-dialog.vue

@@ -0,0 +1,88 @@
+<template>
+    <ele-modal
+        v-model="visible"
+        title="处理反馈"
+        :width="800"
+        :footer="null"
+    >
+        <div class="p-4">
+            <el-form :model="form" label-width="120px">
+                <el-form-item label="体验打分:">
+                    <el-input v-model="form.score" readonly style="width: 200px">
+                        <template #append>分</template>
+                    </el-input>
+                </el-form-item>
+                
+                <el-form-item label="反馈信息:">
+                    <el-input
+                        v-model="form.content"
+                        type="textarea"
+                        :rows="6"
+                        readonly
+                        resize="none"
+                    />
+                </el-form-item>
+                
+                <el-form-item label="反馈图片/视频:">
+                    <div class="flex gap-2" v-if="form.images && form.images.length">
+                        <el-image
+                            v-for="(img, index) in form.images"
+                            :key="index"
+                            :src="img"
+                            :preview-src-list="form.images"
+                            :initial-index="index"
+                            fit="cover"
+                            style="width: 100px; height: 100px; border-radius: 4px;"
+                        />
+                    </div>
+                    <div v-else class="text-gray-400">无图片/视频</div>
+                </el-form-item>
+            </el-form>
+            
+            <div class="flex justify-center gap-4 mt-8">
+                <el-button type="success" @click="handleComplete">处理完成</el-button>
+                <el-button color="#000080" style="color: white" @click="handleFollowUp">跟进</el-button>
+                <el-button type="info" plain @click="visible = false">关闭</el-button>
+            </div>
+        </div>
+    </ele-modal>
+</template>
+
+<script setup>
+import { ref, reactive } from 'vue';
+import { ElMessage } from 'element-plus';
+
+const visible = ref(false);
+const form = reactive({
+    id: null,
+    score: '',
+    content: '',
+    images: []
+});
+
+const handleOpen = (row) => {
+    if (row) {
+        form.id = row.id;
+        form.score = row.score;
+        form.content = row.content;
+        form.images = row.images || [];
+    }
+    visible.value = true;
+};
+
+const handleComplete = () => {
+    // Mock API call
+    ElMessage.success('处理完成');
+    visible.value = false;
+};
+
+const handleFollowUp = () => {
+    // Mock API call
+    ElMessage.success('已跟进');
+    visible.value = false;
+};
+
+defineExpose({
+    handleOpen
+});
+</script>

+ 43 - 0
src/views/data/searchFeedback/components/page-search.vue

@@ -0,0 +1,43 @@
+<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 searchRef = ref(null);
+
+const formItems = reactive([
+    { type: 'input', label: '请输入用户昵称', prop: 'nickname', placeholder: '请输入用户昵称' },
+    { type: 'input', label: '请输入用户ID', prop: 'userId', placeholder: '请输入用户ID' },
+    { 
+        type: 'select', 
+        label: '请选择状态', 
+        prop: 'status', 
+        placeholder: '请选择状态',
+        options: [
+            { label: '待处理', value: 0 },
+            { label: '已处理', value: 1 }
+        ]
+    }
+]);
+
+const initKeys = reactive({
+    nickname: '',
+    userId: '',
+    status: void 0
+});
+
+const search = (data) => {
+    emit('search', data);
+};
+</script>

+ 106 - 0
src/views/data/searchFeedback/index.vue

@@ -0,0 +1,106 @@
+<template>
+    <ele-page flex-table>
+        <page-search @search="reload" />
+        
+        <common-table
+            ref="pageRef"
+            :pageConfig="pageConfig"
+            :columns="columns"
+            :datasource="datasource"
+            :tools="false"
+        >
+            <!-- 状态列 -->
+            <template #status="{ row }">
+                {{ row.status === 0 ? '待处理' : '已处理' }}
+            </template>
+
+            <!-- 图片/视频列 -->
+            <template #media="{ row }">
+                <div class="flex gap-1" v-if="row.images && row.images.length">
+                    <el-image
+                        v-for="(img, idx) in row.images.slice(0, 2)"
+                        :key="idx"
+                        :src="img"
+                        :preview-src-list="row.images"
+                        :initial-index="idx"
+                        preview-teleported
+                        style="width: 40px; height: 40px; border-radius: 4px;"
+                    />
+                </div>
+            </template>
+
+            <!-- 操作列 -->
+            <template #action="{ row }">
+                <el-button link type="primary" @click="handleProcess(row)">[去处理]</el-button>
+                <el-button link type="primary" @click="handleDetail(row)">[详情]</el-button>
+            </template>
+        </common-table>
+
+        <handle-dialog ref="handleDialogRef" />
+    </ele-page>
+</template>
+
+<script setup>
+import { ref, reactive } from 'vue';
+import CommonTable from '@/components/CommonPage/CommonTable.vue';
+import PageSearch from './components/page-search.vue';
+import HandleDialog from './components/handle-dialog.vue';
+
+defineOptions({ name: 'SearchFeedback' });
+
+const pageRef = ref(null);
+const handleDialogRef = ref(null);
+
+const pageConfig = reactive({
+    fileName: '搜索反馈记录',
+    cacheKey: 'searchFeedbackTable',
+    rowKey: 'id'
+});
+
+const columns = ref([
+    { label: '序号', width: 60, type: 'index', align: 'center' },
+    { label: '状态', prop: 'status', width: 100, slot: 'status', align: 'center' },
+    { label: '反馈日期', prop: 'createTime', width: 160, align: 'center' },
+    { label: '用户名', prop: 'nickname', width: 150, align: 'center' },
+    { label: '反馈信息', prop: 'content', minWidth: 300, showOverflowTooltip: true },
+    { label: '打分', prop: 'score', width: 80, align: 'center' },
+    { label: '图片/视频', prop: 'media', width: 120, slot: 'media', align: 'center' },
+    { label: '操作', prop: 'action', width: 150, slot: 'action', fixed: 'right', align: 'center' }
+]);
+
+// Mock Datasource
+const datasource = ({ page, limit, where }) => {
+    return Promise.resolve({
+        code: 0,
+        msg: 'success',
+        count: 1,
+        data: [
+            {
+                id: 1,
+                status: 0,
+                createTime: '2024-06-15 15:00:66',
+                nickname: '书嗨12121634',
+                content: '出VB发货顺序该方法出VB发货顺序该方法出VB发货顺序该方法11454545454456767',
+                score: 3,
+                images: [
+                    'https://img1.baidu.com/it/u=2356551695,3062334057&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=707',
+                    'https://img1.baidu.com/it/u=2356551695,3062334057&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=707'
+                ]
+            }
+        ]
+    });
+};
+
+const reload = (where) => {
+    pageRef.value?.reload(where);
+};
+
+const handleProcess = (row) => {
+    handleDialogRef.value?.handleOpen(row);
+};
+
+const handleDetail = (row) => {
+    // 详情逻辑与处理逻辑类似,可能只是只读或者打开同一个弹窗
+    handleDialogRef.value?.handleOpen(row);
+};
+</script>

+ 96 - 0
src/views/goods/highComplaintBooks/components/add-dialog.vue

@@ -0,0 +1,96 @@
+<template>
+    <simple-form-modal :title="title" :items="formItems" ref="editRef" label-width="110px"
+        :submit-handler="handleSubmit" @success="handleSuccess">
+        <template #isbn="{ model }">
+            <el-input v-model="model.isbn" placeholder="请输入" @blur="() => checkIsbn(model)" />
+        </template>
+    </simple-form-modal>
+</template>
+
+<script setup>
+import { ref, computed } from 'vue';
+import { ElMessage } from 'element-plus';
+import SimpleFormModal from '@/components/CommonPage/SimpleFormModal.vue';
+
+const emit = defineEmits(['success']);
+const editRef = ref(null);
+const title = ref('添加图书');
+const currentId = ref(null);
+
+const formItems = computed(() => [
+    {
+        type: 'isbn', // Custom slot
+        label: 'ISBN:',
+        prop: 'isbn',
+        required: true
+    },
+    {
+        type: 'checkbox',
+        label: '易投诉平台:',
+        prop: 'platforms',
+        required: true,
+        options: [
+            { label: '拼多多', value: 'pdd' },
+            { label: '淘宝', value: 'taobao' },
+            { label: '小程序', value: 'miniapp' },
+            { label: '孔网', value: 'kongwang' }
+        ]
+    },
+    {
+        type: 'select',
+        label: '易投诉原因:',
+        prop: 'reason',
+        required: true,
+        options: [
+            { label: '出版社投诉', value: 'publisher' },
+            { label: '价格投诉', value: 'price' },
+            { label: '质量投诉', value: 'quality' }
+        ]
+    }
+]);
+
+const checkIsbn = (model) => {
+    // Mock check
+    if (model.isbn === '9787563215652' && !currentId.value) {
+        ElMessage.warning('重复添加,提示此ISBN已存在');
+    }
+};
+
+const handleOpen = (row) => {
+    if (row) {
+        title.value = '编辑图书';
+        currentId.value = row.id;
+        editRef.value?.handleOpen({
+            ...row,
+            platforms: row.platforms || []
+        });
+    } else {
+        title.value = '添加图书';
+        currentId.value = null;
+        editRef.value?.handleOpen({
+            isbn: '',
+            platforms: [],
+            reason: ''
+        });
+    }
+};
+
+const handleSubmit = (model) => {
+    return new Promise((resolve, reject) => {
+        // Mock save
+        // Simulate async request
+        setTimeout(() => {
+            resolve();
+            // Note: Success message is handled by SimpleFormModal if submitHandler resolves
+        }, 500);
+    });
+};
+
+const handleSuccess = () => {
+    emit('success');
+};
+
+defineExpose({
+    handleOpen
+});
+</script>

+ 113 - 0
src/views/goods/highComplaintBooks/components/import-dialog.vue

@@ -0,0 +1,113 @@
+<template>
+    <ele-modal :width="460" title="导入易投诉图书" :body-style="{ paddingTop: '8px' }" v-model="visible" position="center">
+        <div class="flex mb-2">
+            <div class="text-sm">请先下载'图书基础数据导出模板':</div>
+            <el-button @click="
+                downloadOssLink(
+                    'https://shuhi.oss-cn-qingdao.aliyuncs.com/default/search_template.xlsx',
+                    '图书基础数据导出模板'
+                )
+                " link type="primary">下载图书基础数据导出模板</el-button>
+        </div>
+
+        <div class="flex flex-col mb-2">
+            <div class="text-sm text-yellow-500">使用说明:</div>
+            <div class="text-sm">1.支持通过导入查询,导出数据结果</div>
+            <div class="text-sm">2.文件ISBN不存在或者错误,会自动过滤掉</div>
+            <div class="text-sm mb-2">3.导入文件第一行需与模版完全一致</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-button type="primary" class="mr-2">选择文件</el-button>
+                <ele-text type="placeholder">只能上传 xls、xlsx 文件</ele-text>
+            </el-upload>
+        </div>
+
+        <template #footer>
+            <el-button @click="visible = false">关闭</el-button>
+            <el-button type="primary" @click="handleSumbit" :loading="loading">导入</el-button>
+        </template>
+    </ele-modal>
+</template>
+
+<script setup>
+import { ref } from 'vue';
+import { genFileId } from 'element-plus';
+import { EleMessage } from 'ele-admin-plus/es';
+import { downloadOssLink } from '@/utils/common';
+// import request from '@/utils/request'; 
+import { ElMessage } from 'element-plus';
+
+const emit = defineEmits(['done']);
+
+/** 弹窗是否打开 */
+const visible = defineModel({ type: Boolean });
+
+/** 导入请求状态 */
+const loading = ref(false);
+
+const uploadRef = ref(null);
+const fileList = ref([]);
+
+function handleExceed(files) {
+    uploadRef.value?.clearFiles();
+    const file = files[0];
+    file.uid = genFileId();
+    uploadRef.value?.handleStart(file);
+}
+
+//导入图书 Mock
+async function importBooks(file) {
+    // const formData = new FormData();
+    // formData.append('file', file);
+    // const res = await request.post('/book/bookInfo/exportByFile', formData);
+    // if (res.data.code === 200) {
+    //     return res.data;
+    // }
+    // return Promise.reject(new Error(res.data.msg));
+    return new Promise((resolve) => {
+        setTimeout(() => {
+            resolve({ data: '导入成功' });
+        }, 1000);
+    });
+}
+
+/** 提交 */
+const handleSumbit = () => {
+    if (fileList.value.length === 0) {
+        EleMessage.warning('请选择文件');
+        return;
+    }
+    loading.value = true;
+    let file = fileList.value[0];
+    importBooks(file.raw)
+        .then((res) => {
+            loading.value = false;
+            ElMessage.success(res.data || res.msg || '导入成功');
+            visible.value = false;
+            emit('done');
+        })
+        .catch((e) => {
+            loading.value = false;
+        });
+};
+
+/** 上传 */
+const doUpload = (file) => {
+    if (
+        ![
+            'application/vnd.ms-excel',
+            'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
+        ].includes(file.type)
+    ) {
+        EleMessage.error('只能选择 excel 文件');
+        return false;
+    }
+    if (file.size / 1024 / 1024 > 10) {
+        EleMessage.error('大小不能超过 10MB');
+        return false;
+    }
+    return false;
+};
+</script>

+ 40 - 0
src/views/goods/highComplaintBooks/components/page-search.vue

@@ -0,0 +1,40 @@
+<template>
+    <ele-card :body-style="{ paddingBottom: '8px' }">
+        <ProSearch :items="formItems" @search="search" />
+    </ele-card>
+</template>
+
+<script setup>
+import { reactive } from 'vue';
+import ProSearch from '@/components/CommonPage/ProSearch2.vue';
+
+const emit = defineEmits(['search']);
+
+const formItems = reactive([
+    { type: 'input', label: 'ISBN', prop: 'isbn' },
+    { type: 'input', label: '书名', prop: 'bookName' },
+    { 
+        type: 'select', 
+        label: '易投诉平台', 
+        prop: 'platform',
+        options: [
+            { label: '拼多多', value: 'pdd' },
+            { label: '淘宝', value: 'taobao' },
+            { label: '孔网', value: 'kongwang' },
+            { label: '小程序', value: 'miniapp' }
+        ]
+    },
+    { 
+        type: 'select', 
+        label: '投诉原因', 
+        prop: 'reason',
+        options: [
+            { label: '出版社投诉', value: 'publisher' },
+            { label: '价格投诉', value: 'price' },
+            { label: '质量投诉', value: 'quality' }
+        ]
+    }
+]);
+
+const search = (data) => emit('search', data);
+</script>

+ 162 - 0
src/views/goods/highComplaintBooks/index.vue

@@ -0,0 +1,162 @@
+<template>
+    <ele-page flex-table>
+        <page-search @search="reload" />
+
+        <common-table ref="pageRef" :pageConfig="pageConfig" :columns="columns" :datasource="datasource">
+            <template #toolbar>
+                <div class="flex gap-2">
+                    <el-button type="primary" @click="handleImport">导入</el-button>
+                    <el-button type="success">导出</el-button>
+                    <el-button type="primary" @click="handleAdd">添加图书</el-button>
+                </div>
+            </template>
+
+            <!-- 易投诉平台列 -->
+            <template #platforms="{ row }">
+                <div class="flex flex-wrap gap-1">
+                    <el-tag v-for="platform in row.platforms" :key="platform" :type="getPlatformTagType(platform)"
+                        :color="getPlatformTagColor(platform)" :style="{ color: getPlatformTagTextColor(platform) }"
+                        size="small" effect="dark">
+                        {{ getPlatformLabel(platform) }}
+                    </el-tag>
+                </div>
+            </template>
+
+            <!-- 操作列 -->
+            <template #action="{ row }">
+                <el-button link type="primary" @click="handleEdit(row)">[编辑]</el-button>
+                <el-button link type="danger" @click="handleDelete(row)">[删除]</el-button>
+            </template>
+        </common-table>
+
+        <add-dialog ref="addDialogRef" @success="reload" />
+        <import-dialog v-model="importVisible" @done="reload" />
+    </ele-page>
+</template>
+
+<script setup>
+import { ref, reactive } from 'vue';
+import { ElMessageBox, ElMessage } from 'element-plus';
+import CommonTable from '@/components/CommonPage/CommonTable.vue';
+import PageSearch from './components/page-search.vue';
+import AddDialog from './components/add-dialog.vue';
+import ImportDialog from './components/import-dialog.vue';
+
+defineOptions({ name: 'HighComplaintBooks' });
+
+const pageRef = ref(null);
+const addDialogRef = ref(null);
+const importVisible = ref(false);
+
+const pageConfig = reactive({
+    fileName: '易投诉书单',
+    cacheKey: 'highComplaintBooksTable',
+    rowKey: 'id'
+});
+
+const columns = ref([
+    { label: 'ISBN', prop: 'isbn', width: 140, align: 'center' },
+    { label: '书名', prop: 'bookName', minWidth: 150 },
+    { label: '易投诉平台', prop: 'platforms', width: 220, slot: 'platforms' },
+    { label: '投诉原因', prop: 'reasonLabel', width: 120 },
+    { label: '操作员', prop: 'operator', width: 100, align: 'center' },
+    { label: '添加日期', prop: 'createTime', width: 160, align: 'center' },
+    { label: '操作', prop: 'action', width: 120, slot: 'action', fixed: 'right', align: 'center' }
+]);
+
+// Helper functions for tags
+const getPlatformLabel = (val) => {
+    const map = {
+        'pdd': '拼多多',
+        'taobao': '淘宝',
+        'kongwang': '孔网',
+        'miniapp': '小程序'
+    };
+    return map[val] || val;
+};
+
+const getPlatformTagType = (val) => {
+    if (val === 'pdd') return 'danger';
+    if (val === 'miniapp') return 'success';
+    return '';
+};
+
+const getPlatformTagColor = (val) => {
+    if (val === 'taobao') return '#f90'; // Orange
+    if (val === 'kongwang') return '#8B4513'; // Brown
+    return '';
+};
+
+const getPlatformTagTextColor = (val) => {
+    if (val === 'taobao' || val === 'kongwang') return '#fff';
+    return '';
+};
+
+// Mock Datasource
+const datasource = ({ page, limit, where }) => {
+    return Promise.resolve({
+        code: 0,
+        msg: 'success',
+        count: 1,
+        data: [
+            {
+                id: 1,
+                isbn: '9787563215652',
+                bookName: '人世间',
+                platforms: ['pdd', 'taobao', 'kongwang', 'miniapp'],
+                reason: 'publisher',
+                reasonLabel: '出版社投诉',
+                operator: 'zzz',
+                createTime: '2022-05-15 15:00:00'
+            },
+            {
+                id: 2,
+                isbn: '9787563215652',
+                bookName: '人世间',
+                platforms: [],
+                reason: '',
+                reasonLabel: '',
+                operator: '',
+                createTime: ''
+            },
+            {
+                id: 3,
+                isbn: '9787563215652',
+                bookName: '人世间',
+                platforms: [],
+                reason: '',
+                reasonLabel: '',
+                operator: '',
+                createTime: ''
+            }
+        ]
+    });
+};
+
+const reload = (where) => {
+    pageRef.value?.reload(where);
+};
+
+const handleAdd = () => {
+    addDialogRef.value?.handleOpen();
+};
+
+const handleEdit = (row) => {
+    addDialogRef.value?.handleOpen(row);
+};
+
+const handleDelete = (row) => {
+    ElMessageBox.confirm('确定要删除该条记录吗?', '提示', {
+        type: 'warning',
+        confirmButtonText: '确定',
+        cancelButtonText: '取消'
+    }).then(() => {
+        ElMessage.success('删除成功');
+        reload();
+    }).catch(() => { });
+};
+
+const handleImport = () => {
+    importVisible.value = true;
+};
+</script>