Bläddra i källkod

feat: 对接轮播图模块,订单部分联调

叶静 1 vecka sedan
förälder
incheckning
11c1a8ccc4

+ 5 - 0
src/app/admin/api/index.js

@@ -342,4 +342,9 @@ export default {
         }),
     },
   },
+
+  // 轮播图管理
+  banner: {
+    ...CRUD('/mall/banner'),
+  },
 };

+ 9 - 0
src/app/admin/routes/index.js

@@ -103,5 +103,14 @@ export default {
         },
       ],
     },
+    {
+      path: 'banner',
+      name: 'admin.banner',
+      component: () => import('@/app/admin/views/banner/index.vue'),
+      meta: {
+        title: '轮播图管理',
+        icon: 'Picture',
+      },
+    },
   ],
 };

+ 146 - 0
src/app/admin/views/banner/edit.vue

@@ -0,0 +1,146 @@
+<template>
+  <el-container>
+    <el-main v-loading="loading" element-loading-text="加载中...">
+      <el-form :model="form.model" :rules="form.rules" ref="formRef" label-width="120px">
+        <el-form-item label="轮播图标题" prop="title">
+          <el-input
+            v-model="form.model.title"
+            placeholder="请输入轮播图标题"
+            maxlength="50"
+            show-word-limit
+          />
+        </el-form-item>
+
+        <el-form-item label="轮播图片" prop="image">
+          <sa-upload-image
+            v-model="imageArray"
+            :max-count="1"
+            :accept="['jpg', 'jpeg', 'png']"
+            :max-size="5"
+            :direct-upload="true"
+            :width="375"
+            :height="200"
+            placeholder="上传图片"
+          />
+        </el-form-item>
+
+        <el-form-item label="链接类型" prop="linkType">
+          <el-radio-group v-model="form.model.linkType">
+            <el-radio :label="0">内链</el-radio>
+            <el-radio :label="1">外链</el-radio>
+          </el-radio-group>
+        </el-form-item>
+
+        <el-form-item label="链接地址" prop="link">
+          <el-input
+            v-model="form.model.link"
+            placeholder="请输入链接地址"
+            maxlength="200"
+            show-word-limit
+          />
+        </el-form-item>
+
+        <el-form-item label="排序" prop="seq">
+          <el-input v-model="form.model.seq" placeholder="请填写排序" />
+        </el-form-item>
+      </el-form>
+    </el-main>
+    <el-footer class="sa-footer--submit">
+      <el-button v-if="modal.params.type == 'add'" type="primary" @click="confirm">保存</el-button>
+      <el-button v-if="modal.params.type == 'edit'" type="primary" @click="confirm">更新</el-button>
+    </el-footer>
+  </el-container>
+</template>
+
+<script setup>
+  import { onMounted, reactive, ref, unref, computed } from 'vue';
+  import api from '../../api';
+  import { cloneDeep } from 'lodash';
+
+  const emit = defineEmits(['modalCallBack']);
+  const props = defineProps({
+    modal: {
+      type: Object,
+    },
+  });
+
+  // 添加 编辑 form
+  let formRef = ref(null);
+  const loading = ref(false);
+
+  // 图片数组和字符串的双向绑定
+  const imageArray = computed({
+    get() {
+      return form.model.image ? [form.model.image] : [];
+    },
+    set(value) {
+      form.model.image = value.length > 0 ? value[0] : '';
+    },
+  });
+
+  const form = reactive({
+    model: {
+      title: '',
+      image: '',
+      linkType: 0,
+      link: '',
+      seq: 0,
+    },
+    rules: {
+      title: [{ required: true, message: '请输入轮播图标题', trigger: 'blur' }],
+      image: [{ required: true, message: '请上传轮播图片', trigger: 'change' }],
+      link: [{ required: true, message: '请输入链接地址', trigger: 'blur' }],
+    },
+  });
+
+  // 获取详情
+  async function getDetail(id) {
+    loading.value = true;
+    const { code, data } = await api.banner.detail(id);
+    if (code == 200) {
+      form.model = data;
+    }
+    loading.value = false;
+  }
+
+  // 表单关闭时提交
+  async function confirm() {
+    unref(formRef).validate(async (valid) => {
+      if (!valid) return;
+      let submitForm = cloneDeep(form.model);
+      if (props.modal.params.type == 'edit') {
+        submitForm.id = props.modal.params.id;
+      }
+
+      const { code } =
+        props.modal.params.type == 'add'
+          ? await api.banner.add(submitForm)
+          : await api.banner.edit(submitForm);
+
+      if (code == 200) {
+        emit('modalCallBack', { event: 'confirm' });
+      }
+    });
+  }
+
+  async function init() {
+    if (props.modal.params.id) {
+      await getDetail(props.modal.params.id);
+    }
+  }
+
+  onMounted(() => {
+    init();
+  });
+</script>
+
+<style lang="scss" scoped>
+  .banner-edit {
+    .form-tip {
+      margin-top: 4px;
+      font-size: 12px;
+      color: #909399;
+      line-height: 1.4;
+    }
+  }
+</style>

