Alex 9 месяцев назад
Родитель
Сommit
4206603f79

+ 116 - 0
src/views/data/productPush/components/page-search.vue

@@ -0,0 +1,116 @@
+<!-- 搜索表单 -->
+<template>
+  <ele-card :body-style="{ paddingBottom: '8px' }">
+    <ProSearch
+      :items="formItems"
+      ref="searchRef"
+      @search="search"
+      :initKeys="initKeys"
+    ></ProSearch>
+  </ele-card>
+</template>
+
+<script setup>
+  import { reactive, ref, defineEmits, defineProps, computed, getCurrentInstance } from 'vue';
+  import ProSearch from '@/components/CommonPage/ProSearch2.vue';
+
+  defineOptions({ name: 'page-search' });
+
+  const props = defineProps({
+    status: {
+      type: String,
+      default: ''
+    }
+  });
+
+  const emit = defineEmits(['search']);
+  const searchRef = ref(null);
+
+  // 初始值
+  const initKeys = reactive({
+    isbn: '',
+    processor: '',
+    isPush: '',
+    pushStatus: props.status,
+    createTimeRange: [],
+    pushTimeRange: []
+  });
+
+  // 表单项定义
+  const formItems = computed(() => {
+    return [
+      {
+        type: 'input',
+        label: 'ISBN',
+        prop: 'isbn'
+      },
+      {
+        type: 'input',
+        label: '处理人',
+        prop: 'processor'
+      },
+      {
+        type: 'select',
+        label: '是否推送',
+        prop: 'isPush',
+        options: [
+          { label: '是', value: '是' },
+          { label: '否', value: '否' }
+        ]
+      },
+      {
+        type: 'select',
+        label: '推送状态',
+        prop: 'pushStatus',
+        options: [
+          { label: '成功', value: '成功' },
+          { label: '失败', value: '失败' },
+          { label: '系统错误', value: '系统错误' },
+          { label: '无数据', value: '无数据' }
+        ]
+      },
+      {
+        type: 'daterange',
+        label: '创建时间',
+        prop: 'createTimeRange',
+        props: {
+          valueFormat: 'YYYY-MM-DD HH:mm:ss',
+          rangeSeparator: '到',
+          startPlaceholder: '开始日期',
+          endPlaceholder: '结束日期'
+        }
+      },
+      {
+        type: 'daterange',
+        label: '推送时间',
+        prop: 'pushTimeRange',
+        props: {
+          valueFormat: 'YYYY-MM-DD HH:mm:ss',
+          rangeSeparator: '到',
+          startPlaceholder: '开始日期',
+          endPlaceholder: '结束日期'
+        }
+      }
+    ];
+  });
+
+  // 搜索
+  const search = (data) => {
+    const params = { ...data };
+
+    // 处理时间范围
+    if (params.createTimeRange && params.createTimeRange.length) {
+      params.createTimeStart = params.createTimeRange[0];
+      params.createTimeEnd = params.createTimeRange[1];
+      delete params.createTimeRange;
+    }
+
+    if (params.pushTimeRange && params.pushTimeRange.length) {
+      params.pushTimeStart = params.pushTimeRange[0];
+      params.pushTimeEnd = params.pushTimeRange[1];
+      delete params.pushTimeRange;
+    }
+
+    emit('search', params);
+  };
+</script>

+ 158 - 0
src/views/data/productPush/index.vue

@@ -0,0 +1,158 @@
+<template>
+  <ele-page flex-table>
+    <page-search @search="reload" :status="useStatus"></page-search>
+
+    <common-table ref="pageRef" :pageConfig="pageConfig" :columns="columns">
+      <template #toolbar>
+        <div class="flex items-center mb-4">
+          <el-button type="warning" plain @click="handleBatchPush">
+            一键推送
+          </el-button>
+          <el-button
+            type="success"
+            plain
+            @click="handleExportExcel"
+            :icon="DownloadOutlined"
+          >
+            导出
+          </el-button>
+        </div>
+      </template>
+
+      <template #isPush="{ row }">
+        <el-tag type="success" v-if="row.isPush === '是'">是</el-tag>
+        <el-tag type="danger" v-else>否</el-tag>
+      </template>
+
+      <template #pushStatus="{ row }">
+        <el-tag type="success" v-if="row.pushStatus === '成功'">成功</el-tag>
+        <el-tag type="danger" v-if="row.pushStatus === '失败'">失败</el-tag>
+        <el-tag type="warning" v-if="row.pushStatus === '系统错误'"
+          >系统错误</el-tag
+        >
+        <el-tag type="info" v-if="row.pushStatus === '无数据'">无数据</el-tag>
+      </template>
+
+      <template #action="{ row }">
+        <div>
+          <el-button type="primary" link @click="handleDetail(row)">
+            删除
+          </el-button>
+          <el-button type="primary" link @click="handlePush(row)">
+            推送
+          </el-button>
+          <el-button type="primary" link @click="handlePushLog(row)">
+            推送日志
+          </el-button>
+        </div>
+      </template>
+    </common-table>
+  </ele-page>
+</template>
+
+<script setup>
+  import { ref, reactive } from 'vue';
+  import { ElMessageBox } from 'element-plus/es';
+  import { EleMessage } from 'ele-admin-plus/es';
+  import { DownloadOutlined } from '@/components/icons';
+  import CommonTable from '@/components/CommonPage/CommonTable.vue';
+  import pageSearch from './components/page-search.vue';
+  import request from '@/utils/request';
+
+  defineOptions({ name: 'productPush' });
+
+  const useStatus = ref('');
+
+  /** 表格列配置 */
+  const columns = ref([
+    { label: '序号', prop: 'index', align: 'center', width: 70 },
+    { label: 'ISBN', prop: 'isbn', align: 'center', minWidth: 120 },
+    { label: '是否推送', prop: 'isPush', align: 'center', slot: 'isPush' },
+    { label: '创建时间', prop: 'createTime', align: 'center', width: 180 },
+    { label: '推送时间', prop: 'pushTime', align: 'center', width: 180 },
+    { label: '扫描提交人', prop: 'submitter', align: 'center' },
+    { label: '后台处理人', prop: 'processor', align: 'center' },
+    {
+      label: '推送状态',
+      prop: 'pushStatus',
+      align: 'center',
+      slot: 'pushStatus'
+    },
+    {
+      label: '操作',
+      width: 240,
+      align: 'center',
+      slot: 'action'
+    }
+  ]);
+
+  /** 页面组件实例 */
+  const pageRef = ref(null);
+
+  const pageConfig = reactive({
+    pageUrl: '/data/product/push/list',
+    exportUrl: '/data/product/push/export',
+    fileName: '商品档案推送',
+    cacheKey: 'productPushTable'
+  });
+
+  // 刷新表格
+  function reload(where) {
+    pageRef.value?.reload(where);
+  }
+
+  // 导出excel
+  function handleExportExcel() {
+    pageRef.value?.exportData('商品档案推送');
+  }
+
+  // 批量推送
+  function handleBatchPush() {
+    ElMessageBox.confirm('确认批量推送选中的商品档案?', '提示', {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'warning'
+    })
+      .then(() => {
+        request.post('/data/product/push/batchPush').then((res) => {
+          if (res.data.code === 200) {
+            EleMessage.success('批量推送成功');
+            reload();
+          } else {
+            EleMessage.error(res.data.msg || '批量推送失败');
+          }
+        });
+      })
+      .catch(() => {});
+  }
+
+  // 推送单个商品
+  function handlePush(row) {
+    ElMessageBox.confirm(`确认推送ISBN为${row.isbn}的商品档案?`, '提示', {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'warning'
+    })
+      .then(() => {
+        request.post(`/data/product/push/push/${row.isbn}`).then((res) => {
+          if (res.data.code === 200) {
+            EleMessage.success('推送成功');
+            reload();
+          } else {
+            EleMessage.error(res.data.msg || '推送失败');
+          }
+        });
+      })
+      .catch(() => {});
+  }
+
+  // 查看详情
+  function handleDetail(row) {
+    // 实现详情查看逻辑
+  }
+
+  // 查看推送日志
+  function handlePushLog(row) {
+    // 实现推送日志查看逻辑
+  }
+</script>

