Ver código fonte

库存管理

Alex 9 meses atrás
pai
commit
ee2e9d02f7

+ 10 - 10
src/views/marketing/messagePush/components/message-push-dialog.vue

@@ -39,9 +39,9 @@
       <div class="content-section">
         <div class="section-title">设置消息内容</div>
 
-        <el-form-item label="消息模板:" prop="templateId" required>
+        <el-form-item label="消息模板:" prop="templeteId" required>
           <el-select
-            v-model="sendForm.templateId"
+            v-model="sendForm.templeteId"
             placeholder="请选择消息模板"
             clearable
             @change="handleTemplateChange"
@@ -134,14 +134,14 @@ const sendForm = reactive({
   id: undefined,
   userType: 1,
   userSearch: [],
-  templateId: '',
+  templeteId: '',
   remark: '',
   sendType: 1,
   sendTime: ''
 });
 
 const rules = {
-  templateId: [{ required: true, message: '请选择消息模板', trigger: 'change' }],
+  templeteId: [{ required: true, message: '请选择消息模板', trigger: 'change' }],
   userSearch: [{ required: true, message: '请输入搜索条件', trigger: 'blur' }],
   sendTime: [{ required: true, message: '请选择发送时间', trigger: 'change' }]
 };
@@ -185,15 +185,15 @@ const getTemplateDetail = async (id) => {
 };
 
 // 模板选择变更
-const handleTemplateChange = async (templateId) => {
-  if (!templateId) {
-    sendForm.templateId = '';
+const handleTemplateChange = async (templeteId) => {
+  if (!templeteId) {
+    sendForm.templeteId = '';
     templateDetail.content = '';
     templateDetail.sign = '';
     return;
   }
-  sendForm.templateId = templateId;
-  await getTemplateDetail(templateId);
+  sendForm.templeteId = templeteId;
+  await getTemplateDetail(templeteId);
 };
 
 function handleReset() {
@@ -201,7 +201,7 @@ function handleReset() {
     id: undefined,
     userType: 1,
     userSearch: [],
-    templateId: '',
+    templeteId: '',
     remark: '',
     sendType: 1,
     sendTime: ''

+ 2 - 2
src/views/marketing/messagePush/components/message-push.vue

@@ -99,8 +99,8 @@
       minWidth: 160
     },
     {
-      label: '备注',
-      prop: 'remark',
+      label: '发送内容',
+      prop: 'content',
       align: 'center',
       minWidth: 140
     },

+ 20 - 3
src/views/marketing/messagePush/components/template-edit.vue

@@ -9,7 +9,7 @@
       ref="formRef"
       :model="form"
       :rules="rules"
-      label-width="100px"
+      label-width="110px"
       @keyup.enter="handleSubmit"
     >
       <el-form-item label="模板名称" prop="name">
@@ -48,6 +48,10 @@
           <el-checkbox label="3">短信推送</el-checkbox>
         </el-checkbox-group>
       </el-form-item>
+
+      <el-form-item label="三方模板KEY" prop="thirdKey" :required="form.pushWay.includes('3')">
+        <el-input v-model="form.thirdKey" placeholder="请输入" clearable />
+      </el-form-item>
     </el-form>
     <template #footer>
       <el-button @click="visible = false">取 消</el-button>
@@ -86,7 +90,19 @@
     sign: [{ required: true, message: '请输入签名名称', trigger: 'blur' }],
     content: [
       { required: true, message: '请输入短信内容', trigger: 'blur' }
-    ]
+    ],
+    thirdKey: [{
+      required: false,
+      message: '请输入三方模板KEY',
+      trigger: 'blur',
+      validator: (rule, value, callback) => {
+        if (form.pushWay.includes('3') && !value) {
+          callback(new Error('三方模板KEY为必填'));
+        } else {
+          callback();
+        }
+      }
+    }]
   };
 
   // 打开弹窗
@@ -121,7 +137,8 @@
         content: form.content,
         remark: form.remark,
         pushWay: form.pushWay.join(','),
-        status: 1
+        status: 1,
+        thirdKey: form.thirdKey
       };
 
       if (editId.value) {

+ 6 - 0
src/views/marketing/messagePush/components/template-manage.vue

@@ -168,6 +168,12 @@
       align: 'center',
       minWidth: 160
     },
