219 lines
5.1 KiB
Vue
219 lines
5.1 KiB
Vue
<template>
|
||
<Modal v-model="modalVisible" :title="'应用排序 - ' + categoryName" width="560" :mask-closable="false"
|
||
@on-visible-change="onVisibleChange">
|
||
<Alert show-icon>拖动左侧手柄进行排序,排序结果会实时记录</Alert>
|
||
|
||
<div class="sort-loading" v-if="loading">
|
||
<span>加载中...</span>
|
||
</div>
|
||
|
||
<draggable v-else class="sort-list" v-model="sortList" item-key="id" handle=".drag-handle" @change="onDragChange">
|
||
<template #item="{ element, index }">
|
||
<div class="sort-item">
|
||
<span class="drag-handle">⠿</span>
|
||
<span class="sort-index">{{ index + 1 }}</span>
|
||
<span class="sort-name">{{ element.name || element.id }}</span>
|
||
<span class="sort-no">排序号: {{ element.sortNo ?? '-' }}</span>
|
||
</div>
|
||
</template>
|
||
</draggable>
|
||
|
||
<div class="sort-empty" v-if="!loading && sortList.length === 0">
|
||
暂无应用数据
|
||
</div>
|
||
|
||
<template #footer>
|
||
<Button type="primary" :loading="saving" @click="handleSave" :disabled="sortList.length === 0">
|
||
保存排序
|
||
</Button>
|
||
<Button @click="modalVisible = false">取消</Button>
|
||
</template>
|
||
</Modal>
|
||
</template>
|
||
|
||
<script lang="ts" setup>
|
||
import { ref, watch } from "vue";
|
||
import draggable from "vuedraggable";
|
||
|
||
const props = withDefaults(
|
||
defineProps<{
|
||
modelValue: boolean;
|
||
categoryId: number | null;
|
||
categoryName: string;
|
||
}>(),
|
||
{
|
||
categoryName: "",
|
||
}
|
||
);
|
||
|
||
const emit = defineEmits<{
|
||
(e: "update:modelValue", val: boolean): void;
|
||
(e: "saved"): void;
|
||
}>();
|
||
|
||
const modalVisible = ref(false);
|
||
const loading = ref(false);
|
||
const saving = ref(false);
|
||
const sortList = ref<{ id: number; name?: string; sortNo?: number }[]>([]);
|
||
const hasChanged = ref(false);
|
||
|
||
watch(
|
||
() => props.modelValue,
|
||
(val) => {
|
||
modalVisible.value = val;
|
||
if (val) {
|
||
fetchSortList();
|
||
}
|
||
}
|
||
);
|
||
|
||
watch(modalVisible, (val) => {
|
||
emit("update:modelValue", val);
|
||
});
|
||
|
||
function onVisibleChange(visible: boolean) {
|
||
if (!visible) {
|
||
hasChanged.value = false;
|
||
}
|
||
}
|
||
|
||
async function fetchSortList() {
|
||
if (!props.categoryId) return;
|
||
|
||
loading.value = true;
|
||
try {
|
||
const res = await Apis.AppSettings.appSortList({
|
||
params: { categoryId: props.categoryId },
|
||
cacheFor: null,
|
||
});
|
||
sortList.value = (res ?? []).slice().sort((a: any, b: any) => (a.sortNo ?? 0) - (b.sortNo ?? 0));
|
||
hasChanged.value = false;
|
||
} catch (e: any) {
|
||
console.warn(e?.message || "获取排序列表失败");
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
}
|
||
|
||
function onDragChange() {
|
||
hasChanged.value = true;
|
||
}
|
||
|
||
async function handleSave() {
|
||
if (!hasChanged.value) {
|
||
Message.info("排序未发生变化");
|
||
modalVisible.value = false;
|
||
return;
|
||
}
|
||
|
||
saving.value = true;
|
||
try {
|
||
const ids = sortList.value.map((item) => item.id);
|
||
await Apis.AppSettings.appSort({ data: ids });
|
||
Message.success("排序保存成功!");
|
||
hasChanged.value = false;
|
||
modalVisible.value = false;
|
||
emit("saved");
|
||
} catch (e: any) {
|
||
console.warn(e?.message || "排序保存失败");
|
||
} finally {
|
||
saving.value = false;
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.sort-list {
|
||
max-height: 400px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.sort-item {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 10px 12px;
|
||
margin-bottom: 6px;
|
||
background: #fafafa;
|
||
border: 1px solid #e8e8e8;
|
||
border-radius: 4px;
|
||
transition: background 0.2s;
|
||
|
||
&:hover {
|
||
background: #f0f4ff;
|
||
border-color: #c0d0ff;
|
||
}
|
||
}
|
||
|
||
/* 拖拽中:当前被拖拽的元素高亮(明显橙色,不放大避免横向滚动条) */
|
||
.sort-list :deep(.sort-item.sortable-chosen),
|
||
.sort-list :deep(.sort-item.sortable-drag) {
|
||
background: #fff7e6;
|
||
border-color: #fa8c16;
|
||
box-shadow: 0 6px 16px rgba(250, 140, 22, 0.4);
|
||
cursor: grabbing;
|
||
|
||
.drag-handle {
|
||
color: #fa8c16;
|
||
}
|
||
}
|
||
|
||
/* 拖拽中:落点占位位置(虚线提示) */
|
||
.sort-list :deep(.sort-item.sortable-ghost) {
|
||
background: #fffbe6;
|
||
border: 1px dashed #fa8c16;
|
||
opacity: 0.6;
|
||
}
|
||
|
||
.drag-handle {
|
||
cursor: grab;
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 0 8px;
|
||
color: #999;
|
||
flex-shrink: 0;
|
||
|
||
&:active {
|
||
cursor: grabbing;
|
||
}
|
||
}
|
||
|
||
.sort-index {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 24px;
|
||
height: 24px;
|
||
border-radius: 50%;
|
||
background: #577afb;
|
||
color: #fff;
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
flex-shrink: 0;
|
||
margin-right: 12px;
|
||
}
|
||
|
||
.sort-name {
|
||
flex: 1;
|
||
font-size: 14px;
|
||
color: #333;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.sort-no {
|
||
flex-shrink: 0;
|
||
font-size: 12px;
|
||
color: #999;
|
||
margin-left: 12px;
|
||
}
|
||
|
||
.sort-loading,
|
||
.sort-empty {
|
||
padding: 40px 0;
|
||
text-align: center;
|
||
color: #999;
|
||
font-size: 14px;
|
||
}
|
||
</style>
|