+ 144 - 0
src/views/finance/subsidyReview/index.vue

@@ -0,0 +1,144 @@
+<template>
+  <ele-page flex-table>
+    <page-search @search="reload" :status="useStatus"></page-search>
+
+    <common-table ref="pageRef" :pageConfig="pageConfig" :columns="columns">
+      <template #toolbar>
+        <div class="flex items-center mb-4">
+          <el-statistic
+            :value="statistics.totalMoney"
+            title="累计申请金额"
+            :precision="2"
+            value-style="font-size:30px"
+            class="mr-20"
+          ></el-statistic>
+          <el-statistic
+            :value="statistics.unFinishMoney"
+            title="待审核补贴"
+            :precision="0"
+            value-style="font-size:30px"
+            class="mr-20"
+          ></el-statistic>
+          <el-statistic
+            :value="statistics.finishMoney"
+            title="已审核补贴金额"
+            :precision="0"
+            value-style="font-size:30px"
+            class="mr-20"
+          ></el-statistic>
+        </div>
+
+        <div class="common-title mb-4">交易记录</div>
+
+        <el-radio-group @change="handleStatusChange" v-model="useStatus">
+          <el-radio-button label="全部" value="" />
+          <el-radio-button label="待审核" value="1" />
+          <el-radio-button label="已审核" value="2" />
+        </el-radio-group>
+      </template>
+
+      <template #status="{ row }">
+        {{ statusDicts.find((d) => d.value == row.status)?.label }}
+      </template>
+
+      <template #orderId="{ row }">
+        <el-button type="primary" link @click="handleOrderId(row)">{{
+          row.orderId
+        }}</el-button>
+      </template>
+    </common-table>
+
+    <orderDetail ref="orderDetailRef" />
+  </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 request from '@/utils/request';
+  import OrderDetail from '@/views/recycleOrder/components/order-detail.vue';
+
+  defineOptions({ name: 'subsidyReview' });
+
+  // 添加统计数据的响应式对象
+  const statistics = reactive({
+    totalMoney: 0,
+    unFinishMoney: 0,
+    finishMoney: 0
+  });
+
+  // 获取统计数据
+  async function fetchStatistics() {
+    try {
+      const res = await request.get('/sys/finance/subsidySum');
+      if (res.data.code === 200) {
+        Object.assign(statistics, res.data.data);
+      }
+    } catch (error) {
+      console.error('获取统计数据失败:', error);
+    }
+  }
+
+  const orderDetailRef = ref(null);
+  const handleOrderId = (row) => {
+    orderDetailRef.value?.handleOpen(row);
+  };
+
+  onMounted(() => {
+    fetchStatistics();
+  });
+
+  const statusDicts = ref([
+    { label: '待审核', value: 1 },
+    { label: '已审核', value: 2 }
+  ]);
+
+  const useStatus = ref('');
+  function handleStatusChange(value) {
+    pageRef.value.reload({ status: value });
+  }
+
+  /** 表格列配置 */
+  const columns = ref([
+    { label: '申请时间', prop: 'applyTime', align: 'center', width: 180 },
+    { label: '用户UID', prop: 'userId', align: 'center' },
+    {
+      label: '支付单号/流水号',
+      prop: 'paymentNo',
+      align: 'center',
+      minWidth: 180,
+      formatter: (row) => row.paymentNo || '-'
+    },
+    { label: '对方账户', prop: 'nickName', align: 'center', minWidth: 140 },
+    { label: '结算金额', prop: 'amount', align: 'center' },
+    { label: '交易状态', prop: 'status', align: 'center', slot: 'status' },
+    {
+      label: '交易类型',
+      prop: 'type',
+      align: 'center',
+      formatter: (row) => (row.status == 2 ? '超时审核' : '变与必箱')
+    },
+    {
+      label: '订单编号',
+      prop: 'orderId',
+      align: 'center',
+      minWidth: 120,
+      slot: 'orderId'
+    }
+  ]);
+
+  /** 页面组件实例 */
+  const pageRef = ref(null);
+
+  const pageConfig = reactive({
+    pageUrl: '/sys/finance/subsidyList',
+    fileName: '补贴审核',
+    cacheKey: 'subsidyReview'
+  });
+
+  //刷新表格
+  function reload(where) {
+    pageRef.value?.reload(where);
+  }
+</script>

+ 83 - 0
src/views/finance/subsidyReview/page-search.vue

@@ -0,0 +1,83 @@
+<!-- 搜索表单 -->
+<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, getCurrentInstance } from 'vue';
+  import ProSearch from '@/components/CommonPage/ProSearch2.vue';
+
+  let { proxy } = getCurrentInstance();
+  const emit = defineEmits(['search']);
+
+  const props = defineProps({
+    status: {
+      type: String,
+      default: ''
+    }
+  });
+
+  const formItems = reactive([
+    { type: 'input', label: '输入用户UID', prop: 'userId' },
+    {
+      type: 'select',
+      label: '请选择状态',
+      prop: 'status',
+      options: [
+        { label: '待审核', value: 1 },
+        { label: '已审核', value: 2 }
+      ]
+    },
+    {
+      type: 'select',
+      label: '请选择交易类型',
+      prop: 'type',
+      options: [
+        { label: '变与必箱', value: 1 },
+        { label: '超时审核', value: 2 }
+      ]
+    },
+    {
+      type: 'daterange',
+      label: '时间',
+      prop: 'timeRange',
+      props: {
+        valueFormat: 'YYYY-MM-DD',
+        format: 'YYYY-MM-DD',
+        startPlaceholder: '开始时间',
+        endPlaceholder: '结束时间',
+        onChange: (value) => {
+          initKeys.startTime = value ? value[0] : '';
+          initKeys.endTime = value ? value[1] : '';
+          searchRef.value?.setData(initKeys);
+        }
+      }
+    }
+  ]);
+
+  const initKeys = reactive({
+    startTime: '',
+    endTime: '',
+    userId: '',
+    status: '',
+    type: ''
+  });
+
+  const searchRef = ref(null);
+  /** 搜索 */
+  const search = (data) => {
+    emit('search', { ...data });
+  };
+</script>