+ 203 - 0
src/app/admin/views/banner/index.vue

@@ -0,0 +1,203 @@
+<template>
+  <el-container class="banner-view panel-block">
+    <el-header class="sa-header">
+      <div class="sa-title sa-flex sa-row-between">
+        <div class="label sa-flex">轮播图管理</div>
+        <div>
+          <el-button class="sa-button-refresh" icon="RefreshRight" @click="getData()"></el-button>
+          <el-button icon="Plus" type="primary" @click="addRow">新建</el-button>
+        </div>
+      </div>
+    </el-header>
+    <el-main class="sa-p-0">
+      <div class="sa-table-wrap panel-block panel-block--bottom" v-loading="loading">
+        <el-table
+          height="100%"
+          class="sa-table"
+          :data="table.data"
+          @sort-change="fieldFilter"
+          row-key="id"
+          stripe
+        >
+          <template #empty>
+            <sa-empty />
+          </template>
+          <el-table-column type="selection" width="48" align="center"></el-table-column>
+          <el-table-column prop="id" label="ID" min-width="100" sortable="custom">
+          </el-table-column>
+          <el-table-column label="轮播图标题" min-width="200">
+            <template #default="scope">
+              <span class="sa-table-line-1">
+                {{ scope.row.title || '-' }}
+              </span>
+            </template>
+          </el-table-column>
+          <el-table-column label="图片" min-width="120" align="center">
+            <template #default="scope">
+              <el-image
+                v-if="scope.row.image"
+                :src="scope.row.image"
+                style="width: 80px; height: 50px"
+                fit="cover"
+                :preview-src-list="[scope.row.image]"
+                :z-index="9999"
+                preview-teleported
+              />
+              <span v-else>-</span>
+            </template>
+          </el-table-column>
+          <el-table-column label="链接类型" min-width="100" align="center">
+            <template #default="scope">
+              <el-tag :type="scope.row.linkType === 0 ? 'primary' : 'success'">
+                {{ scope.row.linkType === 0 ? '内链' : '外链' }}
+              </el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column label="链接地址" min-width="200">
+            <template #default="scope">
+              <span class="sa-table-line-1">
+                {{ scope.row.link || '-' }}
+              </span>
+            </template>
+          </el-table-column>
+          <el-table-column label="排序" min-width="80" align="center">
+            <template #default="scope">
+              {{ scope.row.seq || 0 }}
+            </template>
+          </el-table-column>
+          <el-table-column label="创建时间" min-width="160">
+            <template #default="scope">
+              {{ scope.row.createTime || '-' }}
+            </template>
+          </el-table-column>
+          <el-table-column fixed="right" label="操作">
+            <template #default="scope">
+              <el-button class="is-link" type="primary" @click="editRow(scope.row)">编辑</el-button>
+              <el-popconfirm
+                width="fit-content"
+                confirm-button-text="确认"
+                cancel-button-text="取消"
+                title="确认删除这条记录?"
+                @confirm="deleteApi(scope.row.id)"
+              >
+                <template #reference>
+                  <el-button class="is-link" type="danger"> 删除 </el-button>
+                </template>
+              </el-popconfirm>
+            </template>
+          </el-table-column>
+        </el-table>
+      </div>
+    </el-main>
+    <sa-view-bar>
+      <template #right>
+        <sa-pagination :pageData="pageData" @updateFn="getData" />
+      </template>
+    </sa-view-bar>
+  </el-container>
+</template>
+
+<script setup>
+  import { onMounted, reactive, ref } from 'vue';
+  import { ElMessage } from 'element-plus';
+  import { useModal, usePagination } from '@/sheep/hooks';
+  import api from '../../api';
+  import BannerEdit from './edit.vue';
+
+  // 分页数据
+  const { pageData } = usePagination();
+
+  // 表格数据
+  const table = reactive({
+    data: [],
+    selection: [],
+  });
+
+  const loading = ref(false);
+
+  // 获取数据
+  async function getData(page = pageData.page, searchData = {}) {
+    loading.value = true;
+    try {
+      const params = {
+        page: page,
+        size: pageData.size,
+        ...searchData,
+      };
+
+      const { code, data } = await api.banner.list(params);
+      if (code == 200) {
+        table.data = data.list;
+        pageData.page = data.pageNum;
+        pageData.size = data.pageSize;
+        pageData.total = data.total;
+      }
+    } catch (error) {
+      console.error('获取轮播图列表失败:', error);
+      ElMessage.error('获取数据失败');
+    } finally {
+      loading.value = false;
+    }
+  }
+
+  // 新建
+  function addRow() {
+    useModal(
+      BannerEdit,
+      {
+        title: '新建轮播图',
+        type: 'add',
+      },
+      {
+        confirm: () => {
+          getData();
+        },
+      },
+    );
+  }
+
+  // 编辑
+  function editRow(row) {
+    useModal(
+      BannerEdit,
+      {
+        title: '编辑轮播图',
+        type: 'edit',
+        id: row.id,
+      },
+      {
+        confirm: () => {
+          getData();
+        },
+      },
+    );
+  }
+
+  // 删除
+  async function deleteApi(id) {
+    try {
+      const { code } = await api.banner.delete({ id });
+      if (code == 200) {
+        getData();
+      }
+    } catch (error) {}
+  }
+
+  // 字段排序
+  function fieldFilter({ prop, order }) {
+    // 实现排序逻辑
+    console.log('排序:', prop, order);
+  }
+
+  onMounted(() => {
+    getData();
+  });
+</script>
+
+<style lang="scss" scoped>
+  .banner-view {
+    .search-container {
+      margin-bottom: 16px;
+    }
+  }
+</style>

