还原了搜索栏布局、样式;

还原了文件夹管理布局、样式;
还原了头部布局、样式;
This commit is contained in:
User
2025-10-10 15:01:36 +08:00
parent e40288e8ef
commit f68cd4e0fd
10 changed files with 469 additions and 420 deletions

View File

@@ -38,8 +38,8 @@
--note-star: #ffffdd33; /* Star/favorite color (updated to match original) */ --note-star: #ffffdd33; /* Star/favorite color (updated to match original) */
/* Folder colors */ /* Folder colors */
--folder-name: #99000000; /* Folder name color (60% black) */ --folder-name: #9b9b9b; /* Folder name color (60% black) */
--folder-count: #4c000000; /* Folder item count color (30% black) */ --folder-count: #b4b4b4; /* Folder item count color (30% black) */
--folder-item-selected: #f5f5f5; /* Folder item selected background color */ --folder-item-selected: #f5f5f5; /* Folder item selected background color */
/* Button colors - Based on Smartisan's button styles */ /* Button colors - Based on Smartisan's button styles */

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 119 KiB

View File

@@ -1,221 +1,77 @@
<template> <template>
<div <div @click="onPress" class="code-fun-flex-row code-fun-items-center code-fun-relative folder-item">
@click="onPress" <img class="folder-icon" :src="iconSrc" />
class="folder-item-container" <span class="folder-name">{{ name }}</span>
:class="{ 'folder-item-selected': isSelected, 'folder-item-pressed': isPressed }" <span class="folder-count">{{ noteCount }}</span>
@mousedown="handleMouseDown"
@mouseup="handleMouseUp"
@mouseleave="handleMouseLeave"
>
<div class="folder-item-content">
<!-- Hidden folder icon (matches native implementation) -->
<img
v-if="folderIconSrc"
:src="folderIconSrc"
class="folder-item-icon"
alt="folder icon"
style="visibility: hidden;"
/>
<ion-icon
v-else
:icon="folderIcon"
class="folder-item-icon"
style="visibility: hidden;"
></ion-icon>
<!-- Checkbox for selection (visible when isSelected is true) -->
<img
v-if="isSelected"
src="/assets/icons/drawable-xxhdpi/icon_folder_checked.png"
class="folder-item-checkbox"
alt="selected"
/>
<img
v-else
src="/assets/icons/drawable-xxhdpi/icon_folder_unchecked.png"
class="folder-item-checkbox"
alt="not selected"
/>
<div class="folder-item-text-container">
<div class="folder-item-name">
{{ name }}
</div>
<div class="folder-item-count">
{{ noteCount }} 条便签
</div>
</div>
<!-- Selected indicator (visible when isSelected is true) -->
<img
v-if="isSelected"
src="/assets/icons/drawable-xxhdpi/folder_selected.png"
class="folder-item-selected-icon"
alt="selected"
/>
</div>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, computed } from 'vue'; import { computed } from 'vue'
import { folder, star, trash, document } from 'ionicons/icons';
const props = defineProps({ const props = defineProps({
id: { id: {
type: String, type: String,
required: true required: true,
}, },
name: { name: {
type: String, type: String,
required: true required: true,
}, },
noteCount: { noteCount: {
type: Number, type: Number,
required: true required: true,
}, },
onPress: { onPress: {
type: Function, type: Function,
required: true required: true,
}, },
isSelected: { isSelected: {
type: Boolean, type: Boolean,
default: false default: false,
} },
}); })
const isPressed = ref(false); const iconSrc = computed(() => {
const folderIcon = computed(() => {
switch (props.id) { switch (props.id) {
case 'all': case 'all':
return folder; return 'assets/icons/drawable-xxhdpi/icon_folder_all.png'
case 'starred': case 'starred':
return star; return 'assets/icons/drawable-xxhdpi/icon_folder_favorite.png'
case 'trash': case 'trash':
return trash; return 'assets/icons/drawable-xxhdpi/icon_folder_trash.png'
case 'archive':
return 'assets/icons/drawable-xxhdpi/icon_folder_document.png'
default: default:
return document; return 'assets/icons/drawable-xxhdpi/icon_folder_document.png'
} }
}); })
const folderIconSrc = computed(() => {
switch (props.id) {
case 'all':
return '/assets/icons/drawable-xxhdpi/sidebar_folder_icon_all.png';
case 'starred':
return '/assets/icons/drawable-xxhdpi/sidebar_folder_icon_favorite.png';
case 'trash':
return '/assets/icons/drawable-xxhdpi/sidebar_folder_icon_trash.png';
default:
return '/assets/icons/drawable-xxhdpi/sidebar_folder_icon_document.png';
}
});
const handleMouseDown = () => {
isPressed.value = true;
};
const handleMouseUp = () => {
isPressed.value = false;
};
const handleMouseLeave = () => {
isPressed.value = false;
};
</script> </script>
<style scoped> <style scoped>
.folder-item-container { .folder-item {
position: relative; padding: 0.3rem 0;
min-height: 44px; background-color: #00000000;
background-color: var(--background-card);
cursor: pointer;
overflow: hidden;
} }
.folder-item-container.folder-item-selected { .folder-icon {
background-color: var(--folder-item-selected); width: 1.8rem;
} height: 1.8rem;
.folder-item-container.folder-item-pressed {
background-color: var(--black-05);
transform: scale(0.99);
}
.folder-item-container.folder-item-pressed::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: url('/assets/icons/drawable-xxhdpi/folder_item_pressed_bg.png') repeat;
background-size: 100% 100%;
opacity: 0.5;
pointer-events: none;
transition: opacity 0.2s ease;
}
.folder-item-content {
display: flex;
align-items: center;
width: 100%;
height: 44px;
padding: 0 16px;
position: relative;
}
.folder-item-icon {
width: 30px;
height: 30px;
color: var(--folder-name);
flex-shrink: 0; flex-shrink: 0;
margin-left: 1px; margin-inline: 0.3rem;
padding-left: 11px;
} }
.folder-item-icon[src] { .folder-name {
width: 30px; font-size: 0.9rem;
height: 30px; line-height: 1.52rem;
object-fit: contain; color: #9b9b9b;
} }
.folder-item-checkbox { .folder-count {
width: 30px; font-size: 0.8rem;
height: 30px; line-height: 1.16rem;
flex-shrink: 0; margin-left: 0.7rem;
margin-left: 10px; margin-top: 0.2rem;
} color: #b8b8b8;
.folder-item-selected-icon {
width: 30px;
height: 30px;
margin-left: auto;
flex-shrink: 0;
margin-right: 0;
}
.folder-item-text-container {
flex: 1;
margin-left: 10px;
margin-right: 6px;
}
.folder-item-name {
font-size: 15px;
font-weight: normal;
color: var(--folder-name);
line-height: 1.2;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.folder-item-count {
font-size: 12px;
color: var(--folder-count);
line-height: 1.2;
margin-top: 2px;
} }
</style> </style>

View File

@@ -0,0 +1,135 @@
<template>
<div class="code-fun-flex-col page">
<!-- 全部便签文件夹项 -->
<FolderItem id="all" name="全部便签" :noteCount="allCount" :isSelected="selectedFolder === 'all'" :onPress="handleAllClick" />
<!-- 加星便签文件夹项 -->
<FolderItem id="starred" name="加星便签" :noteCount="starredCount" :isSelected="selectedFolder === 'starred'" :onPress="handleStarredClick" />
<!-- 回收站文件夹项 -->
<FolderItem id="trash" name="回收站" :noteCount="trashCount" :isSelected="selectedFolder === 'trash'" :onPress="handleTrashClick" />
<!-- 同步信息区域 -->
<div class="code-fun-flex-col code-fun-justify-start section_7">
<div class="code-fun-flex-row code-fun-justify-between code-fun-items-center section_8">
<div class="code-fun-flex-row code-fun-items-center">
<img class="code-fun-shrink-0 image_7" :src="`assets/icons/drawable-xxhdpi/btn_edit_folder.png`" />
<span class="text_11 code-fun-ml-10">上次同步:{{ lastSyncTime }}</span>
</div>
<div class="code-fun-flex-row code-fun-items-center">
<img class="image_8 code-fun-ml-12" :src="`assets/icons/drawable-xxhdpi/btn_add_folder.png`" @click="handleAddFolder" />
</div>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
import FolderItem from './FolderItem.vue'
const props = defineProps({
allCount: {
type: Number,
default: 0,
},
starredCount: {
type: Number,
default: 0,
},
trashCount: {
type: Number,
default: 0,
},
archiveCount: {
type: Number,
default: 0,
},
selectedFolder: {
type: String,
default: '',
},
lastSyncTime: {
type: String,
default: '10/10上午9:28',
},
onAllClick: {
type: Function,
default: null,
},
onStarredClick: {
type: Function,
default: null,
},
onTrashClick: {
type: Function,
default: null,
},
onArchiveClick: {
type: Function,
default: null,
},
onAddFolder: {
type: Function,
default: null,
},
})
const handleAllClick = () => {
if (props.onAllClick) {
props.onAllClick()
}
}
const handleStarredClick = () => {
if (props.onStarredClick) {
props.onStarredClick()
}
}
const handleTrashClick = () => {
if (props.onTrashClick) {
props.onTrashClick()
}
}
const handleArchiveClick = () => {
if (props.onArchiveClick) {
props.onArchiveClick()
}
}
const handleAddFolder = event => {
// 阻止事件冒泡到父元素
event.stopPropagation()
if (props.onAddFolder) {
props.onAddFolder()
}
}
</script>
<style lang="less" scoped>
.page {
.section_7 {
margin-top: 2rem;
background-color: #00000000;
.section_8 {
padding: 0.29rem 0.92rem;
background-color: #f4f4f4;
border: solid 0.063rem #f0ece7;
.image_7,
.image_8 {
border-radius: 0.63rem;
width: 2rem;
height: 2rem;
object-fit: contain;
}
.text_11 {
color: #cacaca;
font-size: 0.7rem;
line-height: 1.16rem;
}
}
}
}
</style>

View File

@@ -1,22 +1,18 @@
<template> <template>
<div class="code-fun-flex-row code-fun-items-center component"> <div class="code-fun-flex-row code-fun-items-center component">
<!-- 左侧图标 --> <!-- 左侧图标 -->
<img v-if="leftType == 'settings'" class="left-icon" src="/assets/icons/drawable-xxhdpi/btn_settings.png" @click="handleLeftAction" /> <img class="left-icon" :src="leftIconSource" @click="handleLeftAction" />
<img v-else class="left-icon" src="/assets/icons/drawable-xxhdpi/btn_back.png" @click="handleLeftAction" />
<!-- 标题区域 --> <!-- 标题区域 -->
<div class="title-container" @click="handleTitlePress" v-if="leftType == 'settings'"> <div class="title-container" @click="handleTitlePress">
<span class="text">{{ title }}</span> <span class="text">{{ title }}</span>
<!-- 文件夹展开图标 --> <!-- 文件夹展开图标 -->
<img class="folder-icon" :src="folderExpanded ? '/assets/icons/drawable-xxhdpi/folder_title_arrow_pressed.png' : '/assets/icons/drawable-xxhdpi/folder_title_arrow_normal.png'" @click.stop="handleFolderToggle" /> <img v-if="showFolderIcon" class="folder-icon" :src="folderExpanded ? '/assets/icons/drawable-xxhdpi/folder_title_arrow_pressed.png' : '/assets/icons/drawable-xxhdpi/folder_title_arrow_normal.png'" @click.stop="handleFolderToggle" />
</div>
<div class="title-container" v-else>
<span class="text">{{ title }}</span>
</div> </div>
<!-- 右侧操作按钮 --> <!-- 右侧操作按钮 -->
<img v-if="actionIcon === 'create'" class="image_4" src="/assets/icons/drawable-xxhdpi/btn_create.png" @click="handleAction" /> <img v-if="actionIcon === 'create'" class="image_4" src="/assets/icons/drawable-xxhdpi/btn_create.png" @click="handleAction" />
<i class="image_4" v-else></i> <div v-else class="image_4-placeholder"></div>
</div> </div>
</template> </template>
@@ -77,33 +73,14 @@ const folderExpanded = computed(() => {
return props.isFolderExpanded !== undefined ? props.isFolderExpanded : localFolderExpanded.value return props.isFolderExpanded !== undefined ? props.isFolderExpanded : localFolderExpanded.value
}) })
const actionIconSource = computed(() => { const showFolderIcon = computed(() => {
// 根据actionIcon属性返回对应的图标路径 // 只有当leftType为settings且有onFolderToggle回调时才显示文件夹图标
switch (props.actionIcon) { return props.leftType === 'settings' && props.onFolderToggle
case 'settings':
return '/assets/icons/drawable-xxhdpi/btn_settings.png'
case 'create':
return '/assets/icons/drawable-xxhdpi/btn_create.png'
default:
return null
}
}) })
const leftIconSource = computed(() => { const leftIconSource = computed(() => {
// 根据leftIcon属性返回对应的图标路径 // 根据leftType属性返回对应的图标路径
switch (props.leftIcon) { return props.leftType === 'settings' ? '/assets/icons/drawable-xxhdpi/btn_settings.png' : '/assets/icons/drawable-xxhdpi/btn_back.png'
case 'folder':
// 文件夹图标根据展开状态切换
return folderExpanded.value ? '/assets/icons/drawable-xxhdpi/folder_title_arrow_pressed.png' : '/assets/icons/drawable-xxhdpi/folder_title_arrow_normal.png'
case 'back':
// 返回图标
return '/assets/icons/drawable-xxhdpi/btn_back.png'
case 'settings':
// 设置图标
return '/assets/icons/drawable-xxhdpi/btn_settings.png'
default:
return null
}
}) })
const handleFolderToggle = () => { const handleFolderToggle = () => {
@@ -146,10 +123,10 @@ const handleTitlePress = () => {
<style scoped lang="less"> <style scoped lang="less">
.component { .component {
padding: 2rem 0.72rem 0.5rem 0.72rem; padding: 2rem 0.72rem 0.5rem;
background-color: #00000000; background-color: #00000000;
background-image: url(https://codefun-proj-user-res-1256085488.cos.ap-guangzhou.myqcloud.com/686f20ecd54496f19f54e801/68e862ab9520a30011f388ff/17600644443142372133.png); background-image: url(assets/icons/drawable-xxhdpi/action_bar_default.png);
background-size: cover; background-size: 100% 100%;
background-position: 0% 0%; background-position: 0% 0%;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@@ -162,6 +139,11 @@ const handleTitlePress = () => {
cursor: pointer; cursor: pointer;
} }
.image_4-placeholder {
width: 1.7rem;
height: 1.7rem;
}
.title-container { .title-container {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@@ -0,0 +1,104 @@
<template>
<div class="code-fun-flex-col code-fun-justify-start section_3">
<div class="view">
<div class="image-wrapper">
<!-- 搜索图标 -->
<img class="image_7" :src="`assets/icons/drawable-xxhdpi/search_bar_left_icon.png`" />
<!-- 搜索输入框 -->
<input ref="searchInput" type="input" placeholder="搜索便签..." :value="modelValue" @input="handleInput" @keydown.enter="handleSearch" @focus="handleFocus" @blur="handleBlur" />
<!-- 清除按钮 -->
<img v-if="modelValue && modelValue.length > 0" class="clear-button" :src="`assets/icons/drawable-xxhdpi/search_clear_normal.png`" @click="handleClear" />
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const props = defineProps({
modelValue: {
type: String,
default: '',
},
})
const emit = defineEmits(['update:modelValue', 'search', 'clear', 'focus', 'blur'])
const searchInput = ref(null)
const handleInput = event => {
emit('update:modelValue', event.target.value)
}
const handleSearch = event => {
emit('search', event.target.value)
}
const handleClear = () => {
emit('update:modelValue', '')
emit('clear')
if (searchInput.value) {
searchInput.value.focus()
}
}
const handleFocus = event => {
emit('focus', event)
}
const handleBlur = event => {
emit('blur', event)
}
// 暴露方法给父组件
defineExpose({
focus: () => {
if (searchInput.value) {
searchInput.value.focus()
}
},
blur: () => {
if (searchInput.value) {
searchInput.value.blur()
}
},
})
</script>
<style lang="less" scoped>
.section_3 {
.view {
.image-wrapper {
position: relative;
padding: 0.25rem 0.5rem;
border-radius: 2rem;
background: #f6f9fa;
border: 1px solid #ccb8a3;
display: flex;
flex-direction: row;
align-items: center;
.image_7,
.clear-button {
width: 1.2rem;
height: 1.2rem;
object-fit: contain;
margin-right: 0.1rem;
}
input {
flex: 1;
height: 1.2rem;
line-height: 1.2rem;
background: transparent;
border: none;
outline: none;
font-size: 0.75rem;
color: #333;
padding: 0;
}
}
}
}
</style>

View File

@@ -24,14 +24,16 @@
</div> </div>
<ion-content> <ion-content>
<ion-list style="background-color: var(--background); padding: 0 16px; --ion-item-background: var(--background)"> <ion-list style="background-color: var(--background); padding: 0 16px; --ion-item-background: var(--background)">
<FolderItem <FolderManage
v-for="folder in filteredFolders" :allCount="allNotesCount"
:key="folder.id" :starredCount="starredNotesCount"
:id="folder.id" :trashCount="trashNotesCount"
:name="folder.name" :archiveCount="archiveCount"
:noteCount="folder.noteCount" :selectedFolder="selectedFolder"
:onPress="() => handleFolderPress(folder.id)" :onAllClick="() => handleFolderPress('all')"
:isSelected="folder.id === selectedFolder" :onStarredClick="() => handleFolderPress('starred')"
:onTrashClick="() => handleFolderPress('trash')"
:onArchiveClick="() => handleFolderPress('archive')"
/> />
</ion-list> </ion-list>
</ion-content> </ion-content>
@@ -42,7 +44,7 @@
import { ref, computed, onMounted } from 'vue'; import { ref, computed, onMounted } from 'vue';
import { useAppStore } from '../stores/useAppStore'; import { useAppStore } from '../stores/useAppStore';
import { search, closeCircle } from 'ionicons/icons'; import { search, closeCircle } from 'ionicons/icons';
import FolderItem from '../components/FolderItem.vue'; import FolderManage from '../components/FolderManage.vue';
import Header from '../components/Header.vue'; import Header from '../components/Header.vue';
const store = useAppStore(); const store = useAppStore();
@@ -71,6 +73,7 @@ const allNotesCount = computed(() => store.notes.length);
const starredNotesCount = computed(() => store.notes.filter(note => note.isStarred).length); const starredNotesCount = computed(() => store.notes.filter(note => note.isStarred).length);
// Assuming we have a way to track deleted notes in the future // Assuming we have a way to track deleted notes in the future
const trashNotesCount = 0; const trashNotesCount = 0;
const archiveCount = 0;
const foldersWithAllNotes = computed(() => { const foldersWithAllNotes = computed(() => {
return [ return [

View File

@@ -1,109 +1,60 @@
<template> <template>
<ion-page> <ion-app>
<Header <div class="container">
:title="headerTitle" <Header :title="headerTitle" :onAction="handleAddNote" actionIcon="create" leftType="settings" :onLeftAction="handleSettingsPress" :onFolderToggle="handleFolderToggle" :isFolderExpanded="isFolderExpanded" :onTitlePress="handleFolderToggle" />
:onAction="handleAddNote"
actionIcon="create"
leftType="settings"
:onLeftAction="handleSettingsPress"
:onFolderToggle="handleFolderToggle"
:isFolderExpanded="isFolderExpanded"
:onTitlePress="handleFolderToggle"
/>
<!-- 悬浮文件夹列表 - 使用绝对定位实现 --> <!-- 悬浮文件夹列表 - 使用绝对定位实现 -->
<div <div
v-if="isFolderExpanded" v-if="isFolderExpanded"
style="position: absolute; top: 50px; left: 10%; right: 10%; z-index: 1000; background-color: var(--background-card); border-radius: 8px; box-shadow: 0 2px 4px var(--shadow); border: 1px solid var(--border); overflow: hidden" style="position: absolute; top: 50px; left: 10%; right: 10%; z-index: 1000; background-color: var(--background-card); border-radius: 8px; box-shadow: 0 2px 4px var(--shadow); border: 1px solid #f0ece7; overflow: hidden">
> <FolderManage
<FolderItem :allCount="notes.length"
id="all" :starredCount="starredNotesCount"
name="全部便签" :trashCount="0"
:noteCount="notes.length" :archiveCount="0"
:isSelected="currentFolder === 'all'" :selectedFolder="currentFolder"
:onPress="() => { :onAllClick="
setCurrentFolder('all'); () => {
setIsFolderExpanded(false); setCurrentFolder('all')
}" setIsFolderExpanded(false)
/> }
<FolderItem "
id="starred" :onStarredClick="
name="加星便签" () => {
:noteCount="starredNotesCount" setCurrentFolder('starred')
:isSelected="currentFolder === 'starred'" setIsFolderExpanded(false)
:onPress="() => { }
setCurrentFolder('starred'); "
setIsFolderExpanded(false); :onTrashClick="
}" () => {
/> setCurrentFolder('trash')
<FolderItem setIsFolderExpanded(false)
id="trash" }
name="回收站" " />
:noteCount="0"
:isSelected="currentFolder === 'trash'"
:onPress="() => {
setCurrentFolder('trash');
setIsFolderExpanded(false);
}"
/>
</div> </div>
<div style="flex-direction: row; align-items: center; padding: 8px 16px; background-color: var(--background-card); border-bottom: 1px solid var(--border)"> <div style="padding: 0.8rem 0.5rem">
<div style="flex: 1; flex-direction: row; align-items: center; background-color: var(--background-card); height: 36px; padding: 0 8px; padding-vertical: 0"> <SearchBar v-model="searchQuery" @search="handleSearch" @clear="handleClearSearch" @focus="handleSearchFocus" @blur="handleSearchBlur" />
<div style="flex: 1; flex-direction: row; align-items: center; background-color: #f0f0f0; border-radius: 4; height: 36px; padding: 0 8px; padding-vertical: 0">
<img :src="'/assets/icons/drawable-xxhdpi/search_bar_left_icon.png'" style="width: 20px; height: 20px; tint-color: var(--text-tertiary)" />
<input
placeholder="搜索便签..."
:value="searchQuery"
@input="e => setSearchQuery(e.target.value)"
@keydown.enter="handleSearch"
style="flex: 1; font-size: 16px; color: var(--text-primary); margin-left: 8px; margin-right: 8px; padding: 0; border: none; background: transparent; outline: none"
/>
<ion-button
v-if="searchQuery.length > 0"
fill="clear"
@click="() => setSearchQuery('')"
style="--padding-start: 0; --padding-end: 0; width: 20px; height: 20px; margin: 0; padding: 0"
>
<img :src="'/assets/icons/drawable-xxhdpi/search_bar_clear_btn.png'" style="width: 20px; height: 20px; tint-color: var(--text-tertiary)" />
</ion-button>
</div>
</div>
</div> </div>
<div v-if="filteredAndSortedNotes.length === 0" style="flex: 1; justify-content: center; align-items: center; padding: 16px; background-color: var(--background)"> <div v-if="filteredAndSortedNotes.length === 0" style="flex: 1; justify-content: center; align-items: center; padding: 16px">
<ion-text style="font-size: 18px; font-weight: 600; color: var(--text-tertiary); margin-bottom: 8px"> <ion-text style="font-size: 18px; font-weight: 600; color: var(--text-tertiary); margin-bottom: 8px"> 未找到便签 </ion-text>
未找到便签
</ion-text>
<ion-text style="font-size: 14px; color: var(--text-tertiary); text-align: center; line-height: 20px"> <ion-text style="font-size: 14px; color: var(--text-tertiary); text-align: center; line-height: 20px">
{{ searchQuery ? '尝试其他搜索词' : '点击 + 按钮创建您的第一条便签' }} {{ searchQuery ? '尝试其他搜索词' : '点击 + 按钮创建您的第一条便签' }}
</ion-text> </ion-text>
</div> </div>
<div v-else style="flex: 1; background-color: var(--background)"> <div v-else style="flex: 1">
<ion-text style="font-size: 13px; color: var(--text-tertiary); padding-horizontal: 16px; padding-vertical: 8px; display: block"> <ion-text style="font-size: 13px; color: var(--text-tertiary); padding-horizontal: 16px; padding-vertical: 8px; display: block"> {{ filteredAndSortedNotes.length }} 条便签 </ion-text>
{{ filteredAndSortedNotes.length }} 条便签
</ion-text>
<div style="border-radius: 6px; overflow: hidden; box-shadow: 0 1px 2px var(--shadow); background-color: var(--background-card); margin: 0 16px"> <div style="border-radius: 6px; overflow: hidden; box-shadow: 0 1px 2px var(--shadow); background-color: var(--background-card); margin: 0 16px">
<div v-for="note in filteredAndSortedNotes" :key="note.id"> <div v-for="note in filteredAndSortedNotes" :key="note.id">
<NoteItem <NoteItem :title="note.title" :content="note.content" :date="formatDate(note.updatedAt)" :isStarred="note.isStarred" :onPress="() => handleNotePress(note.id)" :onDelete="() => handleDeleteNote(note.id)" />
:title="note.title"
:content="note.content"
:date="formatDate(note.updatedAt)"
:isStarred="note.isStarred"
:onPress="() => handleNotePress(note.id)"
:onDelete="() => handleDeleteNote(note.id)"
/>
</div> </div>
</div> </div>
</div> </div>
<!-- 点击外部区域收起文件夹列表的覆盖层 --> <!-- 点击外部区域收起文件夹列表的覆盖层 -->
<div <div v-if="isFolderExpanded" @click="() => setIsFolderExpanded(false)" style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; background-color: transparent; z-index: 99"></div>
v-if="isFolderExpanded" </div>
@click="() => setIsFolderExpanded(false)"
style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; background-color: transparent; z-index: 99"
></div>
<ion-alert <ion-alert
:is-open="showAlert" :is-open="showAlert"
@didDismiss="() => setShowAlert(false)" @didDismiss="() => setShowAlert(false)"
@@ -112,165 +63,183 @@
:buttons="[ :buttons="[
{ {
text: '取消', text: '取消',
role: 'cancel' role: 'cancel',
}, },
{ {
text: '删除', text: '删除',
handler: confirmDeleteNote handler: confirmDeleteNote,
} },
]" ]"></ion-alert>
></ion-alert> </ion-app>
</ion-page>
</template> </template>
<script setup> <script setup>
import { ref, computed, onMounted } from 'vue'; import { ref, computed, onMounted } from 'vue'
import { useAppStore } from '../stores/useAppStore'; import { useAppStore } from '../stores/useAppStore'
import { create, settings } from 'ionicons/icons'; import { create, settings } from 'ionicons/icons'
import NoteItem from '../components/NoteItem.vue'; import NoteItem from '../components/NoteItem.vue'
import Header from '../components/Header.vue'; import Header from '../components/Header.vue'
import FolderItem from '../components/FolderItem.vue'; import FolderManage from '../components/FolderManage.vue'
import SearchBar from '../components/SearchBar.vue'
const store = useAppStore(); const store = useAppStore()
// 加载初始数据 // 加载初始数据
onMounted(() => { onMounted(() => {
store.loadData(); store.loadData()
}); })
const searchQuery = ref(''); const searchQuery = ref('')
const sortBy = ref('date'); // 'date', 'title', 'starred' const sortBy = ref('date') // 'date', 'title', 'starred'
const isFolderExpanded = ref(false); const isFolderExpanded = ref(false)
const currentFolder = ref('all'); // 默认文件夹是"全部便签" const currentFolder = ref('all') // 默认文件夹是"全部便签"
const showAlert = ref(false); const showAlert = ref(false)
const noteToDelete = ref(null); const noteToDelete = ref(null)
// 计算加星便签数量 // 计算加星便签数量
const starredNotesCount = computed(() => { const starredNotesCount = computed(() => {
return store.notes.filter(note => note.isStarred).length; return store.notes.filter(note => note.isStarred).length
}); })
// 根据当前文件夹过滤便签 // 根据当前文件夹过滤便签
const filteredNotes = computed(() => { const filteredNotes = computed(() => {
return store.notes.filter(note => { return store.notes.filter(note => {
switch (currentFolder.value) { switch (currentFolder.value) {
case 'all': case 'all':
return true; return true
case 'starred': case 'starred':
return note.isStarred; return note.isStarred
case 'trash': case 'trash':
// 假设我们有一个isDeleted属性来标识已删除的便签 // 假设我们有一个isDeleted属性来标识已删除的便签
return note.isDeleted || false; return note.isDeleted || false
default: default:
return note.folderId === currentFolder.value; return note.folderId === currentFolder.value
} }
}); })
}); })
// Filter and sort notes // Filter and sort notes
const filteredAndSortedNotes = computed(() => { const filteredAndSortedNotes = computed(() => {
return filteredNotes.value return filteredNotes.value
.filter(note => .filter(note => note.title.toLowerCase().includes(searchQuery.value.toLowerCase()) || note.content.toLowerCase().includes(searchQuery.value.toLowerCase()))
note.title.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
note.content.toLowerCase().includes(searchQuery.value.toLowerCase())
)
.sort((a, b) => { .sort((a, b) => {
if (sortBy.value === 'title') { if (sortBy.value === 'title') {
return a.title.localeCompare(b.title); return a.title.localeCompare(b.title)
} else if (sortBy.value === 'starred') { } else if (sortBy.value === 'starred') {
return (b.isStarred ? 1 : 0) - (a.isStarred ? 1 : 0); return (b.isStarred ? 1 : 0) - (a.isStarred ? 1 : 0)
} else { } else {
return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(); return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
} }
}); })
}); })
// 计算头部标题 // 计算头部标题
const headerTitle = computed(() => { const headerTitle = computed(() => {
switch (currentFolder.value) { switch (currentFolder.value) {
case 'all': case 'all':
return '全部便签'; return '全部便签'
case 'starred': case 'starred':
return '加星便签'; return '加星便签'
case 'trash': case 'trash':
return '回收站'; return '回收站'
default: default:
return '文件夹'; return '文件夹'
} }
}); })
const handleNotePress = (noteId) => { const handleNotePress = noteId => {
// 导航到详情页面的逻辑将在路由中处理 // 导航到详情页面的逻辑将在路由中处理
window.location.hash = `#/notes/${noteId}`; window.location.hash = `#/notes/${noteId}`
}; }
const handleAddNote = () => { const handleAddNote = () => {
// 导航到编辑页面的逻辑将在路由中处理 // 导航到编辑页面的逻辑将在路由中处理
window.location.hash = '#/editor'; window.location.hash = '#/editor'
}; }
const handleDeleteNote = (noteId) => { const handleDeleteNote = noteId => {
noteToDelete.value = noteId; noteToDelete.value = noteId
showAlert.value = true; showAlert.value = true
}; }
const confirmDeleteNote = () => { const confirmDeleteNote = () => {
if (noteToDelete.value) { if (noteToDelete.value) {
store.deleteNote(noteToDelete.value); store.deleteNote(noteToDelete.value)
noteToDelete.value = null; noteToDelete.value = null
} }
showAlert.value = false; showAlert.value = false
}; }
const handleSort = () => { const handleSort = () => {
// In a full implementation, this would cycle through sort options // In a full implementation, this would cycle through sort options
const sortOptions = ['date', 'title', 'starred']; const sortOptions = ['date', 'title', 'starred']
const currentIndex = sortOptions.indexOf(sortBy.value); const currentIndex = sortOptions.indexOf(sortBy.value)
const nextIndex = (currentIndex + 1) % sortOptions.length; const nextIndex = (currentIndex + 1) % sortOptions.length
sortBy.value = sortOptions[nextIndex]; sortBy.value = sortOptions[nextIndex]
console.log('Sort by:', sortOptions[nextIndex]); console.log('Sort by:', sortOptions[nextIndex])
}; }
const handleFolderPress = () => { const handleFolderPress = () => {
// 导航到文件夹页面的逻辑将在路由中处理 // 导航到文件夹页面的逻辑将在路由中处理
window.location.hash = '#/folders'; window.location.hash = '#/folders'
}; }
const handleSettingsPress = () => { const handleSettingsPress = () => {
// 导航到设置页面的逻辑将在路由中处理 // 导航到设置页面的逻辑将在路由中处理
window.location.hash = '#/settings'; window.location.hash = '#/settings'
}; }
const handleFolderToggle = () => { const handleFolderToggle = () => {
// 在实际应用中,这里会触发文件夹列表的展开/收起 // 在实际应用中,这里会触发文件夹列表的展开/收起
isFolderExpanded.value = !isFolderExpanded.value; isFolderExpanded.value = !isFolderExpanded.value
console.log('Folder expanded:', !isFolderExpanded.value); console.log('Folder expanded:', !isFolderExpanded.value)
}; }
const handleSearch = () => { const handleSearch = query => {
// In a full implementation, this would filter notes based on searchQuery // 搜索功能已在computed属性filteredAndSortedNotes中实现
console.log('Search for:', searchQuery.value); console.log('Search for:', query)
}; }
const formatDate = (dateString) => { const handleClearSearch = () => {
return new Date(dateString).toLocaleDateString(); // 清除搜索已在v-model中处理
}; console.log('Search cleared')
}
const setCurrentFolder = (folder) => { const handleSearchFocus = () => {
currentFolder.value = folder; console.log('Search bar focused')
}; }
const setIsFolderExpanded = (expanded) => { const handleSearchBlur = () => {
isFolderExpanded.value = expanded; console.log('Search bar blurred')
}; }
const setSearchQuery = (query) => { const formatDate = dateString => {
searchQuery.value = query; return new Date(dateString).toLocaleDateString()
}; }
const setShowAlert = (show) => { const setCurrentFolder = folder => {
showAlert.value = show; currentFolder.value = folder
}; }
const notes = computed(() => store.notes); const setIsFolderExpanded = expanded => {
isFolderExpanded.value = expanded
}
const setSearchQuery = query => {
searchQuery.value = query
}
const setShowAlert = show => {
showAlert.value = show
}
const notes = computed(() => store.notes)
</script> </script>
<style lang="less" scoped>
.container {
width: 100vw;
height: 100vh;
background: url(assets/icons/drawable-xxhdpi/note_background.png);
background-size: cover;
}
</style>