+ 134 - 0
src/views/marketing/ipMrakup/booklist/components/booklist-import.vue

@@ -0,0 +1,134 @@
+<template>
+  <ele-modal
+    :width="460"
+    title="图书导入"
+    :body-style="{ paddingTop: '8px' }"
+    v-model="visible"
+  >
+    <div class="flex mb-2">
+      <div class="text-sm">请先下载'图书基础数据导入模板':</div>
+      <el-button
+        @click="
+          downloadOssLink(
+            'https://shuhi.oss-cn-qingdao.aliyuncs.com/default/book_info_export_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.导入文件属性值不存在或者超长,会自动过滤掉</div>
+      <div class="text-sm mb-2">3.导入文件第一行需与模版完全一致</div>
+
+      <el-radio-group v-model="coverImport">
+        <el-radio :value="1">相同ISBN覆盖导入</el-radio>
+        <el-radio :value="0">相同ISBN忽略导入</el-radio>
+      </el-radio-group>
+    </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, h } from 'vue';
+  import { genFileId } from 'element-plus';
+  import { ElMessageBox } from 'element-plus/es';
+  import { EleMessage } from 'ele-admin-plus/es';
+  import { CloudUploadOutlined } from '@/components/icons';
+  import { downloadOssLink } from '@/utils/common';
+  import request from '@/utils/request';
+
+  const emit = defineEmits(['done']);
+
+  /** 弹窗是否打开 */
+  const visible = defineModel({ type: Boolean });
+
+  /** 导入请求状态 */
+  const loading = ref(false);
+  /** 是否覆盖 */
+  const coverImport = ref(1);
+
+  const uploadRef = ref(null);
+  const fileList = ref([]);
+
+  function handleExceed(files) {
+    uploadRef.value?.clearFiles();
+    const file = files[0];
+    file.uid = genFileId();
+    uploadRef.value?.handleStart(file);
+  }
+
+  //导入图书
+  async function importBooks(file) {
+    const formData = new FormData();
+    formData.append('file', file);
+    formData.append('coverImport', coverImport.value);
+
+    const res = await request.post('/book/bookInfo/import', formData);
+    if (res.data.code === 200) {
+      return res.data.msg;
+    }
+    return Promise.reject(new Error(res.data.msg));
+  }
+
+  /** 提交 */
+  const handleSumbit = () => {
+    loading.value = true;
+    let file = fileList.value[0];
+    importBooks(file.raw)
+      .then((msg) => {
+        loading.value = false;
+        ElMessage.success(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>

+ 225 - 0
src/views/marketing/ipMrakup/booklist/index.vue

@@ -0,0 +1,225 @@
+<template>
+  <ele-page flex-table :bodyStyle="{ padding: 0 }">
+    <common-table
+      ref="pageRef"
+      :pageConfig="pageConfig"
+      :columns="columns"
+      :bodyStyle="{ padding: 0 }"
+    >
+      <template #toolbar>
+        <el-button
+          type="primary"
+          plain
+          :icon="PlusOutlined"
+          v-permission="'ipMrakup:booklist:add'"
+          @click="handleUpdate()"
+        >
+          新建
+        </el-button>
+        <el-button
+          type="danger"
+          plain
+          :icon="DeleteOutlined"
+          v-permission="'ipMrakup:booklist:batchDelete'"
+          @click="handleBatchDelete()"
+        >
+          批量删除
+        </el-button>
+        <el-button
+          type="primary"
+          plain
+          v-permission="'ipMrakup:booklist:import'"
+          @click="handleImportExcel"
+          :icon="UploadOutlined"
+        >
+          导入
+        </el-button>
+        <el-button
+          type="success"
+          plain
+          v-permission="'ipMrakup:booklist:export'"
+          @click="handleExportExcel"
+          :icon="DownloadOutlined"
+        >
+          导出
+        </el-button>
+      </template>
+      <template #cover="{ row }">
+        <el-image
+          style="width: 80px; height: 100px"
+          fit="cover"
+          :src="row.cover"
+          :preview-src-list="[row.cover]"
+          :initial-index="0"
+          preview-teleported
+        />
+      </template>
+
+      <template #action="{ row }">
+        <div>
+          <el-button
+            type="warning"
+            link
+            v-permission="'ipMrakup:booklist:delete'"
+            @click="handleDetail(row)"
+          >
+            [删除]
+          </el-button>
+        </div>
+      </template>
+    </common-table>
+    <booklist-import
+      ref="importRef"
+      v-model="showImport"
+      @done="reload()"
+    ></booklist-import>
+  </ele-page>
+</template>
+
+<script setup>
+  import { ref, reactive } from 'vue';
+  import {
+    PlusOutlined,
+    DeleteOutlined,
+    DownloadOutlined,
+    UploadOutlined
+  } from '@/components/icons';
+  import CommonTable from '@/components/CommonPage/CommonTable.vue';
+  import booklistImport from './components/booklist-import.vue';
+
+  defineOptions({ name: 'ipMarkupBooklist' });
+
+  /** 表格列配置 */
+  const columns = ref([
+    {
+      type: 'selection',
+      columnKey: 'selection',
+      width: 50,
+      align: 'center',
+      fixed: 'left'
+    },
+    {
+      label: '图片',
+      prop: 'cover',
+      slot: 'cover',
+      align: 'center',
+      width: 100
+    },
+    {
+      label: 'ISBN',
+      prop: 'isbn',
+      align: 'center',
+      width: 150
+    },
+    {
+      label: '书名',
+      prop: 'bookName',
+      align: 'center',
+      minWidth: 150
+    },
+    {
+      label: '作者',
+      prop: 'author',
+      align: 'center',
+      width: 120
+    },
+    {
+      label: '出版社',
+      prop: 'publisher',
+      align: 'center',
+      width: 150
+    },
+    {
+      label: '出版时间',
+      prop: 'publishTime',
+      align: 'center',
+      width: 120
+    },
+    {
+      label: '定价',
+      prop: 'price',
+      align: 'center',
+      width: 80
+    },
+    {
+      label: '回收折扣',
+      prop: 'discount',
+      align: 'center',
+      width: 80
+    },
+    {
+      label: '加价金额',
+      prop: 'addPrice',
+      align: 'center',
+      width: 80
+    },
+    {
+      label: '添加时间',
+      prop: 'addTime',
+      align: 'center',
+      width: 160
+    },
+    {
+      columnKey: 'action',
+      label: '操作',
+      width: 150,
+      align: 'center',
+      slot: 'action'
+    }
+  ]);
+
+  /** 页面组件实例 */
+  const pageRef = ref(null);
+
+  const pageConfig = reactive({
+    pageUrl: '/marketing/ipMrakup/booklist/list',
+    exportUrl: '/marketing/ipMrakup/booklist/export',
+    fileName: 'IP营销书单',
+    cacheKey: 'ip-markup-booklist'
+  });
+
+  // 导出excel
+  function handleExportExcel() {
+    pageRef.value?.exportData('IP营销书单');
+  }
+
+  // 导入
+  const showImport = ref(false);
+  function handleImportExcel() {
+    showImport.value = true;
+  }
+
+  // 刷新表格
+  function reload(where) {
+    pageRef.value?.reload(where);
+  }
+
+  // 批量删除
+  function handleBatchDelete(row) {
+    let selections = row ? [row] : pageRef.value?.getSelections();
+    if (!selections || selections.length === 0) {
+      ElMessage.warning('请选择要删除的记录');
+      return;
+    }
+    let ids = selections.map((item) => item.id).join(',');
+    let url = `/marketing/ipMrakup/booklist/removeById/${ids}`;
+    pageRef.value?.operatBatch({
+      title: '确认删除?',
+      method: 'post',
+      url,
+      row
+    });
+  }
+
+  // 新增编辑
+  const editRef = ref(null);
+  function handleUpdate(row) {
+    editRef.value?.handleOpen(row);
+  }
+
+  // 详情
+  const detailRef = ref(null);
+  function handleDetail(row) {
+    detailRef.value?.handleOpen(row);
+  }
+</script>

+ 73 - 111
src/views/marketing/ipMrakup/datalist/components/page-search.vue

@@ -1,127 +1,89 @@
 <!-- 搜索表单 -->
 <template>
-  <ele-card :body-style="{ paddingBottom: '8px' }">
-    <el-form ref="formRef" :model="form" label-width="80px" inline>
-      <el-row :gutter="12">
-        <el-col :span="6">
-          <el-form-item label="IP标题">
-            <el-input
-              v-model="form.name"
-              placeholder="请输入IP标题"
-              clearable
-            />
-          </el-form-item>
-        </el-col>
-        <el-col :span="6">
-          <el-form-item label="IP序号">
-            <el-input v-model="form.id" placeholder="请输入IP序号" clearable />
-          </el-form-item>
-        </el-col>
-        <el-col :span="6">
-          <el-form-item label="类型">
-            <el-select
-              v-model="form.type"
-              placeholder="请选择类型"
-              clearable
-              style="width: 100%"
-            >
-              <el-option label="全部" value="" />
-              <el-option label="小说" value="novel" />
-              <el-option label="动漫" value="anime" />
-              <el-option label="电影" value="movie" />
-              <el-option label="综艺" value="variety" />
-              <el-option label="其他" value="other" />
-            </el-select>
-          </el-form-item>
-        </el-col>
-        <el-col :span="6">
-          <el-form-item label="来源">
-            <el-select
-              v-model="form.source"
-              placeholder="请选择来源"
-              clearable
-              style="width: 100%"
-            >
-              <el-option label="全部" value="" />
-              <el-option label="原创" value="original" />
-              <el-option label="转载" value="repost" />
-              <el-option label="合作" value="cooperation" />
-            </el-select>
-          </el-form-item>
-        </el-col>
-      </el-row>
-      <el-row :gutter="12">
-        <el-col :span="6">
-          <el-form-item label="状态">
-            <el-select
-              v-model="form.status"
-              placeholder="请选择状态"
-              clearable
-              style="width: 100%"
-            >
-              <el-option label="全部" value="" />
-              <el-option label="已启用" value="1" />
-              <el-option label="已禁用" value="0" />
-            </el-select>
-          </el-form-item>
-        </el-col>
-        <el-col :span="6">
-          <el-form-item label="发布日期">
-            <el-date-picker
-              v-model="form.timeRange"
-              type="daterange"
-              range-separator="-"
-              start-placeholder="开始日期"
-              end-placeholder="结束日期"
-              style="width: 100%"
-            />
-          </el-form-item>
-        </el-col>
-        <el-col :span="12" style="text-align: right">
-          <el-button type="primary" @click="handleSearch">查询</el-button>
-          <el-button type="info" @click="handleReset">重置</el-button>
-        </el-col>
-      </el-row>
-    </el-form>
+  <ele-card :body-style="{ padding: '0', paddingTop: '30px' }">
+    <ProSearch
+      :items="formItems"
+      ref="searchRef"
+      @search="search"
+      :initKeys="initKeys"
+    >
+    </ProSearch>
   </ele-card>
 </template>
 
 <script setup>
-  import { ref, reactive } from 'vue';
+  import { reactive, ref, defineEmits, getCurrentInstance } from 'vue';
+  import ProSearch from '@/components/CommonPage/ProSearch2.vue';
 
-  const emit = defineEmits(['search']);
+  let { proxy } = getCurrentInstance();
+  const emit = defineEmits(['search', 'exportExcel']);
 
-  // 表单数据
-  const form = reactive({
-    name: '',
-    id: '',
-    type: '',
-    source: '',
-    status: '',
-    timeRange: []
-  });
+  const formItems = reactive([
+    {
+      type: 'input',
+      label: '活动名称',
+      prop: 'activityName',
+      placeholder: '请输入活动名称'
+    },
+    {
+      type: 'input',
+      label: '活动地区',
+      prop: 'activityRegion',
+      placeholder: '请输入活动地区'
+    },
+    {
+      type: 'date',
+      label: '开始时间',
+      prop: 'startTime',
+      props: {
+        valueFormat: 'YYYY-MM-DD HH:mm:ss',
+        format: 'YYYY-MM-DD HH:mm:ss',
+        placeholder: '开始时间',
+      }
+    },
+    {
+      type: 'date',
+      label: '结束时间',
+      prop: 'endTime',
+      props: {
+        valueFormat: 'YYYY-MM-DD HH:mm:ss',
+        format: 'YYYY-MM-DD HH:mm:ss',
+        placeholder: '结束时间',
+      }
+    },
+    {
+      type: 'input',
+      label: '用户ID',
+      prop: 'userId',
+      placeholder: '请输入用户ID'
+    },
+    {
+      type: 'input',
+      label: '订单编号',
+      prop: 'orderNo',
+      placeholder: '请输入订单编号'
+    }
+  ]);
 
-  // 表单引用
-  const formRef = ref(null);
+  const initKeys = reactive({
+    activityName: '',
+    activityRegion: '',
+    startTime: '',
+    endTime: '',
+    userId: '',
+    orderNo: ''
+  });
 
-  // 处理搜索
-  const handleSearch = () => {
-    // 转换日期范围为开始时间和结束时间
-    const params = { ...form };
-    if (params.timeRange && params.timeRange.length > 0) {
-      params.startTime = params.timeRange[0];
-      params.endTime = params.timeRange[1];
-    }
-    delete params.timeRange;
+  const searchRef = ref(null);
 
-    emit('search', params);
+  /** 搜索 */
+  const search = (data) => {
+    emit('search', { ...data });
   };
 
-  // 处理重置
-  const handleReset = () => {
-    formRef.value.resetFields();
-    emit('search', {});
-  };
+  defineExpose({
+    search
+  });
 </script>
 
 <style lang="scss" scoped>

+ 127 - 155
src/views/marketing/ipMrakup/datalist/index.vue

@@ -1,33 +1,46 @@
 <!-- IP营销数据列表 -->
 <template>
-  <div class="app-container">
+  <ele-page flex-table>
     <!-- 搜索表单 -->
-    <page-search @search="handleSearch" />
+    <page-search ref="searchRef" @search="handleSearch" />
 
-    <!-- 表格数据 -->
-    <ele-card>
-      <ele-pro-table
+    <!-- 表格 -->
+    <ele-card flex-table :bodyStyle="{ padding: 0 }">
+      <CommonTable
         ref="tableRef"
-        row-key="id"
+        :pageConfig="pageConfig"
         :columns="columns"
-        :datasource="datasource"
-        :show-overflow-tooltip="true"
-        v-model:selections="selections"
-        highlight-current-row
-        border
+        :bodyStyle="{ padding: 0 }"
       >
-        <!-- 渲染操作按钮 -->
-        <template #action="{ row }">
-          <el-button link type="primary" @click="handleViewDetail(row)">查看详情</el-button>
-          <el-button link type="primary" @click="handleShare(row)">分享</el-button>
-        </template>
-
-        <!-- 渲染状态 -->
+        <!-- 状态列 -->
         <template #status="{ row }">
           <el-tag type="success" v-if="row.status === 1">已启用</el-tag>
           <el-tag type="danger" v-else>已禁用</el-tag>
         </template>
-      </ele-pro-table>
+
+        <!-- 操作列 -->
+        <template #action="{ row }">
+          <div class="action-buttons">
+            <el-button
+              type="primary"
+              link
+              v-permission="'marketing:ipMarkup:datalist:detail'"
+              @click="handleViewDetail(row)"
+            >
+              [查看详情]
+            </el-button>
+
+            <el-button
+              type="success"
+              link
+              v-permission="'marketing:ipMarkup:datalist:share'"
+              @click="handleShare(row)"
+            >
+              [分享订单]
+            </el-button>
+          </div>
+        </template>
+      </CommonTable>
     </ele-card>
 
     <!-- 查看详情弹窗 -->
@@ -35,149 +48,108 @@
 
     <!-- 分享信息弹窗 -->
     <share-info ref="shareRef" />
-  </div>
+  </ele-page>
 </template>
 
 <script setup>
-import { ref, reactive } from 'vue';
-import { getCurrentInstance } from 'vue';
-import PageSearch from './components/page-search.vue';
-import ViewDetail from './components/view-detail.vue';
-import ShareInfo from './components/share-info.vue';
-
-const { proxy } = getCurrentInstance();
-
-// 表格实例
-const tableRef = ref(null);
-
-// 查看详情弹窗实例
-const detailRef = ref(null);
-
-// 分享信息弹窗实例
-const shareRef = ref(null);
-
-// 表格选中数据
-const selections = ref([]);
-
-// 表格列配置
-const columns = ref([
-  {
-    type: 'selection',
-    columnKey: 'selection',
-    width: 50,
-    align: 'center',
-    fixed: 'left'
-  },
-  {
-    prop: 'id',
-    label: 'IP序号',
-    align: 'center',
-    width: 100
-  },
-  {
-    prop: 'name',
-    label: 'IP标题',
-    align: 'center',
-    minWidth: 150
-  },
-  {
-    prop: 'time',
-    label: '发布时间',
-    align: 'center',
-    width: 160
-  },
-  {
-    prop: 'type',
-    label: '类型',
-    align: 'center',
-    width: 100
-  },
-  {
-    prop: 'source',
-    label: '来源',
-    align: 'center',
-    width: 100
-  },
-  {
-    prop: 'views',
-    label: '浏览量',
-    align: 'center',
-    width: 100
-  },
-  {
-    prop: 'likes',
-    label: '点赞数',
-    align: 'center',
-    width: 100
-  },
-  {
-    prop: 'shares',
-    label: '分享次数',
-    align: 'center',
-    width: 100
-  },
-  {
-    prop: 'status',
-    label: '状态',
-    align: 'center',
-    width: 100,
-    slot: 'status'
-  },
-  {
-    columnKey: 'action',
-    label: '操作',
-    width: 160,
-    align: 'center',
-    slot: 'action',
-    hideInPrint: true,
-    hideInExport: true
-  }
-]);
-
-// 查询参数
-const queryParams = ref({
-  pageNum: 1,
-  pageSize: 20
-});
-
-// 数据源
-const datasource = ({ pages, where, orders }) => {
-  // 这里应该替换为实际的API调用
-  return proxy.$http.get('/marketing/ip/list', {
-    params: {
-      ...where,
-      ...orders,
-      ...pages
-    }
-  }).then(res => {
-    if (res.data.code === 200) {
-      return res.data;
-    }
-    return Promise.reject(new Error(res.data.msg));
+  import { ref, reactive } from 'vue';
+  import { ElMessage } from 'element-plus';
+  import CommonTable from '@/components/CommonPage/CommonTable.vue';
+  import PageSearch from './components/page-search.vue';
+  import ViewDetail from './components/view-detail.vue';
+  import ShareInfo from './components/share-info.vue';
+  import request from '@/utils/request';
+  import { DownloadOutlined } from '@/components/icons';
+
+  // 表格实例
+  const tableRef = ref(null);
+  const searchRef = ref(null);
+  const detailRef = ref(null);
+  const shareRef = ref(null);
+
+  // 页面配置
+  const pageConfig = reactive({
+    pageUrl: '/order/list',
+    exportUrl: '/order/export',
+    fileName: '订单管理列表',
+    cacheKey: 'orderManagementTable',
+    rowKey: 'id'
   });
-};
 
-// 搜索处理
-const handleSearch = (params) => {
-  tableRef.value?.reload?.({
-    page: 1,
-    where: params
+  // 表格列配置
+  const columns = ref([
+    {
+      type: 'selection',
+      columnKey: 'selection',
+      width: 50,
+      align: 'center',
+      fixed: 'left'
+    },
+    { label: '用户UID', prop: 'uid' },
+    { label: '订单编号', prop: 'orderNo' },
+    { label: '参与时间', prop: 'participateTime' },
+    { label: '名份', prop: 'name' },
+    { label: '地区', prop: 'region' },
+    { label: '订单总本数', prop: 'totalBooks' },
+    { label: '加价本数', prop: 'markupBooks' },
+    { label: '订单总金额', prop: 'totalAmount' },
+    { label: '实际加价金额', prop: 'actualMarkupAmount' },
+    {
+      columnKey: 'action',
+      label: '操作',
+      width: 200,
+      align: 'center',
+      slot: 'action',
+      fixed: 'right'
+    }
+  ]);
+
+  // 搜索
+  const handleSearch = (where) => {
+    let data = JSON.parse(JSON.stringify(where));
+    tableRef.value?.reload(data);
+  };
+
+  // 导出Excel
+  const handleExportExcel = () => {
+    tableRef.value?.exportData();
+  };
+
+  // 查看详情
+  const handleViewDetail = (row) => {
+    detailRef.value.openDialog(row);
+  };
+
+  // 分享处理
+  const handleShare = (row) => {
+    shareRef.value.openDialog(row);
+  };
+
+  // 更改状态
+  const handleChangeStatus = (row, status) => {
+    const statusText = status === 1 ? '启用' : '禁用';
+    tableRef.value?.messageBoxConfirm({
+      message: `确认${statusText}该IP?`,
+      fetch: () =>
+        request.post(`/marketing/ip/changeStatus`, {
+          id: row.id,
+          status: status
+        })
+    });
+  };
+
+  // 刷新表格
+  const reload = () => {
+    tableRef.value?.reload();
+  };
+
+  defineExpose({
+    reload
   });
-};
-
-// 查看详情
-const handleViewDetail = (row) => {
-  detailRef.value.openDialog(row);
-};
-
-// 分享处理
-const handleShare = (row) => {
-  shareRef.value.openDialog(row);
-};
 </script>
 
 <style lang="scss" scoped>
-.app-container {
-  padding: 15px;
-}
+  .app-container {
+    padding: 15px;
+  }
 </style>

+ 506 - 0
src/views/marketing/ipMrakup/fission/index.vue

@@ -0,0 +1,506 @@
+<template>
+  <div class="fission-analysis-container">
+    <el-card class="analysis-card mt-4" shadow="never">
+      <div class="date-selector-container">
+        <!-- Top Navigation Section -->
+        <div class="date-period-selector">
+          <el-button
+            :class="{ active: activePeriod === 'yesterday' }"
+            @click="changePeriod('yesterday')"
+            >昨日</el-button
+          >
+          <el-button
+            :class="{ active: activePeriod === '7d' }"
+            @click="changePeriod('7d')"
+            >7日</el-button
+          >
+          <el-button
+            :class="{ active: activePeriod === '15d' }"
+            @click="changePeriod('15d')"
+            >15日</el-button
+          >
+          <el-button
+            :class="{ active: activePeriod === '30d' }"
+            @click="changePeriod('30d')"
+            >30日</el-button
+          >
+
+          <el-date-picker
+            v-model="dateRange"
+            type="date"
+            placeholder="选择日期"
+            format="YYYY-MM-DD"
+            value-format="YYYY-MM-DD"
+            :clearable="false"
+            :disabled-date="(date) => false"
+            @change="fetchData"
+          />
+          <div class="date-action-buttons">
+            <el-button type="primary" @click="fetchData">提交</el-button>
+            <el-button @click="resetFilters">重置</el-button>
+          </div>
+        </div>
+      </div>
+
+      <!-- Metrics Cards -->
+      <div class="metrics-cards">
+        <el-row :gutter="20">
+          <el-col :span="6">
+            <el-card shadow="never" class="metric-card">
+              <div class="metric-title">今日助力人数</div>
+              <div class="metric-date">{{ currentDate }}</div>
+              <div class="metric-value-container">
+                <div class="metric-label">合计</div>
+                <div class="metric-value">{{ metrics.totalUsers }}人</div>
+              </div>
+              <div class="metric-comparison"
+                >环比: {{ metrics.totalUsersChange }}↓</div
+              >
+            </el-card>
+          </el-col>
+          <el-col :span="6">
+            <el-card shadow="never" class="metric-card">
+              <div class="metric-title">今日人均助力次数</div>
+              <div class="metric-date">{{ currentDate }}</div>
+              <div class="metric-value-container">
+                <div class="metric-label">合计</div>
+                <div class="metric-value">{{ metrics.avgHelps }}人</div>
+              </div>
+              <div class="metric-comparison"
+                >环比: {{ metrics.avgHelpsChange }}↓</div
+              >
+            </el-card>
+          </el-col>
+          <el-col :span="6">
+            <el-card shadow="never" class="metric-card">
+              <div class="metric-title">活动书单扫描/参与占比</div>
+              <div class="metric-date">{{ currentDate }}</div>
+              <div class="metric-value-container">
+                <div class="metric-label">合计</div>
+                <div class="metric-value">{{ metrics.scanRate }}</div>
+              </div>
+              <div class="metric-comparison"
+                >环比: {{ metrics.scanRateChange }}↓</div
+              >
+            </el-card>
+          </el-col>
+          <el-col :span="6">
+            <el-card shadow="never" class="metric-card">
+              <div class="metric-title">今日助力新用户占比</div>
+              <div class="metric-date">{{ currentDate }}</div>
+              <div class="metric-value-container">
+                <div class="metric-label">合计</div>
+                <div class="metric-value">{{ metrics.newUserRate }}</div>
+              </div>
+              <div class="metric-comparison"
+                >环比: {{ metrics.newUserRateChange }}↓</div
+              >
+            </el-card>
+          </el-col>
+        </el-row>
+      </div>
+
+      <!-- Numbers Cards -->
+      <div class="number-metrics">
+        <el-row :gutter="20">
+          <el-col :span="4">
+            <el-card shadow="hover" class="number-card">
+              <div class="number-value">{{ stats.participants }}</div>
+              <div class="number-label">参与用户数</div>
+            </el-card>
+          </el-col>
+          <el-col :span="4">
+            <el-card shadow="hover" class="number-card">
+              <div class="number-value">{{ stats.helps }}</div>
+              <div class="number-label">助力次数</div>
+            </el-card>
+          </el-col>
+          <el-col :span="4">
+            <el-card shadow="hover" class="number-card">
+              <div class="number-value">{{ stats.ordersCount }}</div>
+              <div class="number-label">订单总数</div>
+            </el-card>
+          </el-col>
+          <el-col :span="4">
+            <el-card shadow="hover" class="number-card">
+              <div class="number-value">{{ stats.totalOrderAmount }}</div>
+              <div class="number-label">订单预估总金额</div>
+            </el-card>
+          </el-col>
+          <el-col :span="4">
+            <el-card shadow="hover" class="number-card">
+              <div class="number-value">{{ stats.totalAddedPoints }}</div>
+              <div class="number-label">加价积分总金额</div>
+            </el-card>
+          </el-col>
+          <el-col :span="4">
+            <el-card shadow="hover" class="number-card">
+              <div class="number-value">{{ stats.totalSaleAmount }}</div>
+              <div class="number-label">实际成价总金额</div>
+            </el-card>
+          </el-col>
+        </el-row>
+      </div>
+
+      <!-- Chart Section -->
+      <div class="trend-chart-section">
+        <div class="chart-title">30日分享人数趋势</div>
+        <div class="chart-container" ref="chartRef"></div>
+      </div>
+    </el-card>
+  </div>
+</template>
+
+<script setup>
+  import { ref, reactive, onMounted, computed, watch } from 'vue';
+  import { ArrowDown } from '@element-plus/icons-vue';
+  import * as echarts from 'echarts';
+  import dayjs from 'dayjs';
+
+  // State
+  const activePeriod = ref('yesterday');
+  const timeSliderValue = ref(12);
+  const datePopoverVisible = ref(false);
+  const dateDisplayOption = ref('1');
+  const chartRef = ref(null);
+  let chart = null;
+  // Initialize dateRange with current date
+  const dateRange = ref(dayjs().toDate());
+
+  // Mock data
+  const metrics = reactive({
+    totalUsers: '266',
+    totalUsersChange: '56%',
+    avgHelps: '266',
+    avgHelpsChange: '56%',
+    scanRate: '55%',
+    scanRateChange: '56%',
+    newUserRate: '55%',
+    newUserRateChange: '56%'
+  });
+
+  const stats = reactive({
+    participants: '44351316',
+    helps: '44351316',
+    ordersCount: '44351316',
+    totalOrderAmount: '44351316',
+    totalAddedPoints: '48415555',
+    totalSaleAmount: '48415555'
+  });
+
+  const currentDate = computed(() => {
+    return dayjs().format('YYYY-MM-DD');
+  });
+
+  // Methods
+  const formatTooltip = (val) => {
+    return `${val}:00`;
+  };
+
+  const changePeriod = (period) => {
+    activePeriod.value = period;
+
+    // Update dateRange based on the selected period
+    switch (period) {
+      case 'yesterday':
+        dateRange.value = dayjs().subtract(1, 'day').toDate();
+        break;
+      case '7d':
+        dateRange.value = dayjs().subtract(7, 'day').toDate();
+        break;
+      case '15d':
+        dateRange.value = dayjs().subtract(15, 'day').toDate();
+        break;
+      case '30d':
+        dateRange.value = dayjs().subtract(30, 'day').toDate();
+        break;
+      default:
+        dateRange.value = dayjs().toDate();
+    }
+
+    fetchData();
+  };
+
+  const fetchData = () => {
+    // Here you would fetch real data from your API
+    console.log('Fetching data for period:', activePeriod.value);
+    console.log('Selected date:', dayjs(dateRange.value).format('YYYY-MM-DD'));
+    console.log('Time:', timeSliderValue.value);
+
+    // Mock API call and data update
+    // In a real app, you would make an API request and update the state
+  };
+
+  const resetFilters = () => {
+    activePeriod.value = 'yesterday';
+    dateRange.value = dayjs().subtract(1, 'day').toDate(); // Reset dateRange to yesterday
+    timeSliderValue.value = 12;
+    fetchData();
+  };
+
+  const initChart = () => {
+    if (chartRef.value) {
+      chart = echarts.init(chartRef.value);
+
+      // Sample data for the chart
+      const option = {
+        tooltip: {
+          trigger: 'axis',
+          axisPointer: {
+            type: 'shadow'
+          }
+        },
+        grid: {
+          left: '3%',
+          right: '4%',
+          bottom: '3%',
+          containLabel: true
+        },
+        xAxis: {
+          type: 'category',
+          data: ['1月', '2月', '3月', '4月', '5月'],
+          boundaryGap: false
+        },
+        yAxis: {
+          type: 'value',
+          min: 0,
+          max: 250,
+          interval: 50
+        },
+        series: [
+          {
+            name: '分享人数',
+            type: 'line',
+            data: [100, 140, 220, 120, 130],
+            smooth: true,
+            lineStyle: {
+              color: '#5470c6',
+              width: 2
+            },
+            symbol: 'circle',
+            symbolSize: 8,
+            itemStyle: {
+              color: '#5470c6'
+            }
+          }
+        ]
+      };
+
+      chart.setOption(option);
+
+      // Handle resize
+      window.addEventListener('resize', () => {
+        chart.resize();
+      });
+
+      // Use ResizeObserver to detect and respond to size changes of the chart container
+      // This will ensure the chart resizes properly when tabs change
+      const resizeObserver = new ResizeObserver(() => {
+        chart.resize();
+      });
+
+      resizeObserver.observe(chartRef.value);
+    }
+  };
+
+  // Method to manually resize chart - can be called from parent components
+  const resizeChart = () => {
+    if (chart) {
+      chart.resize();
+    }
+  };
+
+  // Watch for changes in dateRange to update activePeriod
+  watch(dateRange, (newValue) => {
+    const selectedDate = dayjs(newValue);
+    const today = dayjs();
+    const yesterday = dayjs().subtract(1, 'day');
+    const days7ago = dayjs().subtract(7, 'day');
+    const days15ago = dayjs().subtract(15, 'day');
+    const days30ago = dayjs().subtract(30, 'day');
+
+    // Check if selected date matches any of the predefined periods
+    if (selectedDate.isSame(yesterday, 'day')) {
+      activePeriod.value = 'yesterday';
+    } else if (selectedDate.isSame(days7ago, 'day')) {
+      activePeriod.value = '7d';
+    } else if (selectedDate.isSame(days15ago, 'day')) {
+      activePeriod.value = '15d';
+    } else if (selectedDate.isSame(days30ago, 'day')) {
+      activePeriod.value = '30d';
+    } else {
+      // If it doesn't match any predefined period, clear the active selection
+      activePeriod.value = '';
+    }
+  });
+
+  onMounted(() => {
+    initChart();
+
+    // Initialize dateRange based on the default activePeriod (yesterday)
+    if (activePeriod.value === 'yesterday') {
+      dateRange.value = dayjs().subtract(1, 'day').toDate();
+    }
+
+    // Force a resize after component is mounted
+    // This ensures the chart properly fills its container
+    setTimeout(() => {
+      resizeChart();
+    }, 300); // Small delay to ensure rendering is complete
+  });
+
+  // Expose the resizeChart method to parent components
+  defineExpose({
+    resizeChart
+  });
+</script>
+
+<style lang="scss" scoped>
+  .date-selector-container {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    margin-bottom: 20px;
+    border-bottom: 1px solid #ebeef5;
+    padding-bottom: 16px;
+  }
+
+  .date-period-selector {
+    display: flex;
+    gap: 10px;
+
+    .el-button {
+      border-radius: 4px;
+    }
+
+    .active {
+      color: #409eff;
+      border-color: #409eff;
+    }
+  }
+
+  .custom-date-range {
+    display: flex;
+    align-items: center;
+    gap: 16px;
+  }
+
+  .time-range-selector {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+
+    .time-label {
+      font-size: 14px;
+      color: #606266;
+    }
+  }
+
+  .time-slider {
+    width: 200px;
+  }
+
+  .date-action-buttons {
+    display: flex;
+    gap: 8px;
+  }
+
+  .date-display {
+    display: flex;
+    align-items: center;
+    gap: 4px;
+    cursor: pointer;
+    padding: 6px 12px;
+    border: 1px solid #dcdfe6;
+    border-radius: 4px;
+    margin-bottom: 20px;
+  }
+
+  .metrics-cards {
+    margin-bottom: 24px;
+  }
+
+  .metric-card {
+    border: 1px solid #ebeef5;
+    height: 150px;
+
+    .metric-title {
+      font-size: 16px;
+      color: #303133;
+      margin-bottom: 4px;
+    }
+
+    .metric-date {
+      font-size: 12px;
+      color: #909399;
+      margin-bottom: 8px;
+    }
+
+    .metric-value-container {
+      display: flex;
+      align-items: center;
+      gap: 8px;
+      margin-bottom: 4px;
+
+      .metric-label {
+        font-size: 14px;
+        color: #606266;
+      }
+
+      .metric-value {
+        font-size: 26px;
+        font-weight: bold;
+        color: #303133;
+      }
+    }
+
+    .metric-comparison {
+      font-size: 14px;
+      color: #606266;
+    }
+  }
+
+  .number-metrics {
+    margin-bottom: 24px;
+  }
+
+  .number-card {
+    text-align: center;
+    padding: 16px;
+    height: 120px;
+
+    .number-value {
+      font-size: 22px;
+      font-weight: bold;
+      color: #f6a623;
+      margin-bottom: 8px;
+    }
+
+    .number-label {
+      font-size: 14px;
+      color: #606266;
+    }
+  }
+
+  .trend-chart-section {
+    margin-top: 24px;
+    width: 100%;
+
+    .chart-title {
+      font-size: 16px;
+      color: #303133;
+      margin-bottom: 16px;
+    }
+
+    .chart-container {
+      height: 350px;
+      width: 100%;
+      position: relative;
+      overflow: hidden;
+    }
+  }
+
+  .date-popover-content {
+    padding: 12px;
+  }
+</style>

+ 1 - 1
src/views/marketing/ipMrakup/index.vue

@@ -43,12 +43,12 @@
 
 <style lang="scss" scoped>
   .ip-markup-management {
-    padding: 16px;
     height: calc(100% - 10px);
     background-color: #fff;
     box-sizing: border-box;
 
     .ip-markup-tabs {
+      margin: 16px;
       :deep(.el-tabs__header) {
         margin-bottom: 0;
       }

+ 144 - 0
src/views/marketing/ipMrakup/rules/index.vue

@@ -0,0 +1,144 @@
+<template>
+  <ele-page flex-table :bodyStyle="{ padding: 0 }">
+    <div class="common-section">
+      <div class="common-title mb-4">加价营销规则</div>
+      <el-form :model="form" class="common-form" label-width="auto">
+        <el-form-item label="加价比例:">
+          <ele-input
+            v-model="form.markupRatio"
+            placeholder="请输入"
+            suffixStr="(%)"
+          />
+        </el-form-item>
+        <el-form-item label="加价限制时间:">
+          <ele-input
+            v-model="form.markupTimeLimit"
+            placeholder="请输入"
+            suffixStr="(小时)"
+          />
+          <span class="ml-2 text-gray-500">以点击分享链接计算</span>
+        </el-form-item>
+        <el-form-item label="每人每种书可加价次数:">
+          <ele-input
+            v-model="form.markupTimesPerBook"
+            placeholder="请输入"
+            suffixStr="(次)"
+          />
+        </el-form-item>
+        <el-form-item label="每个ID每天助力限制:">
+          <ele-input
+            v-model="form.dailyAssistLimit"
+            placeholder="请输入"
+            suffixStr="(次)"
+          />
+        </el-form-item>
+        <el-form-item label="预留手机号:">
+          <ele-input v-model="form.reservedPhone" placeholder="请输入" />
+        </el-form-item>
+        <el-form-item label="单本最高加价:">
+          <ele-input
+            v-model="form.maxPriceMarkup"
+            placeholder="请输入"
+            suffixStr="(元)"
+          />
+        </el-form-item>
+        <el-form-item label="单本最低加价:">
+          <ele-input
+            v-model="form.minPriceMarkup"
+            placeholder="请输入"
+            suffixStr="(元)"
+          />
+        </el-form-item>
+        <el-form-item label="加价结算场景:">
+          <el-select
+            v-model="form.settlementScenarios"
+            multiple
+            collapse-tags
+            style="width: 500px"
+            placeholder="请选择加价结算场景"
+          >
+            <el-option
+              label="验货不合格(其他原因)"
+              value="verification_failed_other"
+            />
+            <el-option label="验货合格" value="verification_passed" />
+          </el-select>
+          <el-button type="success" plain link class="ml-10">[修改]</el-button>
+        </el-form-item>
+        <el-form-item>
+          <el-button
+            type="success"
+            style="width: 100px"
+            plain
+            @click="onSubmit"
+            v-permission="'marketing:ipMarkup:rules:update'"
+            >保存</el-button
+          >
+        </el-form-item>
+      </el-form>
+    </div>
+  </ele-page>
+</template>
+
+<script setup>
+  import { reactive, ref, onMounted } from 'vue';
+  import EleInput from '@/components/CommonPage/EleInput.vue';
+  import { ElMessage } from 'element-plus';
+  import request from '@/utils/request';
+
+  const form = reactive({
+    markupRatio: '10',
+    markupTimeLimit: '24',
+    markupTimesPerBook: '1',
+    dailyAssistLimit: '1',
+    reservedPhone: '18888888888',
+    maxPriceMarkup: '5',
+    minPriceMarkup: '0.5',
+    settlementScenarios: ['verification_failed_other', 'verification_passed']
+  });
+
+  // 获取当前设置
+  function getRules() {
+    request.get('/marketing/ipMarkup/rules/get').then((res) => {
+      if (res.data.code === 200) {
+        const data = res.data.data;
+        form.markupRatio = data.markupRatio;
+        form.markupTimeLimit = data.markupTimeLimit;
+        form.markupTimesPerBook = data.markupTimesPerBook;
+        form.dailyAssistLimit = data.dailyAssistLimit;
+        form.reservedPhone = data.reservedPhone;
+        form.maxPriceMarkup = data.maxPriceMarkup;
+        form.minPriceMarkup = data.minPriceMarkup;
+        form.settlementScenarios = data.settlementScenarios;
+      } else {
+        ElMessage.error(res.data.msg);
+      }
+    });
+  }
+
+  // 保存设置
+  const onSubmit = () => {
+    request
+      .post('/marketing/ipMarkup/rules/update', {
+        markupRatio: form.markupRatio,
+        markupTimeLimit: form.markupTimeLimit,
+        markupTimesPerBook: form.markupTimesPerBook,
+        dailyAssistLimit: form.dailyAssistLimit,
+        reservedPhone: form.reservedPhone,
+        maxPriceMarkup: form.maxPriceMarkup,
+        minPriceMarkup: form.minPriceMarkup,
+        settlementScenarios: form.settlementScenarios
+      })
+      .then((res) => {
+        if (res.data.code === 200) {
+          ElMessage.success('保存成功');
+        } else {
+          ElMessage.error(res.data.msg);
+        }
+      });
+  };
+
+  onMounted(() => {
+    getRules();
+  });
+</script>