feat: 扩展 AI 记账能力并优化收支记录交互
- AI 能力:新增分类 Emoji 推荐、标签颜色推荐、自然语言生成分类/标签、单条记录分类推荐 - AI 批量建议:按类型+支付方式+备注去重聚合后分批调用,支持分类/标签/两者范围并记忆偏好,结果可勾选应用 - 分类/标签管理:新增 EmojiPicker 与预设色板,支持 AI 一键推荐并在列表可视化 - 收支记录:新增序号列,标签筛选支持无标签,编辑记录时允许不设置分类 - 修复:账单导入未选择默认标签时生成空 IN 列表导致的 SQL 语法错误 - AI 未启用时隐藏所有 AI 入口与弹窗
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
<script setup lang="ts">
|
||||
// Emoji 选择器组件:提供常用 Emoji 分组快速点选与自定义文本输入,供分类图标等场景复用
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { NPopover, NInput, NButton, NScrollbar } from 'naive-ui'
|
||||
|
||||
/** 组件属性 */
|
||||
const props = withDefaults(defineProps<{
|
||||
/** 当前选中的 Emoji 或图标文本 */
|
||||
modelValue?: string
|
||||
/** 未选择时的占位提示 */
|
||||
placeholder?: string
|
||||
/** 自定义输入框的占位提示 */
|
||||
inputPlaceholder?: string
|
||||
/** 清除按钮文案 */
|
||||
clearText?: string
|
||||
}>(), {
|
||||
modelValue: '',
|
||||
placeholder: '',
|
||||
inputPlaceholder: '',
|
||||
clearText: '',
|
||||
})
|
||||
|
||||
const emit = defineEmits<{ 'update:modelValue': [value: string] }>()
|
||||
|
||||
/** Emoji 分组数据,按常见记账场景归类 */
|
||||
const EMOJI_GROUPS: { key: string; label: string; emojis: string[] }[] = [
|
||||
{ key: 'dining', label: '餐饮', emojis: ['🍚', '🍜', '🍔', '🍕', '🍰', '☕', '🍺', '🍎', '🥤', '🍦', '🍟', '🍣', '🥗', '🍲'] },
|
||||
{ key: 'transport', label: '交通', emojis: ['🚗', '🚕', '🚌', '🚇', '🚄', '✈️', '🚲', '⛽', '🅿️', '🛵', '🚢', '🚝', '🚦', '🛺'] },
|
||||
{ key: 'shopping', label: '购物', emojis: ['🛒', '👕', '👟', '👜', '💄', '📱', '💻', '🎁', '🧴', '👓', '⌚', '🎧', '💍', '🧢'] },
|
||||
{ key: 'home', label: '居家', emojis: ['🏠', '💡', '💧', '🔥', '🛋️', '🧹', '🧺', '🪑', '🚿', '🛏️', '📺', '🧻', '🪴', '🔌'] },
|
||||
{ key: 'fun', label: '娱乐', emojis: ['🎮', '🎬', '🎵', '⚽', '🏀', '🎤', '🎨', '📷', '🎳', '🏋️', '🎯', '🎪', '🎫', '🎲'] },
|
||||
{ key: 'health', label: '医疗', emojis: ['💊', '🏥', '🩺', '💉', '🦷', '👶', '🧘', '🩹', '🩻', '🧑⚕️', '🏃', '😴'] },
|
||||
{ key: 'study', label: '学习', emojis: ['📚', '✏️', '🎓', '🖊️', '📝', '🧑🏫', '📖', '🔬', '🗂️', '📐', '🖥️', '📅'] },
|
||||
{ key: 'finance', label: '收支', emojis: ['💰', '💵', '💳', '🏦', '📈', '📉', '🧾', '🪙', '💴', '💶', '💷', '🤑', '🧧', '🏧'] },
|
||||
{ key: 'other', label: '其他', emojis: ['📦', '🧳', '🐱', '🐶', '🌱', '🎉', '❤️', '⭐', '🔧', '🧸', '🍀', '🌈', '🕒', '📌'] },
|
||||
]
|
||||
|
||||
const show = ref(false) // 选择面板显示状态
|
||||
const keyword = ref(props.modelValue || '') // 自定义输入内容,与选中值同步
|
||||
|
||||
// 外部值变化时同步到输入框,保证编辑回填正确
|
||||
watch(() => props.modelValue, (val) => {
|
||||
if ((val || '') !== keyword.value) keyword.value = val || ''
|
||||
})
|
||||
|
||||
/** 触发框展示文本:优先显示已选图标,否则显示占位提示 */
|
||||
const displayText = computed(() => props.modelValue || props.placeholder)
|
||||
|
||||
/** 自定义输入变化时实时回写选中值 */
|
||||
function handleInput(value: string) {
|
||||
keyword.value = value
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
|
||||
/** 点选预设 Emoji:回写并关闭面板 */
|
||||
function handleSelect(emoji: string) {
|
||||
keyword.value = emoji
|
||||
emit('update:modelValue', emoji)
|
||||
show.value = false
|
||||
}
|
||||
|
||||
/** 清除已选图标 */
|
||||
function handleClear() {
|
||||
keyword.value = ''
|
||||
emit('update:modelValue', '')
|
||||
show.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NPopover v-model:show="show" trigger="click" placement="bottom-start" :width="360">
|
||||
<template #trigger>
|
||||
<div class="emoji-trigger" :class="{ 'is-empty': !props.modelValue }">
|
||||
<span class="emoji-current">{{ displayText }}</span>
|
||||
<span class="emoji-arrow">▾</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="emoji-panel">
|
||||
<!-- 自定义输入:可直接输入任意 Emoji 或文字,兼容旧的纯文本图标 -->
|
||||
<NInput
|
||||
:value="keyword"
|
||||
size="small"
|
||||
clearable
|
||||
:placeholder="props.inputPlaceholder"
|
||||
@update:value="handleInput"
|
||||
/>
|
||||
|
||||
<NScrollbar style="max-height: 260px; margin-top: 8px;">
|
||||
<div v-for="group in EMOJI_GROUPS" :key="group.key">
|
||||
<div class="emoji-group-label">{{ group.label }}</div>
|
||||
<div class="emoji-grid">
|
||||
<button
|
||||
v-for="emoji in group.emojis"
|
||||
:key="emoji"
|
||||
type="button"
|
||||
class="emoji-item"
|
||||
:class="{ active: emoji === props.modelValue }"
|
||||
@click="handleSelect(emoji)"
|
||||
>
|
||||
{{ emoji }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</NScrollbar>
|
||||
|
||||
<div class="emoji-footer">
|
||||
<NButton size="tiny" quaternary @click="handleClear">{{ props.clearText }}</NButton>
|
||||
</div>
|
||||
</div>
|
||||
</NPopover>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.emoji-trigger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
height: 34px;
|
||||
padding: 0 12px;
|
||||
box-sizing: border-box;
|
||||
background: #fff;
|
||||
border: 1px solid #e0e0e6;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.emoji-trigger:hover {
|
||||
border-color: #36ad6a;
|
||||
}
|
||||
|
||||
.emoji-current {
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.emoji-trigger.is-empty .emoji-current {
|
||||
font-size: 14px;
|
||||
color: #c2c2c2;
|
||||
}
|
||||
|
||||
.emoji-arrow {
|
||||
margin-left: 8px;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.emoji-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(8, 1fr);
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.emoji-item {
|
||||
padding: 4px 0;
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.emoji-item:hover {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.emoji-item.active {
|
||||
background: #e6f7ec;
|
||||
}
|
||||
|
||||
.emoji-group-label {
|
||||
margin: 8px 0 4px;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.emoji-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user