Bladeren bron

预定-分类加排序

zhuli 3 weken geleden
bovenliggende
commit
213ef25340

+ 1 - 1
src/api/modules/scheduledService.ts

@@ -30,7 +30,7 @@ export const scheduleDel = (params: { id: number | string }) => {
 
 
 /** 分类排序 */
 /** 分类排序 */
 export const scheduleSort = (data: { categoryIds: (number | string)[]; storeId: number | string }) => {
 export const scheduleSort = (data: { categoryIds: (number | string)[]; storeId: number | string }) => {
-  return httpApi.post(`${BASE_BOOKING}/sort`, data);
+  return httpApi.post(`${BASE_BOOKING}/updateSort`, data);
 };
 };
 
 
 /** 新增/编辑分类(保存或更新) */
 /** 新增/编辑分类(保存或更新) */

+ 35 - 17
src/components/ProTable/index.vue

@@ -62,9 +62,9 @@
             <el-radio v-if="item.type == 'radio'" v-model="radio" :label="scope.row[rowKey]">
             <el-radio v-if="item.type == 'radio'" v-model="radio" :label="scope.row[rowKey]">
               <i />
               <i />
             </el-radio>
             </el-radio>
-            <!-- sort -->
-            <el-tag v-if="item.type == 'sort' && item.condition && item.condition(scope, index)" class="move">
-              <el-icon> <DCaret /></el-icon>
+            <!-- sort:未配置 condition 时默认显示拖拽手柄 -->
+            <el-tag v-if="item.type == 'sort' && showSortHandle(item, scope, index)" class="move sort-drag-handle">
+              <el-icon><Rank /></el-icon>
             </el-tag>
             </el-tag>
           </template>
           </template>
         </el-table-column>
         </el-table-column>
@@ -104,13 +104,13 @@
 </template>
 </template>
 
 
 <script setup lang="ts" name="ProTable">
 <script setup lang="ts" name="ProTable">
-import { ref, watch, provide, onMounted, unref, computed, reactive } from "vue";
+import { ref, watch, provide, onMounted, unref, computed, reactive, nextTick } from "vue";
 import { ElTable } from "element-plus";
 import { ElTable } from "element-plus";
 import { useTable } from "@/hooks/useTable";
 import { useTable } from "@/hooks/useTable";
 import { useSelection } from "@/hooks/useSelection";
 import { useSelection } from "@/hooks/useSelection";
 import { BreakPoint } from "@/components/Grid/interface";
 import { BreakPoint } from "@/components/Grid/interface";
 import { ColumnProps, TypeProps } from "@/components/ProTable/interface";
 import { ColumnProps, TypeProps } from "@/components/ProTable/interface";
-import { Refresh, Operation, Search } from "@element-plus/icons-vue";
+import { Refresh, Operation, Search, Rank } from "@element-plus/icons-vue";
 import { generateUUID, handleProp } from "@/utils";
 import { generateUUID, handleProp } from "@/utils";
 import SearchForm from "@/components/SearchForm/index.vue";
 import SearchForm from "@/components/SearchForm/index.vue";
 import Pagination from "./components/Pagination.vue";
 import Pagination from "./components/Pagination.vue";
@@ -157,6 +157,12 @@ const uuid = ref("id-" + generateUUID());
 // column 列类型
 // column 列类型
 const columnTypes: TypeProps[] = ["selection", "radio", "index", "expand", "sort"];
 const columnTypes: TypeProps[] = ["selection", "radio", "index", "expand", "sort"];
 
 
+function showSortHandle(item: ColumnProps, scope: any, index: number) {
+  const fn = item.condition as ((a?: any, b?: number) => boolean) | undefined;
+  if (fn == null) return true;
+  return fn(scope, index);
+}
+
 // 是否显示搜索模块
 // 是否显示搜索模块
 const isShowSearch = ref(true);
 const isShowSearch = ref(true);
 
 
@@ -178,12 +184,7 @@ const { tableData, pageable, searchParam, searchInitParam, getTableList, search,
 // 清空选中数据列表
 // 清空选中数据列表
 const clearSelection = () => tableRef.value!.clearSelection();
 const clearSelection = () => tableRef.value!.clearSelection();
 
 
-// 初始化表格数据 && 拖拽排序
-onMounted(() => {
-  dragSort();
-  props.requestAuto && getTableList();
-  props.data && (pageable.value.total = props.data.length);
-});
+let sortableInstance: Sortable | null = null;
 
 
 // 处理表格数据
 // 处理表格数据
 const processTableData = computed(() => {
 const processTableData = computed(() => {
@@ -297,20 +298,37 @@ const handleRefresh = () => {
   }
   }
 };
 };
 
 
-// 表格拖拽排序
-const dragSort = () => {
-  const tbody = document.querySelector(`#${uuid.value} tbody`) as HTMLElement;
-  Sortable.create(tbody, {
+// 表格拖拽排序(存在 sort 列时启用;数据更新后需重建实例)
+const dragSortInit = () => {
+  const hasSortCol = tableColumns.some((c: ColumnProps) => c.type === "sort");
+  if (!hasSortCol) return;
+  sortableInstance?.destroy();
+  sortableInstance = null;
+  const tbody = document.querySelector(`#${uuid.value} tbody`) as HTMLElement | null;
+  if (!tbody) return;
+  sortableInstance = Sortable.create(tbody, {
     handle: ".move",
     handle: ".move",
     animation: 300,
     animation: 300,
     onEnd({ newIndex, oldIndex }) {
     onEnd({ newIndex, oldIndex }) {
-      const [removedItem] = processTableData.value.splice(oldIndex!, 1);
-      processTableData.value.splice(newIndex!, 0, removedItem);
+      if (oldIndex == null || newIndex == null || oldIndex === newIndex) return;
+      const [removedItem] = processTableData.value.splice(oldIndex, 1);
+      processTableData.value.splice(newIndex, 0, removedItem);
       emit("dragSort", { newIndex, oldIndex });
       emit("dragSort", { newIndex, oldIndex });
     }
     }
   });
   });
 };
 };
 
 
+onMounted(() => {
+  props.data && (pageable.value.total = props.data.length);
+  if (props.requestAuto) {
+    getTableList().then(() => nextTick(dragSortInit));
+  } else {
+    nextTick(dragSortInit);
+  }
+});
+
+watch(tableData, () => nextTick(dragSortInit));
+
 // 暴露给父组件的参数和方法 (外部需要什么,都可以从这里暴露出去)
 // 暴露给父组件的参数和方法 (外部需要什么,都可以从这里暴露出去)
 defineExpose({
 defineExpose({
   element: tableRef,
   element: tableRef,

+ 1 - 1
src/components/ProTable/interface/index.ts

@@ -81,7 +81,7 @@ export interface ColumnProps<T = any>
   headerRender?: (scope: HeaderRenderScope<T>) => VNode; // 自定义表头内容渲染(tsx语法)
   headerRender?: (scope: HeaderRenderScope<T>) => VNode; // 自定义表头内容渲染(tsx语法)
   render?: (scope: RenderScope<T>) => VNode | string; // 自定义单元格内容渲染(tsx语法)
   render?: (scope: RenderScope<T>) => VNode | string; // 自定义单元格内容渲染(tsx语法)
   _children?: ColumnProps<T>[]; // 多级表头
   _children?: ColumnProps<T>[]; // 多级表头
-  condition?: (params?: any) => boolean;
+  condition?: (scope?: any, index?: number) => boolean;
 }
 }
 
 
 export type ProTableInstance = Omit<InstanceType<typeof ProTable>, keyof ComponentPublicInstance | keyof ProTableProps>;
 export type ProTableInstance = Omit<InstanceType<typeof ProTable>, keyof ComponentPublicInstance | keyof ProTableProps>;

+ 69 - 4
src/views/appoinmentManagement/classifyManagement.vue

@@ -1,9 +1,20 @@
 <template>
 <template>
   <div class="table-box classify-management">
   <div class="table-box classify-management">
-    <ProTable ref="proTable" :columns="columns" :request-api="getTableList" :init-param="initParam">
+    <ProTable
+      ref="proTable"
+      :columns="columns"
+      :request-api="getTableList"
+      :init-param="initParam"
+      :pagination="false"
+      @drag-sort="onDragSortEnd"
+    >
       <template #tableHeader>
       <template #tableHeader>
         <div class="table-header">
         <div class="table-header">
-          <el-button type="primary" @click="handleNew"> 新建 </el-button>
+          <div class="table-header-left">
+            <el-button type="primary" @click="handleNew"> 新建 </el-button>
+            <span v-if="sortSaving" class="sort-saving-tip">正在保存排序…</span>
+            <span v-else class="sort-tip">拖动「排序」列图标可调整顺序,松手后自动保存</span>
+          </div>
         </div>
         </div>
       </template>
       </template>
       <template #isDisplay="{ row }">
       <template #isDisplay="{ row }">
@@ -77,6 +88,7 @@
 <script setup lang="ts">
 <script setup lang="ts">
 import { ref, reactive } from "vue";
 import { ref, reactive } from "vue";
 import { ElMessage, ElMessageBox } from "element-plus";
 import { ElMessage, ElMessageBox } from "element-plus";
+import { useDebounceFn } from "@vueuse/core";
 import type { FormInstance, FormRules } from "element-plus";
 import type { FormInstance, FormRules } from "element-plus";
 import type { UploadRequestOptions, UploadUserFile } from "element-plus";
 import type { UploadRequestOptions, UploadUserFile } from "element-plus";
 import { Plus } from "@element-plus/icons-vue";
 import { Plus } from "@element-plus/icons-vue";
@@ -88,7 +100,8 @@ import {
   scheduleAdd,
   scheduleAdd,
   scheduleEdit,
   scheduleEdit,
   scheduleDetail,
   scheduleDetail,
-  getHasReservation
+  getHasReservation,
+  scheduleSort
 } from "@/api/modules/scheduledService";
 } from "@/api/modules/scheduledService";
 import { uploadFileToOss } from "@/api/upload.js";
 import { uploadFileToOss } from "@/api/upload.js";
 import { localGet } from "@/utils";
 import { localGet } from "@/utils";
@@ -103,8 +116,10 @@ export interface CategoryRow {
 }
 }
 
 
 const proTable = ref<ProTableInstance>();
 const proTable = ref<ProTableInstance>();
+const sortSaving = ref(false);
 
 
 const columns: ColumnProps<CategoryRow>[] = [
 const columns: ColumnProps<CategoryRow>[] = [
+  { type: "sort", label: "排序", width: 64, align: "center" },
   { prop: "_seq", label: "序号", width: 60, align: "center" },
   { prop: "_seq", label: "序号", width: 60, align: "center" },
   { prop: "name", label: "名称" },
   { prop: "name", label: "名称" },
   { prop: "maxBookingTime", label: "最长预订时间(分钟)", align: "center" },
   { prop: "maxBookingTime", label: "最长预订时间(分钟)", align: "center" },
@@ -112,8 +127,11 @@ const columns: ColumnProps<CategoryRow>[] = [
   { prop: "operation", label: "操作", fixed: "right", width: 140, align: "center" }
   { prop: "operation", label: "操作", fixed: "right", width: 140, align: "center" }
 ];
 ];
 
 
+/** 关闭分页并一次拉全量,拖拽排序后提交的 categoryIds 与门店顺序一致 */
 const initParam = reactive({
 const initParam = reactive({
-  storeId: localGet("geeker-user")?.userInfo?.storeId ?? localGet("createdId") ?? ""
+  storeId: localGet("geeker-user")?.userInfo?.storeId ?? localGet("createdId") ?? "",
+  pageNum: 1,
+  pageSize: 500
 });
 });
 
 
 function mapCategoryItem(item: any): CategoryRow {
 function mapCategoryItem(item: any): CategoryRow {
@@ -230,6 +248,39 @@ function getStoreId(): number | string | null {
   return localGet("geeker-user")?.userInfo?.storeId ?? localGet("createdId") ?? null;
   return localGet("geeker-user")?.userInfo?.storeId ?? localGet("createdId") ?? null;
 }
 }
 
 
+function getTableRowsFromProTable(): CategoryRow[] {
+  const p = proTable.value as any;
+  const td = p?.tableData;
+  const list = td && typeof td === "object" && "value" in td ? td.value : td;
+  return Array.isArray(list) ? list : [];
+}
+
+/** 拖拽结束后按当前表格顺序保存(防抖,避免连续误触) */
+const persistSortOrder = useDebounceFn(async () => {
+  const storeId = getStoreId();
+  if (!storeId) return;
+  const rows = getTableRowsFromProTable();
+  const categoryIds = rows.map(r => r.id).filter(id => id != null && id !== "");
+  if (!categoryIds.length) return;
+  sortSaving.value = true;
+  try {
+    await scheduleSort({ categoryIds, storeId });
+    rows.forEach((row, i) => {
+      row._seq = i + 1;
+    });
+    ElMessage.success("排序已保存");
+  } catch (e: any) {
+    ElMessage.error(e?.message || "排序保存失败");
+    proTable.value?.getTableList();
+  } finally {
+    sortSaving.value = false;
+  }
+}, 400);
+
+function onDragSortEnd() {
+  persistSortOrder();
+}
+
 /** 接口返回 true 表示该分类下桌号存在预定,不可编辑/删除 */
 /** 接口返回 true 表示该分类下桌号存在预定,不可编辑/删除 */
 function parseHasReservationResponse(res: any): boolean {
 function parseHasReservationResponse(res: any): boolean {
   const body = res?.data ?? res;
   const body = res?.data ?? res;
@@ -395,6 +446,20 @@ async function submitForm() {
   width: 100%;
   width: 100%;
   padding: 12px 0;
   padding: 12px 0;
 }
 }
+.table-header-left {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 12px;
+  align-items: center;
+}
+.sort-tip,
+.sort-saving-tip {
+  font-size: 12px;
+  color: #909399;
+}
+.sort-saving-tip {
+  color: #6c8ff8;
+}
 .status-show {
 .status-show {
   color: #67c23a;
   color: #67c23a;
 }
 }