+    {
+      label: '三方模板KEY',
+      prop: 'thirdKey',
+      align: 'center',
+      minWidth: 120
+    },
     {
       columnKey: 'action',
       label: '操作',

+ 57 - 0
src/views/recycle/inventory/components/book-base-info.vue

@@ -0,0 +1,57 @@
+<template>
+  <div class="recycle-order-number flex flex-col items-start ml-6">
+    <div class="common-text cursor-pointer">
+      <el-text type="primary" @click="handleClick(row)">{{
+        row.bookName
+      }}</el-text>
+    </div>
+    <div class="flex justify-between items-start">
+      <div class="flex flex-col items-start" style="min-width:240px">
+        <div class="common-text">
+          <el-text>ISBN:</el-text>
+          <el-text>{{ row.isbn }}</el-text>
+        </div>
+        <div class="common-text">
+          <el-text>作 者:</el-text>
+          <el-text>{{ row.author || '-' }} </el-text>
+        </div>
+        <div class="common-text">
+          <el-text>出版社:</el-text>
+          <el-text>{{ row.publish || '-' }}</el-text>
+        </div>
+      </div>
+      <div class="flex flex-col items-start ml-20">
+        <div class="common-text">
+          <el-text>定价:</el-text>
+          <el-text>¥ {{ row.price }}</el-text>
+        </div>
+        <div class="common-text">
+          <el-text>出版时间:</el-text>
+          <el-text
+            >{{ row.pubDate ? dayjs(row.pubDate).format('YYYY-MM-DD') : '-' }}
+          </el-text>
+        </div>
+        <div class="common-text">
+          <el-text>装帧:</el-text>
+          <el-text>{{ row.bookPack || '暂无' }}</el-text>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+  import { dayjs } from 'element-plus';
+  const emit = defineEmits(['click']);
+
+  const props = defineProps({
+    row: {
+      type: Object,
+      default: () => {}
+    }
+  });
+
+  function handleClick(row) {
+    emit('click', row);
+  }
+</script>

+ 122 - 0
src/views/recycle/inventory/components/book-check-new.vue

@@ -0,0 +1,122 @@
+<template>
+  <el-dialog
+    v-model="visible"
+    title="检测上新"
+    width="600px"
+    :close-on-click-modal="false"
+    @closed="handleClosed"
+  >
+    <div class="check-new-container">
+      <div class="check-period">
+        <el-radio-group v-model="selectedPeriod" @change="handlePeriodChange">
+          <el-radio :value="7">近7日</el-radio>
+          <el-radio :value="15">近15日</el-radio>
+          <el-radio :value="30">近30日</el-radio>
+        </el-radio-group>
+      </div>
+
+      <div class="time-range">
+        <el-date-picker
+          v-model="timeRange"
+          type="datetimerange"
+          range-separator="到"
+          start-placeholder="建单时间(开始时间)"
+          end-placeholder="建单时间(结束时间)"
+          format="YYYY-MM-DD HH:mm:ss"
+          value-format="YYYY-MM-DD HH:mm:ss"
+        />
+      </div>
+
+      <div class="dialog-footer">
+        <el-button @click="visible = false">关闭</el-button>
+        <el-button type="primary" @click="handleConfirm">确定</el-button>
+      </div>
+    </div>
+  </el-dialog>
+</template>
+
+<script setup>
+  import { ref, watch } from 'vue';
+  import { ElMessage } from 'element-plus';
+  import request from '@/utils/request';
+  import dayjs from 'dayjs';
+
+  const visible = ref(false);
+  const selectedPeriod = ref(7);
+  const timeRange = ref(null);
+  const emit = defineEmits(['refresh']);
+  const handleOpen = () => {
+    visible.value = true;
+    handlePeriodChange(7);
+  };
+
+  const handlePeriodChange = (period) => {
+    const end = dayjs();
+    const start = dayjs().subtract(period, 'day');
+    timeRange.value = [
+      start.format('YYYY-MM-DD HH:mm:ss'),
+      end.format('YYYY-MM-DD HH:mm:ss')
+    ];
+  };
+
+  const handleConfirm = async () => {
+    if (!timeRange.value || !timeRange.value[0] || !timeRange.value[1]) {
+      ElMessage.warning('请选择时间范围');
+      return;
+    }
+
+    const params = {
+      startTime: timeRange.value[0],
+      endTime: timeRange.value[1]
+    };
+
+    request
+      .get(
+        '/book/stock/syncbookInfo?startTime=' +
+          params.startTime +
+          '&endTime=' +
+          params.endTime
+      )
+      .then((res) => {
+        if (res.data.code === 200) {
+          ElMessage.success(res.data.msg + ',' + res.data.data);
+          visible.value = false;
+          emit('refresh');
+        } else {
+          ElMessage.error(res.data.msg);
+        }
+      });
+  };
+
+  const handleClosed = () => {
+    timeRange.value = null;
+    selectedPeriod.value = 7;
+  };
+
+  defineExpose({
+    handleOpen
+  });
+</script>
+
+<style scoped>
+  .check-new-container {
+    padding: 20px;
+  }
+
+  .check-period {
+    margin-bottom: 20px;
+  }
+
+  .time-range {
+    margin-bottom: 20px;
+  }
+
+  .dialog-footer {
+    text-align: right;
+    margin-top: 20px;
+  }
+
+  :deep(.el-date-picker) {
+    width: 100%;
+  }
+</style>

+ 28 - 0
src/views/recycle/inventory/components/book-stock.vue

@@ -0,0 +1,28 @@
+<template>
+  <div class="recycle-order-number flex flex-col items-start">
+    <div class="common-text">
+      <el-text>中等:{{ row.mediumNum }}</el-text>
+      <el-text></el-text>
+    </div>
+    <div class="common-text">
+      <el-text>良好:{{ row.goodNum }}</el-text>
+    </div>
+    <div class="common-text">
+      <el-text>次品:{{ row.badNum }}</el-text>
+      <el-text></el-text>
+    </div>
+    <div class="common-text">
+      <el-text>合计:{{ row.totalNum }}</el-text>
+      <el-text></el-text>
+    </div>
+  </div>
+</template>
+
+<script setup>
+  const props = defineProps({
+    row: {
+      type: Object,
+      default: () => {}
+    }
+  });
+</script>

+ 86 - 0
src/views/recycle/inventory/components/page-search.vue

@@ -0,0 +1,86 @@
+<!-- 搜索表单 -->
+<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 } from 'vue';
+  import ProSearch from '@/components/CommonPage/ProSearch2.vue';
+
+  let { proxy } = getCurrentInstance();
+  const emit = defineEmits(['search']);
+
+  const formItems = reactive([
+    { type: 'input', label: '书名', prop: 'bookName' },
+    { type: 'input', label: '作者', prop: 'author' },
+    {
+      type: 'inputNumberRange',
+      label: '售价',
+      prop: 'price',
+      keys: ['minPrice', 'maxPrice'],
+      props: {
+        onChange: (val) => {
+          searchRef.value?.setData({ minPrice: val.min, maxPrice: val.max });
+        }
+      }
+    },
+    {
+      type: 'inputNumberRange',
+      label: '库存',
+      prop: 'stock',
+      keys: ['minStock', 'maxStock'],
+      props: {
+        minAttrs: { min: 0 },
+        maxAttrs: { max: 1 },
+        onChange: (val) => {
+          searchRef.value?.setData({
+            minStock: val.min,
+            maxStock: val.max
+          });
+        }
+      }
+    },
+    {
+      type: 'input',
+      label: 'ISBN',
+      prop: 'isbn',
+      props: {
+        placeholder: "ISBN'中英文逗号、空格或换行'分割"
+      },
+      colProps: { span: 7 }
+    }
+  ]);
+
+  const initKeys = reactive({
+    bookName: '',
+    isbn: '',
+    author: '',
+    publish: '',
+    pubDateStart: '',
+    pubDateEnd: '',
+    minPrice: void 0,
+    maxPrice: void 0,
+    minDiscount: void 0,
+    maxDiscount: void 0,
+    searchType: 0,
+    globalInDiscount: void 0,
+    globalNotInDiscount: void 0
+  });
+
+  const searchRef = ref(null);
+  /** 搜索 */
+  const search = (data) => {
+    let params = JSON.parse(JSON.stringify(data));
+    delete params.price;
+    delete params.discount;
+    delete params.pubDate;
+    emit('search', params);
+  };
+</script>

