468 lines
13 KiB
Vue
468 lines
13 KiB
Vue
|
|
<template>
|
||
|
|
<BasicListLayout
|
||
|
|
:show-nav-bar="true"
|
||
|
|
:nav-bar-props="{ title: '教学资源' }"
|
||
|
|
@register="register"
|
||
|
|
>
|
||
|
|
<template #top>
|
||
|
|
<view class="search-section">
|
||
|
|
<view class="search-box">
|
||
|
|
<uni-icons type="search" size="18" color="#999"></uni-icons>
|
||
|
|
<input
|
||
|
|
class="search-input"
|
||
|
|
type="text"
|
||
|
|
placeholder="搜索教学资源..."
|
||
|
|
v-model="searchKeyword"
|
||
|
|
@input="onSearchInput"
|
||
|
|
@confirm="onSearchConfirm"
|
||
|
|
/>
|
||
|
|
<view class="search-clear" v-if="searchKeyword" @click="clearSearch">
|
||
|
|
<uni-icons type="clear" size="16" color="#999"></uni-icons>
|
||
|
|
</view>
|
||
|
|
</view>
|
||
|
|
</view>
|
||
|
|
</template>
|
||
|
|
|
||
|
|
<template #default="{ list }">
|
||
|
|
<view class="list-container">
|
||
|
|
<!-- Add @click handler to navigate -->
|
||
|
|
<view class="resource-item" v-for="item in list" :key="item.id" @click="goToDetail(item.id)">
|
||
|
|
<view class="item-icon-container">
|
||
|
|
<view class="item-icon">{{ item.iconLetter }}</view>
|
||
|
|
<text class="item-pages">-{{ item.pages }}页-</text>
|
||
|
|
</view>
|
||
|
|
<view class="item-details">
|
||
|
|
<text class="item-title">{{ item.title }}</text>
|
||
|
|
<!-- 标签区域 -->
|
||
|
|
<view class="tags-container" v-if="item.tags && item.tags.length > 0">
|
||
|
|
<view class="tag-item" v-for="tag in item.tags" :key="tag">{{ tag }}</view>
|
||
|
|
</view>
|
||
|
|
<view class="item-meta">
|
||
|
|
<text class="meta-text">{{ item.publishDate }}</text>
|
||
|
|
<text class="meta-text">浏览量: {{ item.views }}</text>
|
||
|
|
<text class="meta-text">下载量: {{ item.downloads }}</text>
|
||
|
|
</view>
|
||
|
|
</view>
|
||
|
|
</view>
|
||
|
|
</view>
|
||
|
|
</template>
|
||
|
|
|
||
|
|
</BasicListLayout>
|
||
|
|
|
||
|
|
<!-- Filter Popup -->
|
||
|
|
<uni-popup ref="filterPopupRef" type="bottom" background-color="#fff">
|
||
|
|
<view class="popup-content">
|
||
|
|
<view class="popup-header">
|
||
|
|
<text>{{ currentFilterTitle }}</text>
|
||
|
|
<uni-icons type="closeempty" size="20" @click="closeFilterPopup"></uni-icons>
|
||
|
|
</view>
|
||
|
|
<scroll-view scroll-y class="popup-options">
|
||
|
|
<view
|
||
|
|
class="option-item"
|
||
|
|
v-for="option in currentFilterOptions"
|
||
|
|
:key="option.id ?? 'null'"
|
||
|
|
:class="{ active: isOptionSelected(option) }"
|
||
|
|
@click="selectFilterOption(option)"
|
||
|
|
>
|
||
|
|
{{ option.name }}
|
||
|
|
</view>
|
||
|
|
</scroll-view>
|
||
|
|
</view>
|
||
|
|
</uni-popup>
|
||
|
|
|
||
|
|
</template>
|
||
|
|
|
||
|
|
<script lang="ts" setup>
|
||
|
|
import { ref, computed } from "vue";
|
||
|
|
import { useLayout } from "@/components/BasicListLayout/hooks/useLayout";
|
||
|
|
|
||
|
|
interface Resource {
|
||
|
|
id: number;
|
||
|
|
iconLetter: string;
|
||
|
|
title: string;
|
||
|
|
pages: number;
|
||
|
|
publishDate: string;
|
||
|
|
views: number;
|
||
|
|
downloads: number;
|
||
|
|
tags: string[];
|
||
|
|
}
|
||
|
|
|
||
|
|
interface FilterOption {
|
||
|
|
id: string | number | null;
|
||
|
|
name: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- Filter Data (Mock) ---
|
||
|
|
const directories = ref<FilterOption[]>([
|
||
|
|
{ id: null, name: '全部目录' },
|
||
|
|
{ id: 'd1', name: '数学教案' },
|
||
|
|
{ id: 'd2', name: '语文课件' },
|
||
|
|
{ id: 'd3', name: '英语练习' },
|
||
|
|
{ id: 'd4', name: '科学实验' },
|
||
|
|
]);
|
||
|
|
|
||
|
|
const types1 = ref<FilterOption[]>([
|
||
|
|
{ id: null, name: '全部类型1' },
|
||
|
|
{ id: 't1-1', name: '课件PPT' },
|
||
|
|
{ id: 't1-2', name: '教学视频' },
|
||
|
|
{ id: 't1-3', name: 'Word文档' },
|
||
|
|
]);
|
||
|
|
|
||
|
|
const types2 = ref<FilterOption[]>([
|
||
|
|
{ id: null, name: '全部类型2' },
|
||
|
|
{ id: 't2-1', name: '期中' },
|
||
|
|
{ id: 't2-2', name: '期末' },
|
||
|
|
{ id: 't2-3', name: '单元测试' },
|
||
|
|
]);
|
||
|
|
|
||
|
|
// --- Search State ---
|
||
|
|
const searchKeyword = ref<string>('');
|
||
|
|
|
||
|
|
// --- Filter State ---
|
||
|
|
const selectedDirectory = ref<FilterOption | null>(directories.value[0]);
|
||
|
|
const selectedType1 = ref<FilterOption | null>(types1.value[0]);
|
||
|
|
const selectedType2 = ref<FilterOption | null>(types2.value[0]);
|
||
|
|
const filterPopupRef = ref<any>(null);
|
||
|
|
const currentFilterType = ref<'directory' | 'type1' | 'type2' | null>(null);
|
||
|
|
|
||
|
|
// --- Computed Properties for Popup ---
|
||
|
|
const currentFilterOptions = computed(() => {
|
||
|
|
switch (currentFilterType.value) {
|
||
|
|
case 'directory': return directories.value;
|
||
|
|
case 'type1': return types1.value;
|
||
|
|
case 'type2': return types2.value;
|
||
|
|
default: return [];
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
const currentFilterTitle = computed(() => {
|
||
|
|
switch (currentFilterType.value) {
|
||
|
|
case 'directory': return '选择目录';
|
||
|
|
case 'type1': return '选择类型1';
|
||
|
|
case 'type2': return '选择类型2';
|
||
|
|
default: return '选择选项';
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
const isOptionSelected = (option: FilterOption) => {
|
||
|
|
switch (currentFilterType.value) {
|
||
|
|
case 'directory': return selectedDirectory.value?.id === option.id;
|
||
|
|
case 'type1': return selectedType1.value?.id === option.id;
|
||
|
|
case 'type2': return selectedType2.value?.id === option.id;
|
||
|
|
default: return false;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// --- Mock API (Changed return type to Promise<any>) ---
|
||
|
|
const testList = async (params: any): Promise<any> => {
|
||
|
|
console.log('API called with params:', params);
|
||
|
|
const page = params.page || 1;
|
||
|
|
const pageSize = params.pageSize || 10;
|
||
|
|
|
||
|
|
const allItems: Resource[] = Array.from({ length: 50 }).map((_, index) => {
|
||
|
|
const id = index + 1;
|
||
|
|
const directoryFilter = params.directoryId ? `[目录${params.directoryId}]` : '';
|
||
|
|
const type1Filter = params.type1Id ? `[类型1-${params.type1Id}]` : '';
|
||
|
|
const type2Filter = params.type2Id ? `[类型2-${params.type2Id}]` : '';
|
||
|
|
|
||
|
|
// 生成随机标签
|
||
|
|
const subjectTags = ['语文', '数学', '英语', '科学', '历史'];
|
||
|
|
const gradeTags = ['一年级', '二年级', '三年级', '四年级', '五年级', '六年级'];
|
||
|
|
const termTags = ['上册', '下册'];
|
||
|
|
const typeTags = ['练习题', '课件', '教案', '测试卷'];
|
||
|
|
|
||
|
|
const tags = [
|
||
|
|
subjectTags[id % subjectTags.length],
|
||
|
|
gradeTags[id % gradeTags.length],
|
||
|
|
termTags[id % termTags.length],
|
||
|
|
...(id % 3 === 0 ? [typeTags[id % typeTags.length]] : [])
|
||
|
|
];
|
||
|
|
|
||
|
|
return {
|
||
|
|
id: id,
|
||
|
|
iconLetter: 'W',
|
||
|
|
title: `专题${String(id).padStart(2, '0')}${directoryFilter}${type1Filter}${type2Filter} (突破)`,
|
||
|
|
pages: 30 + (id % 15),
|
||
|
|
publishDate: `0${1 + (id % 9)}月${10 + (id % 20)}日发布`,
|
||
|
|
views: 1500 + (id * 17 % 500),
|
||
|
|
downloads: 50 + (id * 7 % 40),
|
||
|
|
tags: tags,
|
||
|
|
};
|
||
|
|
});
|
||
|
|
|
||
|
|
let filteredItems = allItems;
|
||
|
|
|
||
|
|
// 搜索关键词过滤
|
||
|
|
if (params.keyword) {
|
||
|
|
filteredItems = filteredItems.filter(item =>
|
||
|
|
item.title.toLowerCase().includes(params.keyword.toLowerCase())
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (params.directoryId) {
|
||
|
|
filteredItems = filteredItems.filter(item => item.title.includes(`[目录${params.directoryId}]`));
|
||
|
|
}
|
||
|
|
if (params.type1Id) {
|
||
|
|
filteredItems = filteredItems.filter(item => item.title.includes(`[类型1-${params.type1Id}]`));
|
||
|
|
}
|
||
|
|
if (params.type2Id) {
|
||
|
|
filteredItems = filteredItems.filter(item => item.title.includes(`[类型2-${params.type2Id}]`));
|
||
|
|
}
|
||
|
|
|
||
|
|
const totalFilteredItems = filteredItems.length;
|
||
|
|
const paginatedItems = filteredItems.slice((page - 1) * pageSize, page * pageSize);
|
||
|
|
|
||
|
|
return new Promise((resolve) => {
|
||
|
|
setTimeout(() => {
|
||
|
|
// Return structure expected by useLayout
|
||
|
|
resolve({ message: "成功", resultCode: 200, rows: paginatedItems, total: totalFilteredItems });
|
||
|
|
}, 300);
|
||
|
|
});
|
||
|
|
};
|
||
|
|
|
||
|
|
// --- Layout Hook (Removed 'immediate' from componentProps) ---
|
||
|
|
const [register, { reload, setParam }] = useLayout({
|
||
|
|
api: testList,
|
||
|
|
componentProps: {
|
||
|
|
// No problematic props
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
// --- Search Methods ---
|
||
|
|
const onSearchInput = () => {
|
||
|
|
setParam({ keyword: searchKeyword.value });
|
||
|
|
};
|
||
|
|
|
||
|
|
const onSearchConfirm = () => {
|
||
|
|
setParam({ keyword: searchKeyword.value });
|
||
|
|
};
|
||
|
|
|
||
|
|
const clearSearch = () => {
|
||
|
|
searchKeyword.value = '';
|
||
|
|
setParam({ keyword: '' });
|
||
|
|
};
|
||
|
|
|
||
|
|
// --- Navigation ---
|
||
|
|
const goToDetail = (id: number) => {
|
||
|
|
uni.navigateTo({
|
||
|
|
url: `./detail?id=${id}` // Navigate to detail page in the same directory
|
||
|
|
});
|
||
|
|
};
|
||
|
|
|
||
|
|
// --- Filter Popup Methods ---
|
||
|
|
const openFilterPopup = (type: 'directory' | 'type1' | 'type2') => {
|
||
|
|
currentFilterType.value = type;
|
||
|
|
filterPopupRef.value?.open();
|
||
|
|
};
|
||
|
|
|
||
|
|
const closeFilterPopup = () => {
|
||
|
|
filterPopupRef.value?.close();
|
||
|
|
};
|
||
|
|
|
||
|
|
const selectFilterOption = (option: FilterOption) => {
|
||
|
|
switch (currentFilterType.value) {
|
||
|
|
case 'directory':
|
||
|
|
selectedDirectory.value = option;
|
||
|
|
setParam({ directoryId: option.id });
|
||
|
|
break;
|
||
|
|
case 'type1':
|
||
|
|
selectedType1.value = option;
|
||
|
|
setParam({ type1Id: option.id });
|
||
|
|
break;
|
||
|
|
case 'type2':
|
||
|
|
selectedType2.value = option;
|
||
|
|
setParam({ type2Id: option.id });
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
closeFilterPopup();
|
||
|
|
};
|
||
|
|
|
||
|
|
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<style scoped lang="scss">
|
||
|
|
.search-section {
|
||
|
|
padding: 20rpx 30rpx;
|
||
|
|
background-color: #ffffff;
|
||
|
|
border-bottom: 1rpx solid #e0e0e0;
|
||
|
|
position: sticky;
|
||
|
|
top: 0;
|
||
|
|
z-index: 10;
|
||
|
|
|
||
|
|
.search-box {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
background-color: #f5f5f5;
|
||
|
|
border-radius: 50rpx;
|
||
|
|
padding: 0 20rpx;
|
||
|
|
height: 70rpx;
|
||
|
|
|
||
|
|
uni-icons {
|
||
|
|
margin-right: 15rpx;
|
||
|
|
}
|
||
|
|
|
||
|
|
.search-input {
|
||
|
|
flex: 1;
|
||
|
|
height: 100%;
|
||
|
|
border: none;
|
||
|
|
background: transparent;
|
||
|
|
font-size: 28rpx;
|
||
|
|
color: #333;
|
||
|
|
|
||
|
|
&::placeholder {
|
||
|
|
color: #999;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.search-clear {
|
||
|
|
margin-left: 15rpx;
|
||
|
|
cursor: pointer;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.list-container {
|
||
|
|
background-color: #f4f5f7;
|
||
|
|
/* Add padding-top to prevent content from hiding behind sticky filter */
|
||
|
|
/* Adjust value based on filter-section height */
|
||
|
|
}
|
||
|
|
|
||
|
|
.resource-item {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
background-color: #ffffff;
|
||
|
|
border-radius: 16rpx;
|
||
|
|
padding: 25rpx 20rpx;
|
||
|
|
margin-bottom: 20rpx;
|
||
|
|
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
|
||
|
|
cursor: pointer; // Add cursor pointer to indicate clickability
|
||
|
|
|
||
|
|
.item-icon-container {
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
align-items: center;
|
||
|
|
margin-right: 25rpx;
|
||
|
|
flex-shrink: 0;
|
||
|
|
|
||
|
|
.item-icon {
|
||
|
|
width: 80rpx;
|
||
|
|
height: 80rpx;
|
||
|
|
line-height: 80rpx;
|
||
|
|
border-radius: 12rpx;
|
||
|
|
background-color: #409eff;
|
||
|
|
color: #ffffff;
|
||
|
|
font-size: 40rpx;
|
||
|
|
font-weight: bold;
|
||
|
|
text-align: center;
|
||
|
|
margin-bottom: 8rpx;
|
||
|
|
}
|
||
|
|
|
||
|
|
.item-pages {
|
||
|
|
font-size: 22rpx;
|
||
|
|
color: #999;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.item-details {
|
||
|
|
flex: 1;
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
overflow: hidden;
|
||
|
|
|
||
|
|
.item-title {
|
||
|
|
font-size: 30rpx;
|
||
|
|
color: #333;
|
||
|
|
font-weight: bold;
|
||
|
|
margin-bottom: 15rpx;
|
||
|
|
white-space: nowrap;
|
||
|
|
overflow: hidden;
|
||
|
|
text-overflow: ellipsis;
|
||
|
|
}
|
||
|
|
|
||
|
|
.tags-container {
|
||
|
|
display: flex;
|
||
|
|
flex-wrap: wrap;
|
||
|
|
gap: 10rpx;
|
||
|
|
margin-bottom: 15rpx;
|
||
|
|
|
||
|
|
.tag-item {
|
||
|
|
background-color: #f0f2f5;
|
||
|
|
color: #666;
|
||
|
|
font-size: 22rpx;
|
||
|
|
padding: 8rpx 16rpx;
|
||
|
|
border-radius: 20rpx;
|
||
|
|
white-space: nowrap;
|
||
|
|
border: 1rpx solid #e0e0e0;
|
||
|
|
|
||
|
|
&:first-child {
|
||
|
|
background-color: #e7f4ff;
|
||
|
|
color: #1890ff;
|
||
|
|
border-color: #91d5ff;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.item-meta {
|
||
|
|
display: flex;
|
||
|
|
justify-content: space-between;
|
||
|
|
align-items: center;
|
||
|
|
font-size: 24rpx;
|
||
|
|
color: #999;
|
||
|
|
flex-wrap: wrap;
|
||
|
|
|
||
|
|
.meta-text {
|
||
|
|
margin-right: 15rpx;
|
||
|
|
white-space: nowrap;
|
||
|
|
&:last-child {
|
||
|
|
margin-right: 0;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Popup Styles
|
||
|
|
.popup-content {
|
||
|
|
background-color: #fff;
|
||
|
|
border-top-left-radius: 20rpx;
|
||
|
|
border-top-right-radius: 20rpx;
|
||
|
|
padding: 20rpx;
|
||
|
|
padding-bottom: 40rpx; // Add space at the bottom
|
||
|
|
}
|
||
|
|
|
||
|
|
.popup-header {
|
||
|
|
display: flex;
|
||
|
|
justify-content: space-between;
|
||
|
|
align-items: center;
|
||
|
|
padding: 15rpx 10rpx;
|
||
|
|
font-size: 32rpx;
|
||
|
|
font-weight: bold;
|
||
|
|
border-bottom: 1rpx solid #eee;
|
||
|
|
margin-bottom: 10rpx;
|
||
|
|
}
|
||
|
|
|
||
|
|
.popup-options {
|
||
|
|
max-height: 60vh; // Limit popup height
|
||
|
|
}
|
||
|
|
|
||
|
|
.option-item {
|
||
|
|
padding: 25rpx 20rpx;
|
||
|
|
font-size: 28rpx;
|
||
|
|
color: #333;
|
||
|
|
border-bottom: 1rpx solid #f5f5f5;
|
||
|
|
cursor: pointer;
|
||
|
|
|
||
|
|
&:last-child {
|
||
|
|
border-bottom: none;
|
||
|
|
}
|
||
|
|
|
||
|
|
&.active {
|
||
|
|
color: #409eff; // Highlight selected item
|
||
|
|
font-weight: bold;
|
||
|
|
}
|
||
|
|
|
||
|
|
&:hover {
|
||
|
|
background-color: #f9f9f9; // Subtle hover effect
|
||
|
|
}
|
||
|
|
}
|
||
|
|
</style>
|