Skip to content

Commit 4607301

Browse files
committed
feat: 完善组合订阅中单条订阅排序
1 parent e30534a commit 4607301

2 files changed

Lines changed: 214 additions & 88 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "sub-store-front-end",
3-
"version": "2.16.52",
3+
"version": "2.16.55",
44
"private": true,
55
"scripts": {
66
"dev": "vite --host",

src/views/SubEditor.vue

Lines changed: 213 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -331,12 +331,17 @@
331331
<nut-checkbox v-model="subCheckbox" :indeterminate="subCheckboxIndeterminate" @click="subCheckboxClick"></nut-checkbox>
332332
</div>
333333
<nut-checkboxgroup
334-
v-model="form.subscriptions"
335-
class="subs-checkbox-wrapper"
334+
v-model="visibleSelectedSubscriptions"
335+
:class="[
336+
'subs-checkbox-wrapper',
337+
{
338+
'is-simple-mode': appearanceSetting.isSimpleMode,
339+
'is-dragging': isDragging,
340+
},
341+
]"
336342
>
337343
<draggable
338-
:list="filteredSubsSelectList"
339-
:sort="true"
344+
v-model="displayedSubsSelectList"
340345
item-key="0"
341346
animation="300"
342347
:scroll-sensitivity="200"
@@ -349,7 +354,6 @@
349354
>
350355
<template #item="{ element }">
351356
<nut-checkbox
352-
v-show="shouldShowElement(element[3])"
353357
:key="element[0]"
354358
:label="element[0]"
355359
text-position="left"
@@ -359,7 +363,7 @@
359363
<nut-avatar
360364
:class="{ 'sub-item-customer-icon': !element[4], 'icon': true }"
361365
v-if="element[2]"
362-
size="32"
366+
:size="chooserAvatarSize"
363367
:url="rewriteGithubUrl(element[2])"
364368
bg-color=""
365369
></nut-avatar>
@@ -564,12 +568,17 @@ const githubUrlRewriter = computed(() => {
564568
const rewriteGithubUrl = (url?: string | null) => {
565569
return githubUrlRewriter.value(url);
566570
};
571+
const chooserAvatarSize = computed(() => {
572+
return appearanceSetting.value.isSimpleMode ? "28" : "32";
573+
});
574+
575+
type SubSelectRow = [string, string, string | undefined, string[] | undefined, boolean];
567576
568577
const sub = computed(() => subsStore.getOneSub(configName));
569578
const collection = computed(() => subsStore.getOneCollection(configName));
570579
571580
572-
const subsSelectList = computed(() => {
581+
const subsSelectList = computed<SubSelectRow[]>(() => {
573582
return subsStore.subs.map(item => {
574583
return [
575584
item.name,
@@ -1197,73 +1206,151 @@ const urlValidator = (val: string): Promise<boolean> => {
11971206
if(tag.value === 'untagged') return !Array.isArray(element) || element.length === 0
11981207
return element.includes(tag.value)
11991208
};
1209+
const ensureSubscriptions = (): string[] => {
1210+
if (!Array.isArray(form.subscriptions)) {
1211+
form.subscriptions = [];
1212+
}
1213+
return form.subscriptions;
1214+
};
1215+
const hasSameSubscriptions = (left: string[], right: string[]) => {
1216+
return left.length === right.length && left.every((name, index) => name === right[index]);
1217+
};
1218+
const skipNextDisplayedListSync = ref(false);
1219+
const replaceSubscriptions = (
1220+
nextSubscriptions: string[],
1221+
options?: { preserveDisplayedOrder?: boolean },
1222+
) => {
1223+
const subscriptions = ensureSubscriptions();
1224+
1225+
if (hasSameSubscriptions(subscriptions, nextSubscriptions)) {
1226+
return;
1227+
}
1228+
1229+
if (options?.preserveDisplayedOrder) {
1230+
skipNextDisplayedListSync.value = true;
1231+
}
1232+
1233+
subscriptions.splice(0, subscriptions.length, ...nextSubscriptions);
1234+
};
1235+
const syncSubscriptionsFromRows = (rows: SubSelectRow[]) => {
1236+
const selectedSet = new Set(ensureSubscriptions());
1237+
const nextSubscriptions = rows
1238+
.filter(([name]) => selectedSet.has(name))
1239+
.map(([name]) => name);
1240+
1241+
replaceSubscriptions(nextSubscriptions, { preserveDisplayedOrder: true });
1242+
};
1243+
const selectedSubscriptions = computed(() => {
1244+
return Array.isArray(form.subscriptions) ? form.subscriptions : [];
1245+
});
1246+
const isRowVisibleByName = (name: string) => {
1247+
const row = subsSelectList.value.find((item) => item[0] === name);
1248+
return row ? shouldShowElement(row[3]) : false;
1249+
};
1250+
const orderedSubsSelectList = computed<SubSelectRow[]>(() => {
1251+
const selectedRows = selectedSubscriptions.value
1252+
.map(name => subsSelectList.value.find(item => item[0] === name))
1253+
.filter((item): item is SubSelectRow => Boolean(item));
1254+
const selectedSet = new Set(selectedRows.map(([name]) => name));
1255+
1256+
return [
1257+
...selectedRows,
1258+
...subsSelectList.value.filter(([name]) => !selectedSet.has(name)),
1259+
];
1260+
});
1261+
const getCurrentVisibleRows = () => {
1262+
if (displayedSubsSelectList.value.length > 0) {
1263+
return displayedSubsSelectList.value;
1264+
}
1265+
1266+
return orderedSubsSelectList.value.filter((item) => shouldShowElement(item[3]));
1267+
};
1268+
const visibleSelectedSubscriptions = computed<string[]>({
1269+
get: () => {
1270+
return selectedSubscriptions.value.filter((name) => isRowVisibleByName(name));
1271+
},
1272+
set: (visibleSelectedNames) => {
1273+
const subscriptions = ensureSubscriptions();
1274+
const visibleRowOrder = getCurrentVisibleRows().map(([name]) => name);
1275+
const visibleSet = new Set(visibleRowOrder);
1276+
const visibleSelectedSet = new Set(visibleSelectedNames);
1277+
const visibleSelectedRowOrder = visibleRowOrder.filter((name) => visibleSelectedSet.has(name));
1278+
const nextSubscriptions = subscriptions.filter((name) => {
1279+
return !visibleSet.has(name) || visibleSelectedSet.has(name);
1280+
});
1281+
const findExistingIndex = (name: string) => nextSubscriptions.indexOf(name);
1282+
1283+
visibleSelectedRowOrder.forEach((name, index) => {
1284+
if (findExistingIndex(name) > -1) return;
1285+
1286+
const nextVisibleAnchor = visibleSelectedRowOrder
1287+
.slice(index + 1)
1288+
.find((candidate) => findExistingIndex(candidate) > -1);
1289+
1290+
if (nextVisibleAnchor) {
1291+
nextSubscriptions.splice(findExistingIndex(nextVisibleAnchor), 0, name);
1292+
return;
1293+
}
1294+
1295+
const previousVisibleAnchor = visibleSelectedRowOrder
1296+
.slice(0, index)
1297+
.reverse()
1298+
.find((candidate) => findExistingIndex(candidate) > -1);
1299+
1300+
if (previousVisibleAnchor) {
1301+
nextSubscriptions.splice(findExistingIndex(previousVisibleAnchor) + 1, 0, name);
1302+
return;
1303+
}
1304+
1305+
nextSubscriptions.push(name);
1306+
});
1307+
1308+
replaceSubscriptions(nextSubscriptions, { preserveDisplayedOrder: true });
1309+
},
1310+
});
1311+
const mergeVisibleOrder = <T>(
1312+
source: T[],
1313+
reorderedVisibleItems: T[],
1314+
shouldInclude: (item: T) => boolean,
1315+
) => {
1316+
let visibleIndex = 0;
1317+
1318+
return source.map((item) => {
1319+
if (!shouldInclude(item)) {
1320+
return item;
1321+
}
1322+
1323+
const reorderedItem = reorderedVisibleItems[visibleIndex];
1324+
visibleIndex += 1;
1325+
return reorderedItem ?? item;
1326+
});
1327+
};
12001328
const subCheckboxIndeterminate = ref(true);
12011329
const subCheckbox = ref(true);
12021330
// const subCheckboxChange = (v) => {
12031331
// console.log(`${!v} -> ${v}`)
12041332
// };
12051333
const subCheckboxClick = () => {
1206-
// 确保 form.subscriptions 存在
1207-
if (!form.subscriptions) {
1208-
form.subscriptions = [];
1209-
}
1210-
// const selected = toRaw(form.subscriptions) || []
1211-
const group = subsSelectList.value.filter(item => shouldShowElement(item[3])).map(item => item[0]) || []
1334+
const group = getCurrentVisibleRows().map(([name]) => name);
12121335
if (subCheckboxIndeterminate.value) {
12131336
console.log(`半选, 应变为全选`)
1214-
for (let i = 0; i < group.length; i++) {
1215-
const index = form.subscriptions.indexOf(group[i])
1216-
if (index === -1) {
1217-
form.subscriptions.push(group[i])
1218-
}
1219-
}
1337+
visibleSelectedSubscriptions.value = group;
12201338
} else if (!subCheckbox.value) {
12211339
console.log(`全选, 应变为不选`)
1222-
// 用遍历与 form.subscriptions.slice 的方式, 去掉 form.subscriptions 中所有被 group 包含的元素
1223-
for (let i = 0; i < group.length; i++) {
1224-
const index = form.subscriptions.indexOf(group[i])
1225-
if (index > -1) {
1226-
form.subscriptions.splice(index, 1)
1227-
}
1228-
}
1340+
visibleSelectedSubscriptions.value = [];
12291341
// subCheckbox.value = !subCheckbox.value
12301342
} else {
12311343
console.log(`不选, 应变为全选`)
1232-
for (let i = 0; i < group.length; i++) {
1233-
const index = form.subscriptions.indexOf(group[i])
1234-
if (index === -1) {
1235-
form.subscriptions.push(group[i])
1236-
}
1237-
}
1344+
visibleSelectedSubscriptions.value = group;
12381345
// subCheckbox.value = !subCheckbox.value
12391346
}
12401347
subCheckboxIndeterminate.value = false
12411348
};
1242-
const filteredSubsSelectList = ref([]);
1243-
1244-
const updateFilteredSubsList = () => {
1245-
if (!subsSelectList.value?.length) {
1246-
filteredSubsSelectList.value = [];
1247-
return;
1248-
}
1249-
1250-
form.subscriptions = form.subscriptions || [];
1251-
1252-
// 当选中"全部"标签时,将已选中的订阅提到最前面;否则保持原顺序
1253-
filteredSubsSelectList.value = tag.value === 'all'
1254-
? [
1255-
// 已选中的(按选中顺序)
1256-
...form.subscriptions.map(name => subsSelectList.value.find(item => item[0] === name)).filter(Boolean),
1257-
// 未选中的
1258-
...subsSelectList.value.filter(item => !form.subscriptions.includes(item[0]))
1259-
]
1260-
: [...subsSelectList.value];
1261-
};
1262-
// 监听 tag、subsSelectList 和 subsStore.subs 的变化时更新列表
1263-
watch([tag, subsSelectList, () => subsStore.subs], () => {
1264-
updateFilteredSubsList();
1265-
}, { immediate: true, deep: true });
1349+
const displayedSubsSelectList = ref<SubSelectRow[]>([]);
12661350
const isDragging = ref(false);
1351+
const syncDisplayedSubsSelectList = () => {
1352+
displayedSubsSelectList.value = orderedSubsSelectList.value.filter((item) => shouldShowElement(item[3]));
1353+
};
12671354
12681355
const onStartDrag = () => {
12691356
console.log("开始拖拽");
@@ -1272,38 +1359,29 @@ const urlValidator = (val: string): Promise<boolean> => {
12721359
12731360
const onEndDrag = () => {
12741361
console.log("结束拖拽");
1362+
const mergedRows = mergeVisibleOrder(
1363+
orderedSubsSelectList.value,
1364+
displayedSubsSelectList.value,
1365+
(item) => shouldShowElement(item[3]),
1366+
);
12751367
isDragging.value = false;
1276-
1277-
// 获取当前过滤视图中可见的订阅顺序
1278-
const visibleOrder = filteredSubsSelectList.value
1279-
.filter(item => shouldShowElement(item[3]))
1280-
.map(item => item[0]);
1281-
1282-
const newSubscriptions = [];
1283-
1284-
// 确保 form.subscriptions 存在
1285-
if (!form.subscriptions) {
1286-
form.subscriptions = [];
1287-
}
1288-
1289-
// 先按新顺序添加当前过滤列表中已选中的订阅
1290-
visibleOrder.forEach(name => {
1291-
if (form.subscriptions.includes(name)) {
1292-
newSubscriptions.push(name);
1293-
}
1294-
});
1295-
1296-
// 添加不在当前过滤列表中但已选中的订阅(保持原有顺序)
1297-
form.subscriptions.forEach(name => {
1298-
if (!visibleOrder.includes(name)) {
1299-
newSubscriptions.push(name);
1300-
}
1301-
});
1302-
form.subscriptions.splice(0, form.subscriptions.length, ...newSubscriptions);
1303-
console.log("更新后的 form.subscriptions:", form.subscriptions);
1368+
syncSubscriptionsFromRows(mergedRows);
1369+
syncDisplayedSubsSelectList();
13041370
};
1305-
watch([tag, form.subscriptions, subsSelectList], () => {
1306-
const selected = toRaw(form.subscriptions || []) || []
1371+
watch([tag, subsSelectList], () => {
1372+
if (isDragging.value) return;
1373+
syncDisplayedSubsSelectList();
1374+
}, { immediate: true });
1375+
watch(selectedSubscriptions, () => {
1376+
if (isDragging.value) return;
1377+
if (skipNextDisplayedListSync.value) {
1378+
skipNextDisplayedListSync.value = false;
1379+
return;
1380+
}
1381+
syncDisplayedSubsSelectList();
1382+
}, { deep: true });
1383+
watch([tag, selectedSubscriptions, subsSelectList], () => {
1384+
const selected = toRaw(selectedSubscriptions.value) || []
13071385
const group = subsSelectList.value.filter(item => shouldShowElement(item[3])).map(item => item[0]) || []
13081386
// 1. group 中不包含 selected 中的任何元素, subCheckbox 为 false, subCheckboxIndeterminate 为 false
13091387
// 2. group 中包含 selected 中的任意元素, subCheckbox 为 true, subCheckboxIndeterminate 为 true
@@ -1321,7 +1399,7 @@ const urlValidator = (val: string): Promise<boolean> => {
13211399
subCheckbox.value = false
13221400
subCheckboxIndeterminate.value = false
13231401
}
1324-
}, { immediate: true });
1402+
}, { immediate: true, deep: true });
13251403
// const subCheckboxIndeterminate = computed(() => {
13261404
// const selected = toRaw(form.subscriptions)
13271405
// const currentGroup = subsSelectList.value.filter(item => shouldShowElement(item[3])).map(item => item[0])
@@ -1521,6 +1599,54 @@ const handleEditGlobalClick = () => {
15211599
.subs-checkbox-wrapper {
15221600
flex-direction: row-reverse;
15231601
1602+
&.is-dragging {
1603+
.subs-checkbox,
1604+
.sub-img-wrapper,
1605+
.sub-img-wrapper * {
1606+
-webkit-user-select: none;
1607+
user-select: none;
1608+
}
1609+
}
1610+
1611+
&.is-simple-mode {
1612+
.subs-checkbox {
1613+
padding: 10px 0 0 0;
1614+
1615+
&:not(:last-child) {
1616+
padding: 10px 0;
1617+
}
1618+
1619+
.sub-img-wrapper {
1620+
font-size: 13px;
1621+
1622+
.icon {
1623+
margin-right: 6px;
1624+
}
1625+
1626+
.sub-item {
1627+
margin: -3px 0 0 -3px;
1628+
1629+
.name {
1630+
margin: 3px 0 0 3px;
1631+
}
1632+
1633+
.tag {
1634+
margin: 3px 0 0 3px;
1635+
}
1636+
}
1637+
1638+
.sub-item-customer-icon {
1639+
margin-right: 8px;
1640+
}
1641+
1642+
.drag-handle {
1643+
font-size: 14px;
1644+
padding: 6px;
1645+
}
1646+
}
1647+
}
1648+
}
1649+
15241650
.subs-checkbox {
15251651
justify-content: space-between;
15261652
// margin-left: 16px;

0 commit comments

Comments
 (0)