重构角色筛选组件

This commit is contained in:
BTMuli
2025-12-28 22:32:47 +08:00
parent aa74818c47
commit bfab4a6ac6
3 changed files with 310 additions and 88 deletions

View File

@@ -1,43 +1,44 @@
<!-- 选项组件 -->
<template>
<div class="uav-select-chips-box">
<v-chip-group :model-value="result" multiple>
<!-- ALL -->
<v-chip
:size="props.size"
:value="'__all__'"
class="uav-scb-all"
filter
title="全部"
variant="elevated"
@click="toggleAll()"
>
<template #filter>
<!-- ALL -->
<v-chip
key="all"
:size="props.size"
class="uav-scb-all"
title="全部"
variant="elevated"
@click.stop="toggleAll"
>
<template #prepend>
<div :class="{ active: isAllSelected }" class="icon-wrap">
<v-icon color="var(--tgc-od-green)">mdi-check</v-icon>
</template>
<slot :selected="selectAll" name="all">All</slot>
</v-chip>
</div>
</template>
<div :class="{ shifted: isAllSelected }" class="all-label">
<slot :selected="isAllSelected" name="all">All</slot>
</div>
</v-chip>
<v-chip-group v-model="result" filter multiple>
<!-- Options -->
<v-chip
v-for="(item, idx) in props.items"
:key="idx"
v-for="item in props.items"
:key="item.value"
:size="props.size"
:title="getItemTitle(item)"
:value="getItemValue(item)"
:title="item.title"
:value="item.value"
class="uav-scb-item"
filter
variant="elevated"
@click="selectItem(item)"
>
<template #filter>
<v-icon color="var(--tgc-od-blue)">mdi-check</v-icon>
</template>
<slot :selected="result.includes(getItemValue(item))" name="item">
<template v-if="typeof item === 'string'">{{ item }}</template>
<template v-else>
<slot :selected="result.includes(item.value)" name="item">
<div class="uav-scb-inner">
<TMiImg v-if="item.icon" :src="item.icon" alt="icon" />
<span>{{ item.label }}</span>
</template>
</div>
</slot>
</v-chip>
</v-chip-group>
@@ -46,25 +47,36 @@
<script lang="ts" setup>
import TMiImg from "@comp/app/t-mi-img.vue";
import { computed, defineModel, defineProps, shallowRef, triggerRef, watch } from "vue";
import { computed, defineProps, ref, watch } from "vue";
export type UavSelectChipsItem = {
label: string;
/** 渲染文本 */
label?: string;
/** 渲染图标 */
icon?: string;
title?: string;
/** 提示文本 */
title: string;
/** 选项值 */
value: string;
};
type UavSelectChipsProps = {
/** 选中 */
selected: Array<string>;
/** 选项 */
items: Array<string | UavSelectChipsItem>;
items: Array<UavSelectChipsItem>;
/** 尺寸 */
size?: "x-small" | "small" | "default" | "large" | "x-large" | number;
};
type UavSelectChipsEmits = (e: "chip-select", v: Array<string>) => void;
const props = withDefaults(defineProps<UavSelectChipsProps>(), { size: "default" });
const result = shallowRef<Array<string>>([]);
const model = defineModel<Array<string>>({ default: [] });
const selectAll = computed<boolean>(() => result.value.includes("__all__"));
const emits = defineEmits<UavSelectChipsEmits>();
const result = ref<Array<string>>(props.selected);
const isAllSelected = computed<boolean>(() => {
if (!props.items || props.items.length === 0) return false;
return props.items.every((i) => result.value.includes(i.value.toString()));
});
const iconHeight = computed<string>(() => {
switch (props.size) {
case "x-small":
@@ -82,41 +94,22 @@ const iconHeight = computed<string>(() => {
}
});
watch(
() => props.selected,
(v) => (result.value = [...v]),
{ deep: true },
);
watch(
() => result.value,
() => {
model.value = result.value.filter((i) => i !== "__all__");
},
() => emits("chip-select", result.value),
{ deep: true },
);
async function toggleAll(): Promise<void> {
if (result.value.includes("__all__")) {
if (isAllSelected.value) {
result.value = [];
} else {
result.value = ["__all__", ...props.items.map(getItemValue)];
}
}
function getItemValue(item: UavSelectChipsItem | string): string {
if (typeof item === "string") return item;
return item.value;
}
function getItemTitle(item: UavSelectChipsItem | string): string | undefined {
if (typeof item === "string") return item;
return item.title;
}
function selectItem(item: UavSelectChipsItem | string): void {
const itemValue = getItemValue(item);
if (result.value.includes(itemValue)) {
result.value = result.value.filter((i) => i !== "__all__" && i !== itemValue);
} else {
result.value.push(itemValue);
if (result.value.length === props.items.length) {
result.value.push("__all__");
}
triggerRef(result);
result.value = props.items.map((i) => i.value);
}
}
</script>
@@ -133,12 +126,53 @@ function selectItem(item: UavSelectChipsItem | string): void {
.uav-scb-all {
@include github-styles.github-tag-dark-gen(#41b883);
--icon-size: 16px;
--icon-gap: 2px;
display: inline-flex;
align-items: center;
.icon-wrap {
display: inline-flex;
overflow: hidden;
width: 0;
align-items: center;
justify-content: center;
margin-right: 0;
opacity: 0;
transition:
width 0.3s ease-in-out,
opacity 0.3s ease-in-out,
margin-right 0.3s ease-in-out;
&.active {
width: var(--icon-size);
margin-right: var(--icon-gap);
opacity: 1;
}
}
.all-label {
display: inline-block;
transform: translateX(0);
transition: transform var(--anim-duration) var(--anim-ease);
will-change: transform;
}
}
.uav-scb-item {
@include github-styles.github-tag-dark-gen(#548af7);
position: relative;
}
.uav-scb-inner {
position: relative;
display: flex;
align-items: center;
justify-content: center;
column-gap: 2px;
img {
position: relative;

View File

@@ -0,0 +1,191 @@
<!-- 角色筛选组件 -->
<template>
<v-bottom-sheet v-model="visible">
<div class="uav-select-container">
<div class="uav-select-item">
<div class="uav-select-title">星级</div>
<div class="uav-select-props">
<UavSelectChips
:items="starOpts"
:selected="model.star"
size="small"
@chipSelect="handleStarSelect"
/>
</div>
</div>
<div class="uav-select-item">
<div class="uav-select-title">武器</div>
<div class="uav-select-props">
<UavSelectChips
:items="weaponOpts"
:selected="model.weapon"
size="small"
@chipSelect="handleWeaponSelect"
/>
</div>
</div>
<div class="uav-select-item">
<div class="uav-select-title">元素</div>
<div class="uav-select-props">
<UavSelectChips
:items="elementOpts"
:selected="model.element"
size="small"
@chipSelect="handleElementSelect"
/>
</div>
</div>
<div class="uav-select-item">
<div class="uav-select-title">地区</div>
<div class="uav-select-props">
<UavSelectChips
:items="areaOpts"
:selected="model.area"
size="small"
@chipSelect="handleAreaSelect"
/>
</div>
</div>
<div class="uav-select-acts">
<v-btn class="uav-act-btn" prepend-icon="mdi-check" variant="elevated" @click="onConfirm()">
确定
</v-btn>
<v-btn class="uav-act-btn" prepend-icon="mdi-cancel" variant="elevated" @click="onCancel()">
取消
</v-btn>
</div>
</div>
</v-bottom-sheet>
</template>
<script lang="ts" setup>
import UavSelectChips, { type UavSelectChipsItem } from "@comp/userAvatar/uav-select-chips.vue";
import { ref } from "vue";
/** 返回数据 */
export type UavSelectModel = {
/** 星级 */
star: Array<string>;
/** 武器 */
weapon: Array<string>;
/** 元素 */
element: Array<string>;
/** 地区 */
area: Array<string>;
};
type UavSelectEmits = (e: "select", v: UavSelectModel) => void;
const starOpts: Array<UavSelectChipsItem> = [
{ label: "⭐⭐⭐⭐", value: "4", title: "四星" },
{ label: "⭐⭐⭐⭐⭐", value: "5", title: "五星" },
];
const weaponOpts: Array<UavSelectChipsItem> = ["单手剑", "双手剑", "弓", "法器", "长柄武器"].map(
(i) => ({ label: i, value: i, title: i, icon: `/icon/weapon/${i}.webp` }),
);
const elementOpts: Array<UavSelectChipsItem> = ["冰", "岩", "水", "火", "草", "雷", "风"].map(
(i) => ({ label: i, value: i, title: `${i}元素`, icon: `/icon/element/${i}元素.webp` }),
);
const areaOpts: Array<UavSelectChipsItem> = [
"未知",
"蒙德",
"璃月",
"主角",
"愚人众",
"稻妻",
"游侠",
"须弥",
"枫丹",
"纳塔",
"至冬",
"寰宇劫灭",
"挪德卡莱",
].map((i) => ({ label: i, value: i, title: i }));
const emits = defineEmits<UavSelectEmits>();
const starSelected = ref<Array<string>>([]);
const weaponSelected = ref<Array<string>>([]);
const elementSelected = ref<Array<string>>([]);
const areaSelected = ref<Array<string>>([]);
const model = defineModel<UavSelectModel>({
default: { star: [], weapon: [], area: [], element: [] },
});
const visible = defineModel<boolean>("show");
function onCancel(): void {
visible.value = false;
}
async function onConfirm(): Promise<void> {
model.value = {
star: starSelected.value,
weapon: weaponSelected.value,
element: elementSelected.value,
area: areaSelected.value,
};
// await nextTick();
emits("select", model.value);
visible.value = false;
}
function handleStarSelect(e: Array<string>): void {
console.log("starSelect", e);
starSelected.value = e;
}
function handleWeaponSelect(e: Array<string>): void {
console.log("weaponSelect", e);
weaponSelected.value = e;
}
function handleElementSelect(e: Array<string>): void {
console.log("elementSelect", e);
elementSelected.value = e;
}
function handleAreaSelect(e: Array<string>): void {
console.log("areaSelect", e);
areaSelected.value = e;
}
</script>
<style lang="scss" scoped>
.uav-select-container {
position: absolute;
z-index: 1;
bottom: 0;
left: 0;
display: flex;
width: 100%;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 16px;
border-radius: 4px;
background: var(--app-page-bg);
}
.uav-select-item {
position: relative;
display: flex;
width: 100%;
align-items: center;
justify-content: flex-start;
color: var(--common-text-title);
column-gap: 8px;
}
.uav-select-acts {
position: relative;
display: flex;
align-items: center;
margin-top: 8px;
column-gap: 8px;
}
.uav-act-btn {
background: var(--tgc-btn-1);
color: var(--btn-text);
font-family: var(--font-text);
}
</style>

View File

@@ -6,7 +6,7 @@
<img alt="icon" src="/source/UI/userAvatar.webp" />
<span>我的角色</span>
<v-btn class="uc-top-btn" variant="elevated" @click="showSelect = true">筛选角色</v-btn>
<v-btn class="uc-top-btn" variant="elevated" @click="resetSelect = true">重置筛选</v-btn>
<v-btn class="uc-top-btn" variant="elevated" @click="resetList()">重置筛选</v-btn>
</div>
</template>
<template #append>
@@ -138,15 +138,16 @@
@to-next="handleSwitch"
@to-avatar="selectRole"
/>
<TwoSelectC v-model="showSelect" v-model:reset="resetSelect" @select-c="handleSelect" />
<!-- <TwoSelectC v-model="showSelect" v-model:reset="resetSelect" @select-c="handleSelect" />-->
<UavSelect v-model:show="showSelect" :model-value="selectOpts" @select="handleSelect" />
</template>
<script lang="ts" setup>
import showDialog from "@comp/func/dialog.js";
import showLoading from "@comp/func/loading.js";
import showSnackbar from "@comp/func/snackbar.js";
import TwoSelectC, { type SelectedCValue } from "@comp/pageWiki/two-select-c.vue";
import TuaAvatarBox from "@comp/userAvatar/tua-avatar-box.vue";
import TuaDetailOverlay from "@comp/userAvatar/tua-detail-overlay.vue";
import UavSelect, { type UavSelectModel } from "@comp/userAvatar/uav-select.vue";
import recordReq from "@req/recordReq.js";
import TSUserAvatar from "@Sqlm/userAvatar.js";
import useAppStore from "@store/app.js";
@@ -181,7 +182,6 @@ const showOverlay = ref<boolean>(false);
const selectIndex = ref<number>(0);
const showSelect = ref<boolean>(false);
const showMode = ref<"classic" | "card" | "dev">("dev");
const resetSelect = ref<boolean>(false);
const uidCur = ref<string>();
// 排序
@@ -192,6 +192,7 @@ const isConstUp = ref<boolean | null>(null);
const uidList = shallowRef<Array<string>>([]);
const roleOverview = shallowRef<Array<OverviewItem>>([]);
const roleList = shallowRef<Array<TGApp.Sqlite.Character.TableTrans>>([]);
const selectOpts = shallowRef<UavSelectModel>({ star: [], weapon: [], area: [], element: [] });
const selectedList = shallowRef<Array<TGApp.Sqlite.Character.TableTrans>>([]);
const dataVal = shallowRef<TGApp.Sqlite.Character.TableTrans>();
const enableShare = computed<boolean>(
@@ -209,25 +210,6 @@ onMounted(async () => {
await showLoading.end();
});
watch(
() => resetSelect.value,
() => {
if (resetSelect.value) {
selectedList.value = getOrderedList(roleList.value);
showSnackbar.success("已重置筛选条件");
if (!dataVal.value) return;
selectIndex.value = selectedList.value.indexOf(dataVal.value);
if (selectIndex.value === -1) {
dataVal.value = selectedList.value[0];
selectIndex.value = 0;
}
isLevelUp.value = null;
isFetterUp.value = null;
isConstUp.value = null;
}
},
);
watch(
() => showMode.value,
() => {
@@ -307,6 +289,21 @@ function getSortDesc(value: boolean | null): string {
}
}
function resetList(): void {
selectedList.value = getOrderedList(roleList.value);
showSnackbar.success("已重置筛选条件");
if (!dataVal.value) return;
selectIndex.value = selectedList.value.indexOf(dataVal.value);
if (selectIndex.value === -1) {
dataVal.value = selectedList.value[0];
selectIndex.value = 0;
}
isLevelUp.value = null;
isFetterUp.value = null;
isConstUp.value = null;
selectOpts.value = { star: [], weapon: [], area: [], element: [] };
}
function getOrderedList(
data: Array<TGApp.Sqlite.Character.TableTrans>,
): Array<TGApp.Sqlite.Character.TableTrans> {
@@ -502,13 +499,13 @@ function selectRole(role: TGApp.Sqlite.Character.TableTrans): void {
if (!showOverlay.value) showOverlay.value = true;
}
function handleSelect(val: SelectedCValue) {
showSelect.value = false;
function handleSelect(val: UavSelectModel): void {
selectOpts.value = val;
const filterC = AppCharacterData.filter((avatar) => {
if (!val.star.includes(avatar.star)) return false;
if (!val.weapon.includes(avatar.weapon)) return false;
if (!val.elements.includes(avatar.element)) return false;
if (!val.area.includes(avatar.area)) return false;
if (val.star.length > 0 && !val.star.includes(avatar.star.toString())) return false;
if (val.weapon.length > 0 && !val.weapon.includes(avatar.weapon)) return false;
if (val.element.length > 0 && !val.element.includes(avatar.element)) return false;
if (val.area.length > 0 && !val.area.includes(avatar.area)) return false;
return roleList.value.find(
(role) =>
role.avatar.id === avatar.id && getZhElement(role.avatar.element) === avatar.element,