+ 3 - 11
src/app/shop/admin/order/order.service.js

@@ -130,12 +130,10 @@ const api = {
     // 发货
     dispatch: (data) =>
       request({
-        url: '/shop/admin/order/order/dispatch',
+        url: '/mall/order/batchDelivery',
         method: 'POST',
         data,
       }),
-    // 导出
-    export: (type, params) => EXPORT(`/shop/admin/order/order/${type}`, params),
     // 上传发货单发货
     dispatchByUpload: (data) =>
       request({
@@ -165,7 +163,7 @@ const api = {
       return request({
         url: '/mall/order/cancel',
         method: 'POST',
-        params: { ids: idsString },
+        params: { id: idsString },
       });
     },
     // 退款订单
@@ -175,7 +173,7 @@ const api = {
       return request({
         url: '/mall/order/refund',
         method: 'POST',
-        params: { ids: idsString },
+        params: { id: idsString },
       });
     },
     // 联系用户
@@ -184,12 +182,6 @@ const api = {
         url: `/shop/admin/order/order/contact/${id}`,
         method: 'POST',
       }),
-    // 导出订单
-    export: (id) =>
-      request({
-        url: `/shop/admin/order/order/export/${id}`,
-        method: 'GET',
-      }),
     // 关闭订单
     close: (id) =>
       request({

+ 25 - 3
src/app/shop/admin/order/order/detail.vue

@@ -159,6 +159,14 @@
         <el-button v-if="state.orderDetail.paid === 0" type="warning" plain @click="onCancelOrder"
           >取消订单</el-button
         >
+        <!-- 立即发货按钮 - 已付款待发货状态显示 -->
+        <el-button type="primary" @click="onDispatch">立即发货测试</el-button>
+        <el-button
+          type="primary"
+          @click="onDispatch"
+          v-if="state.orderDetail.paid === 1 && state.orderDetail.status === 0"
+          >立即发货</el-button
+        >
         <!-- 全部退款按钮 - 固定显示 -->
         <el-button type="danger" plain @click="onRefund">全部退款</el-button>
       </div>
@@ -457,6 +465,22 @@
     } catch (error) {}
   }
 
+  // 立即发货
+  function onDispatch() {
+    useModal(
+      OrderDispatch,
+      {
+        title: '订单发货',
+        data: state.orderDetail,
+      },
+      {
+        success: () => {
+          getOrderDetail(); // 刷新订单详情
+        },
+      },
+    );
+  }
+
   // 全部退款
   async function onRefund() {
     try {
@@ -530,9 +554,7 @@
       OrderDispatch,
       {
         title: '订单发货',
-        params: {
-          data: state.orderDetail,
-        },
+        data: state.orderDetail,
       },
       {
         success: () => {

+ 44 - 39
src/app/shop/admin/order/order/dispatch.vue

@@ -22,46 +22,56 @@
           <el-table-column label="商品信息" min-width="360">
             <template #default="scope">
               <div class="sa-flex">
-                <sa-image :url="scope.row.goods_image" size="40"></sa-image>
-                <div class="sa-m-l-8">{{ scope.row.goods_title }}</div>
+                <sa-image :url="scope.row.image" size="40"></sa-image>
+                <div class="sa-m-l-8">{{ scope.row.productName }}</div>
               </div>
             </template>
           </el-table-column>
-          <el-table-column prop="goods_num" label="数量" min-width="80"></el-table-column>
-          <el-table-column prop="dispatch_status_text" label="状态" min-width="140">
+          <el-table-column label="规格" min-width="120">
             <template #default="scope">
-              {{ scope.row.dispatch_status_text }}/{{ scope.row.aftersale_status_text }}
+              {{ scope.row.sku || '-' }}
             </template>
           </el-table-column>
+          <el-table-column label="数量" min-width="80">
+            <template #default="scope">
+              {{ scope.row.payNum }}
+            </template>
+          </el-table-column>
+          <el-table-column label="状态" min-width="140">
+            <template #default="scope"> 待发货 </template>
+          </el-table-column>
           <el-table-column label="快递单号" min-width="80">
             <template #default>-</template>
           </el-table-column>
         </el-table>
         <div class="address">
           <div class="title sa-m-b-16">配送信息</div>
-          <div class="sa-m-l-16" v-if="props.modal?.params?.params?.data?.address">
+          <div class="sa-m-l-16" v-if="props.modal?.params?.data?.orderAddressVO">
             <div class="sa-flex sa-m-b-8">
               <div class="label">收货信息:</div>
               <div class="content">
-                {{ props.modal?.params?.params?.data?.address?.consignee }}&nbsp;
-                {{ props.modal?.params?.params?.data?.address?.mobile }}
+                {{ props.modal?.params?.data?.orderAddressVO?.realName }}&nbsp;
+                {{ props.modal?.params?.data?.orderAddressVO?.phone }}
               </div>
             </div>
             <div class="sa-flex sa-col-top sa-m-b-16">
               <div class="label">收货地址:</div>
               <div class="content">
                 <div>
-                  {{ props.modal?.params?.params?.data?.address?.province_name }}&nbsp;
-                  {{ props.modal?.params?.params?.data?.address?.city_name }}&nbsp;
-                  {{ props.modal?.params?.params?.data?.address?.district_name }}
+                  {{ props.modal?.params?.data?.orderAddressVO?.province }}&nbsp;
+                  {{ props.modal?.params?.data?.orderAddressVO?.city }}&nbsp;
+                  {{ props.modal?.params?.data?.orderAddressVO?.district }}
+                </div>
+                <div>{{ props.modal?.params?.data?.orderAddressVO?.detail }}</div>
+                <div v-if="props.modal?.params?.data?.orderAddressVO?.postCode">
+                  邮编:{{ props.modal?.params?.data?.orderAddressVO?.postCode }}
                 </div>
-                <div>{{ props.modal?.params?.params?.data?.address?.address }}</div>
               </div>
             </div>
           </div>
-          <template v-if="!props.modal?.params?.params?.data?.address">{{
-            props.modal?.params?.params?.data?.address_id
-          }}</template>
+          <template v-if="!props.modal?.params?.data?.orderAddressVO">
+            地址ID:{{ props.modal?.params?.data?.addressId }}
+          </template>
         </div>
         <div class="express">
           <div class="title sa-m-b-16">物流信息</div>
@@ -212,7 +222,7 @@
   </el-container>
 </template>
 <script setup>
-  import { computed, getCurrentInstance, onMounted, reactive, ref, nextTick, watch } from 'vue';
+  import { computed, getCurrentInstance, onMounted, reactive, ref, watch, nextTick } from 'vue';
   import { api } from '../order.service';
   import useExpress from '@/app/shop/admin/data/express/express.js';
   import draggable from 'vuedraggable';
@@ -228,13 +238,12 @@
 
   const state = reactive({
     nosendItem: computed(() => {
-      if (!props.modal?.params?.params?.data?.items) return [];
-      return props.modal.params.params.data.items.filter(
-        (i) =>
-          (i.dispatch_status == 0 || i.dispatch_status === undefined) &&
-          (i.refund_status == 0 || i.refund_status === undefined) &&
-          (i.dispatch_type == 'express' || i.dispatch_type === undefined),
-      );
+      console.log('发货弹窗接收到的数据:', props.modal);
+      console.log('订单详情数据:', props.modal?.params?.data);
+      console.log('商品信息数据:', props.modal?.params?.data?.orderInfoVO);
+      console.log('地址信息数据:', props.modal?.params?.data?.orderAddressVO);
+      if (!props.modal?.params?.data?.orderInfoVO) return [];
+      return props.modal.params?.data.orderInfoVO;
     }),
     method: 'input',
     expressItem: [],
@@ -246,13 +255,8 @@
 
     dispatch_type: 'express',
     customItem: computed(() => {
-      if (!props.modal?.params?.params?.data?.items) return [];
-      return props.modal.params.params.data.items.filter(
-        (i) =>
-          (i.dispatch_status == 0 || i.dispatch_status === undefined) &&
-          (i.refund_status == 0 || i.refund_status === undefined) &&
-          i.dispatch_type == 'custom',
-      );
+      // 手动发货暂不支持,返回空数组
+      return [];
     }),
     custom_type: 'text',
     custom_content: [],
@@ -275,14 +279,15 @@
         proxy.$refs.expressRef &&
           proxy.$refs.expressRef.validate(async (valid) => {
             if (valid) {
+              const { name, no } = express;
               state.sendLoading = true;
-              const { code } = await api.order.dispatch({
-                order_id: props.modal?.params?.params?.data?.id,
-                order_item_ids,
-                action: 'confirm',
-                method: 'input',
-                express: express,
-              });
+              const { code } = await api.order.dispatch([
+                {
+                  orderId: props.modal.params?.data?.id,
+                  deliveryName: name,
+                  deliveryNo: no,
+                },
+              ]);
               state.sendLoading = false;
               if (code == '200') {
                 emit('modalCallBack', { event: 'confirm' });
@@ -292,7 +297,7 @@
       } else if (state.method == 'api') {
         state.sendLoading = true;
         const { code } = await api.order.dispatch({
-          order_id: props.modal?.params?.params?.data?.id,
+          order_id: props.modal.data.id,
           order_item_ids,
           action: 'confirm',
           method: 'api',
@@ -304,7 +309,7 @@
       }
     } else if (state.dispatch_type == 'custom') {
       const { code } = await api.order.customDispatch({
-        order_id: props.modal?.params?.params?.data?.id,
+        order_id: props.modal.data.id,
         order_item_ids,
         custom_type: state.custom_type,
         custom_content: state.custom_content,

+ 31 - 9
src/app/shop/admin/order/order/index.vue

@@ -184,6 +184,9 @@
               <el-table-column label="操作" min-width="150" fixed="right">
                 <template #default="scope">
                   <div class="sa-flex">
+                    <el-button class="is-link" type="primary" @click="onSend(scope.row)"
+                      >发货测试</el-button
+                    >
                     <el-button
                       v-if="scope.row.paid === 1 && scope.row.status === 0"
                       class="is-link"
@@ -305,7 +308,9 @@
   const handleTabChange = (status) => {
     currentStatus.value = status;
     const statusParams = statusMap[status] || {};
-    getData(1, statusParams);
+    // 合并当前搜索条件和状态参数
+    const mergedParams = { ...currentSearchParams.value, ...statusParams };
+    getData(1, mergedParams);
   };
 
   // 使用公共配置的状态处理函数
@@ -353,8 +358,15 @@
 
   const { pageData } = usePagination();
 
+  // 保存当前搜索条件
+  const currentSearchParams = ref({});
+
   // 获取数据
   async function getData(page, searchParams = {}) {
+    // 保存搜索条件供导出使用
+    if (Object.keys(searchParams).length > 0) {
+      currentSearchParams.value = { ...searchParams };
+    }
     if (page) pageData.page = page;
     loading.value = true;
 
@@ -399,10 +411,22 @@
   async function onExport(type) {
     exportLoading.value = true;
     try {
-      await api.order.export(type, {});
-    } catch (error) {
-      console.error('导出失败:', error);
-    }
+      // 构建导出参数,使用当前搜索条件但不包含分页参数
+      const exportParams = { ...currentSearchParams.value };
+
+      // 处理时间范围搜索
+      if (exportParams.createTime && exportParams.createTime.length === 2) {
+        exportParams.startTime = exportParams.createTime[0];
+        exportParams.endTime = exportParams.createTime[1];
+        delete exportParams.createTime;
+      }
+
+      // 添加当前状态筛选
+      const statusParams = statusMap[currentStatus.value] || {};
+      Object.assign(exportParams, statusParams);
+
+      await api.order.export(exportParams, '订单记录');
+    } catch (error) {}
     exportLoading.value = false;
   }
 
@@ -460,7 +484,7 @@
         type: 'warning',
       });
 
-      const { code, message } = await api.order.cancel([id]);
+      const { code, message } = await api.order.cancel(id);
       if (code == 200) {
         ElMessage.success('订单取消成功');
         getData(); // 刷新列表
@@ -495,9 +519,7 @@
       OrderDispatch,
       {
         title: '订单发货',
-        params: {
-          data: row,
-        },
+        data: row,
       },
       {
         success: () => {

+ 10 - 7
src/sheep/request/crud.js

@@ -96,7 +96,7 @@ export const RESTORE = (url, id) =>
   });
 
 // 通用增删改查
-export const CRUD = (url, methods = ['list', 'detail', 'add', 'edit', 'delete']) => {
+export const CRUD = (url, methods = ['list', 'detail', 'add', 'edit', 'delete', 'export']) => {
   const apis = {};
   if (methods.includes('list'))
     apis.list = (params, pageInParams = true) => LIST(url, params, pageInParams);
@@ -105,6 +105,8 @@ export const CRUD = (url, methods = ['list', 'detail', 'add', 'edit', 'delete'])
   if (methods.includes('edit')) apis.edit = (id, data) => EDIT(url, id, data);
   if (methods.includes('delete')) apis.delete = (id) => DELETE(url, id);
   if (methods.includes('report')) apis.report = (params, filename) => REPORT(url, params, filename);
+  if (methods.includes('export'))
+    apis.export = (params, filename) => REPORT(url, params, filename, 'export');
   return apis;
 };
 
@@ -118,7 +120,7 @@ export const RECYCLE = (url) => {
 };
 
 // 导出报表 - 需要超级管理员权限
-export const REPORT = async (url, params, filename = '导出数据') => {
+export const REPORT = async (url, params, filename = '导出数据', typeName = 'report') => {
   // 获取用户信息进行权限验证
   const userInfo = $storage.get('userInfo');
 
@@ -127,18 +129,19 @@ export const REPORT = async (url, params, filename = '导出数据') => {
     ElMessage.error('只有超级管理员可以导出');
     return Promise.reject(new Error('权限不足:只有超级管理员可以导出'));
   }
-
+  let response = null;
   try {
     // 发送导出请求
-    const response = await request({
-      url: url + '/report',
-      method: 'GET',
-      params,
+    response = await request({
+      url: url + `/${typeName}`,
+      method: typeName == 'export' ? 'POST' : 'GET',
+      ...(typeName == 'export' ? { data: params } : { params }),
       responseType: 'blob', // 用于文件下载
       options: {
         showSuccessMessage: false,
       },
     });
+
     // 检查响应状态和内容类型
     if (response.status === 200 && response.headers['content-type']?.includes('excel')) {
       // 成功响应,处理文件下载