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"
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"
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(() => {
564568const 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