yumi-admin/apps/src/views/resident-activity/components/resource-select-drawer.vue

443 lines
9.6 KiB
Vue

<script lang="ts" setup>
import { computed, reactive, ref, watch } from 'vue';
import { listBadgePictureBySysOrigin } from '#/api/legacy/badge';
import { listGiftBySysOrigin } from '#/api/legacy/gift';
import {
listNotFamilyBySysOriginType,
pagePropsSource,
} from '#/api/legacy/props';
import {
Button,
Drawer,
Empty,
Input,
Pagination,
Select,
Space,
Spin,
} from 'antdv-next';
defineOptions({ name: 'ResidentActivityResourceSelectDrawer' });
type ResourceItem = {
amount?: number | string;
cover: string;
id: number | string;
name: string;
sourceUrl: string;
type: string;
};
const props = withDefaults(
defineProps<{
defaultType?: string;
fixedType?: boolean;
open: boolean;
sysOrigin: string;
title?: string;
}>(),
{
defaultType: 'ALL_PROPS',
fixedType: false,
title: '选择资源',
},
);
const emit = defineEmits<{
close: [];
select: [item: ResourceItem];
}>();
const typeOptions = [
{ label: '全部资源', value: 'ALL_PROPS' },
{ label: '自定义', value: 'CUSTOMIZE' },
{ label: '头像框', value: 'AVATAR_FRAME' },
{ label: '座驾', value: 'RIDE' },
{ label: '聊天气泡', value: 'CHAT_BUBBLE' },
{ label: '贵族 VIP', value: 'NOBLE_VIP' },
{ label: '主题背景', value: 'THEME' },
{ label: '飘窗', value: 'FLOAT_PICTURE' },
{ label: '礼物', value: 'GIFT' },
{ label: '徽章', value: 'BADGE' },
].map((item) => ({ ...item, value: item.value as any }));
const listOnlyTypes = new Set(['BADGE', 'GIFT', 'NOBLE_VIP']);
const loading = ref(false);
const list = ref<ResourceItem[]>([]);
const total = ref(0);
const query = reactive({
cursor: 1,
keyword: '',
limit: 24,
type: 'ALL_PROPS',
});
const localRows = ref<ResourceItem[]>([]);
const filteredLocalRows = computed(() => {
const keyword = query.keyword.trim().toLowerCase();
if (!keyword) {
return localRows.value;
}
return localRows.value.filter(
(item) =>
String(item.id).toLowerCase().includes(keyword) ||
item.name.toLowerCase().includes(keyword),
);
});
function firstPresent(record: Record<string, any>, keys: string[]) {
for (const key of keys) {
const value = record?.[key];
if (value !== undefined && value !== null && String(value).trim() !== '') {
return value;
}
}
return '';
}
function normalizeResource(
item: Record<string, any>,
type: string,
options: {
coverKeys?: string[];
idKeys?: string[];
nameKeys?: string[];
sourceUrlKeys?: string[];
} = {},
): ResourceItem {
const id = firstPresent(
item,
options.idKeys || ['id', 'resourceId', 'sourceId', 'propsId'],
);
return {
amount: firstPresent(item, ['amount', 'giftCandy', 'price']),
cover: String(
firstPresent(
item,
options.coverKeys || [
'cover',
'coverUrl',
'resourceCover',
'resourceCoverUrl',
'giftPhoto',
'selectUrl',
'icon',
],
),
),
id: id as number | string,
name: String(
firstPresent(
item,
options.nameKeys || [
'name',
'resourceName',
'sourceName',
'giftName',
'badgeName',
'title',
'remark',
],
) || (id ? `资源 ${id}` : ''),
),
sourceUrl: String(
firstPresent(
item,
options.sourceUrlKeys || [
'sourceUrl',
'resourceUrl',
'url',
'giftSourceUrl',
'animationUrl',
],
),
),
type,
};
}
function applyLocalPage() {
const rows = filteredLocalRows.value;
total.value = rows.length;
const start = (query.cursor - 1) * query.limit;
list.value = rows.slice(start, start + query.limit);
}
async function loadListType(type: string) {
if (type === 'GIFT') {
const result = await listGiftBySysOrigin(props.sysOrigin);
localRows.value = (result || []).map((item) =>
normalizeResource(item, type, {
coverKeys: ['giftPhoto', 'cover'],
idKeys: ['id', 'giftId'],
nameKeys: ['giftName', 'name'],
sourceUrlKeys: ['giftSourceUrl', 'sourceUrl'],
}),
);
applyLocalPage();
return;
}
if (type === 'BADGE') {
const result = await listBadgePictureBySysOrigin(props.sysOrigin, 'ACTIVITY');
localRows.value = (result || []).map((item) =>
normalizeResource(item, type, {
coverKeys: ['selectUrl', 'cover'],
idKeys: ['badgeConfigId', 'id'],
nameKeys: ['badgeName', 'name'],
sourceUrlKeys: ['animationUrl', 'sourceUrl'],
}),
);
applyLocalPage();
return;
}
if (type === 'NOBLE_VIP') {
const result = await listNotFamilyBySysOriginType(props.sysOrigin, type);
localRows.value = (result || []).map((item) =>
normalizeResource(item, type),
);
applyLocalPage();
}
}
async function loadData(reset = false) {
if (!props.open || !props.sysOrigin) {
return;
}
if (reset) {
query.cursor = 1;
}
loading.value = true;
try {
if (listOnlyTypes.has(query.type)) {
await loadListType(query.type);
return;
}
const keyword = query.keyword.trim();
const params: Record<string, any> = {
cursor: query.cursor,
del: false,
limit: query.limit,
sysOrigin: props.sysOrigin,
};
if (query.type && query.type !== 'ALL_PROPS') {
params.type = query.type;
}
if (keyword) {
if (/^\d+$/.test(keyword)) {
params.id = keyword;
} else {
params.name = keyword;
}
}
const result = await pagePropsSource(params);
list.value = (result.records || []).map((item) =>
normalizeResource(item, String(item.type || query.type || '')),
);
total.value = result.total || 0;
} finally {
loading.value = false;
}
}
function handleTypeChange(value: string) {
query.type = value || 'ALL_PROPS';
query.keyword = '';
localRows.value = [];
void loadData(true);
}
function handleSearch() {
void loadData(true);
}
function handlePageChange(page: number, pageSize: number) {
query.cursor = page;
query.limit = pageSize;
if (listOnlyTypes.has(query.type)) {
applyLocalPage();
return;
}
void loadData();
}
function handleSelect(item: ResourceItem) {
emit('select', item);
}
watch(
() => props.open,
(open) => {
if (!open) {
return;
}
query.type = props.defaultType || 'ALL_PROPS';
query.keyword = '';
query.cursor = 1;
localRows.value = [];
void loadData(true);
},
{ immediate: true },
);
watch(
() => props.defaultType,
(value) => {
if (!props.open || !value) {
return;
}
query.type = value;
query.keyword = '';
localRows.value = [];
void loadData(true);
},
);
watch(
() => props.sysOrigin,
() => {
localRows.value = [];
if (props.open) {
void loadData(true);
}
},
);
</script>
<template>
<Drawer
:open="open"
destroy-on-close
:title="title"
width="920"
@close="emit('close')"
>
<Space class="toolbar" wrap>
<Select
v-if="!fixedType"
v-model:value="query.type"
:options="typeOptions"
option-label-prop="label"
style="width: 180px"
@change="handleTypeChange"
/>
<Input
v-model:value="query.keyword"
allow-clear
placeholder="资源名称 / ID"
style="width: 260px"
@press-enter="handleSearch"
/>
<Button :loading="loading" type="primary" @click="handleSearch">
搜索
</Button>
</Space>
<Spin :spinning="loading">
<div v-if="list.length > 0" class="resource-grid">
<button
v-for="item in list"
:key="`${item.type}-${item.id}`"
class="resource-card"
type="button"
@click="handleSelect(item)"
>
<div class="resource-cover-wrap">
<img
:src="item.cover || 'https://dummyimage.com/88x88/e2e8f0/64748b&text=+'"
alt=""
class="resource-cover"
/>
</div>
<div class="resource-name">{{ item.name || '-' }}</div>
<div class="resource-meta">ID {{ item.id }}</div>
<div class="resource-type">{{ item.type || '-' }}</div>
</button>
</div>
<Empty v-else description="暂无可选资源" />
</Spin>
<div class="pager">
<Pagination
:current="query.cursor"
:page-size="query.limit"
:total="total"
show-size-changer
@change="handlePageChange"
@showSizeChange="handlePageChange"
/>
</div>
</Drawer>
</template>
<style scoped>
.toolbar {
margin-bottom: 16px;
}
.resource-grid {
display: grid;
gap: 16px;
grid-template-columns: repeat(auto-fill, minmax(132px, 1fr));
}
.resource-card {
background: #fff;
border: 1px solid #e2e8f0;
border-radius: 8px;
cursor: pointer;
padding: 12px;
text-align: center;
transition:
border-color 0.2s ease,
box-shadow 0.2s ease,
transform 0.2s ease;
}
.resource-card:hover {
border-color: #2563eb;
box-shadow: 0 12px 24px rgb(37 99 235 / 12%);
transform: translateY(-2px);
}
.resource-cover-wrap {
align-items: center;
display: flex;
height: 88px;
justify-content: center;
margin-bottom: 10px;
}
.resource-cover {
border-radius: 8px;
height: 88px;
object-fit: cover;
width: 88px;
}
.resource-name {
font-weight: 600;
line-height: 1.4;
min-height: 40px;
word-break: break-all;
}
.resource-meta,
.resource-type {
color: #64748b;
font-size: 12px;
margin-top: 6px;
word-break: break-all;
}
.pager {
display: flex;
justify-content: flex-end;
margin-top: 16px;
}
</style>