Files
gao-customer-h5/src/pages/store/StoreSort.vue
T
2026-08-14 16:08:05 +08:00

224 lines
4.4 KiB
Vue

<template>
<div v-can="permissionCodes.store.update" class="store-sort">
<div class="tip" v-text="'按住右侧拖动手柄调整门店顺序,保存后生效'" />
<div class="items">
<t-cell-group v-if="storeList.length" theme="card">
<vue-draggable-next
v-model="storeList"
class="draggable"
item-key="id"
handle=".handle"
ghost-class="ghost"
chosen-class="chosen"
drag-class="dragging"
:animation="180"
:disabled="isLoading || isSaving"
@change="handleSortChange"
>
<t-cell
v-for="element in storeList"
:key="element.id"
class="cell"
:title="element.name || '未命名门店'"
:description="`编码:${element.code || '-'}`"
>
<template #note>
<t-tag
:theme="getStatusTheme(element.status)"
variant="outline"
:content="getStatusText(element.status)"
/>
</template>
<template #rightIcon>
<t-icon
class="handle"
role="button"
name="move"
aria-label="拖动排序"
/>
</template>
</t-cell>
</vue-draggable-next>
</t-cell-group>
<div v-else class="empty-block" v-text="isLoading ? '正在加载门店' : '暂无门店'" />
</div>
<t-button
class="save"
theme="primary"
:loading="isSaving"
:disabled="!sortDirty || isSaving || isLoading"
@click="saveSort"
>
保存排序
</t-button>
</div>
</template>
<script setup lang="ts">
import { Toast, type TagProps } from "tdesign-mobile-vue";
import { VueDraggableNext } from "vue-draggable-next";
import StoreAPI from "@/api/StoreAPI";
import { useNavBarStore } from "@/store/navBarStore";
import type { Store, StoreStatus } from "@/types/Store";
import { permissionCodes } from "@/utils/permission";
defineOptions({
name: "StoreSort"
});
const navBarStore = useNavBarStore();
const router = useRouter();
// ---------- 列表数据 ----------
const storeList = ref<Store[]>([]);
const isLoading = ref(false);
const isSaving = ref(false);
const sortDirty = ref(false);
// ---------- 展示文案 ----------
function getStatusText(status?: StoreStatus): string {
return status === "ACTIVE" ? "启用" : "停用";
}
function getStatusTheme(status?: StoreStatus): TagProps["theme"] {
return status === "ACTIVE" ? "primary" : "default";
}
// ---------- 拖拽排序 ----------
function handleSortChange(): void {
sortDirty.value = true;
}
// ---------- 保存 ----------
async function saveSort(): Promise<void> {
if (!sortDirty.value || isSaving.value) {
return;
}
isSaving.value = true;
try {
const idList = storeList.value
.map(item => item.id)
.filter((id): id is string => Boolean(id));
await StoreAPI.sort(idList);
storeList.value.forEach((item, index) => {
item.sort = index;
});
sortDirty.value = false;
Toast.success("门店排序已保存");
await router.back();
} catch {
Toast.error("门店排序保存失败");
} finally {
isSaving.value = false;
}
}
// ---------- 页面数据 ----------
async function loadData(): Promise<void> {
isLoading.value = true;
try {
const result = await StoreAPI.list({
index: 0,
size: 100,
orderMap: {
sort: "ASC",
createdAt: "DESC"
}
});
storeList.value = result.list || [];
sortDirty.value = false;
} finally {
isLoading.value = false;
}
}
onMounted(async () => {
navBarStore.clearRight();
try {
await loadData();
} catch {
Toast.error("门店加载失败");
}
});
onUnmounted(() => {
navBarStore.clearRight();
});
</script>
<style scoped lang="less">
.store-sort {
padding: calc(var(--app-nav-height) + 1rem) 0 6rem;
.tip {
color: #52607a;
padding: 0 1rem .75rem;
font-size: .8125rem;
}
.items {
.draggable {
.cell {
position: relative;
.handle {
color: #52607a;
cursor: grab;
touch-action: none;
}
}
.ghost {
opacity: .4;
}
.chosen,
.dragging {
z-index: 1;
box-shadow: 0 .5rem 1rem rgba(15, 23, 42, .16);
}
}
}
.empty-block {
color: #52607a;
padding: 2rem 1rem;
text-align: center;
}
.save {
left: 1rem;
right: 1rem;
bottom: calc(.75rem + env(safe-area-inset-bottom, 0));
position: fixed;
}
}
:global(html.theme-dark) {
.store-sort {
.tip,
.empty-block,
.handle {
color: #94a3b8;
}
.draggable {
.cell {
background: rgba(15, 23, 42, .88);
box-shadow: 0 .5rem 1rem rgba(2, 6, 23, .28);
}
.ghost {
background: rgba(15, 23, 42, .6);
}
}
}
}
</style>