+ 199 - 0
src/views/recycle/inventory/index.vue

@@ -0,0 +1,199 @@
+<template>
+  <ele-page flex-table>
+    <page-search @search="reload"></page-search>
+
+    <common-table ref="pageRef" :pageConfig="pageConfig" :columns="columns">
+      <template #toolbar>
+        <el-button
+          type="primary"
+          plain
+          :icon="PlusOutlined"
+          v-permission="'data:inventory:add'"
+          @click="handleUpdate()"
+        >
+          新增图书商品
+        </el-button>
+        <el-button
+          type="primary"
+          plain
+          v-permission="'data:inventory:import'"
+          @click="handleImportExcel"
+          :icon="UploadOutlined"
+        >
+          批量导入
+        </el-button>
+        <el-button
+          type="success"
+          plain
+          v-permission="'data:inventory:checkNew'"
+          @click="handleCheckNew"
+          :icon="CloudUploadOutlined"
+        >
+          检测上新
+        </el-button>
+      </template>
+      <template #cover="{ row }">
+        <el-image
+          style="width: 80px; height: 100px"
+          fit="cover"
+          :src="row.cover"
+        />
+      </template>
+
+      <template #baseInfo="{ row }">
+        <book-base-info :row="row.bookInfo" @click="handleUpdate"></book-base-info>
+      </template>
+
+      <template #stock="{ row }">
+        <book-stock :row="row"></book-stock>
+      </template>
+
+      <template #action="{ row }">
+        <div>
+          <el-button
+            type="success"
+            link
+            v-permission="'data:inventory:update'"
+            @click="handleUpdate(row)"
+          >
+            [编辑]
+          </el-button>
+          <el-button
+            type="danger"
+            link
+            v-permission="'data:inventory:recycleDetail'"
+            @click="handleRecycleDetail(row)"
+          >
+            [回收明细]
+          </el-button>
+          <el-button
+            type="warning"
+            link
+            v-permission="'data:inventory:saleDetail'"
+            @click="handleSaleDetail(row)"
+          >
+            [销售明细]
+          </el-button>
+        </div>
+      </template>
+    </common-table>
+    <books-edit ref="editRef" @success="reload()"></books-edit>
+    <books-import
+      ref="importRef"
+      v-model="showImport"
+      @done="reload()"
+    ></books-import>
+    <book-check-new ref="checkNewRef"></book-check-new>
+  </ele-page>
+</template>
+
+<script setup>
+  import { ref, reactive } from 'vue';
+  import {
+    PlusOutlined,
+    UploadOutlined,
+    CloudUploadOutlined
+  } from '@/components/icons';
+  import pageSearch from './components/page-search.vue';
+  import CommonTable from '@/components/CommonPage/CommonTable.vue';
+  import booksEdit from '@/views/data/books/components/books-edit.vue';
+  import booksImport from '@/views/data/books/components/books-import.vue';
+  import bookBaseInfo from './components/book-base-info.vue';
+  import bookCheckNew from './components/book-check-new.vue';
+  import bookStock from './components/book-stock.vue';
+  import { useDictData } from '@/utils/use-dict-data';
+
+  defineOptions({ name: 'recycleOrderCancelled' });
+
+  const [perCheckDicts] = useDictData(['is_common_yes']);
+
+  /** 表格列配置 */
+  const columns = ref([
+    {
+      type: 'selection',
+      columnKey: 'selection',
+      width: 50,
+      align: 'center',
+      fixed: 'left'
+    },
+    {
+      label: '图片',
+      prop: 'cover',
+      slot: 'cover',
+      align: 'center'
+    },
+    {
+      label: '基础信息',
+      prop: 'baseInfo',
+      slot: 'baseInfo',
+      align: 'center',
+      minWidth: 320
+    },
+    {
+      label: '商品类型',
+      prop: 'goodsType',
+      formatter: (row) => '图书商品',
+      align: 'center'
+    },
+    {
+      label: '售价',
+      prop: 'salePrice',
+      align: 'center',
+      minWidth: 100,
+      formatter: (row) => '¥' + row.salePrice || 0
+    },
+    {
+      label: '库存',
+      prop: 'stock',
+      align: 'center',
+      minWidth: 100,
+      slot: 'stock'
+    },
+    {
+      label: '销量',
+      prop: 'salesNum',
+      align: 'center',
+      minWidth: 100,
+      formatter: (row) => row.salesNum || 0
+    },
+    {
+      columnKey: 'action',
+      label: '操作',
+      width: 200,
+      align: 'center',
+      slot: 'action'
+    }
+  ]);
+
+  /** 页面组件实例 */
+  const pageRef = ref(null);
+
+  const pageConfig = reactive({
+    pageUrl: '/book/stock/pagelist',
+    fileName: '库存数据',
+    cacheKey: 'books-stock-data'
+  });
+
+  //导入
+  const showImport = ref(false);
+  function handleImportExcel() {
+    showImport.value = true;
+  }
+
+  //刷新表格
+  function reload(where) {
+    pageRef.value?.reload(where);
+  }
+
+  //新增编辑图书
+  const editRef = ref(null);
+  function handleUpdate(row) {
+    editRef.value?.handleOpen(row);
+  }
+
+  //检测上新
+  const checkNewRef = ref(null);
+  function handleCheckNew() {
+    checkNewRef.value?.handleOpen();
+  }
+</script>