Compare commits
4 Commits
768c72d7b7
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 324a3ec5c9 | |||
| 9f5802d634 | |||
| f586f9e117 | |||
| 6b26951b1b |
@@ -128,60 +128,54 @@ defineExpose({ focus, blur })
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 金额显示区 — 未聚焦时低调,聚焦时粘土卡片浮起
|
||||||
.amount-display {
|
.amount-display {
|
||||||
min-height: 156rpx;
|
|
||||||
padding: $space-lg $space-xl;
|
|
||||||
background: $surface;
|
|
||||||
border: 2rpx solid $border;
|
|
||||||
border-radius: $radius-2xl;
|
|
||||||
box-shadow: $shadow-clay;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: baseline;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: $space-xs;
|
gap: 8rpx;
|
||||||
|
padding: $space-lg $space-xl $space-md;
|
||||||
|
border-radius: $radius-xl;
|
||||||
|
border: 2rpx solid transparent;
|
||||||
|
background: transparent;
|
||||||
|
transition: background $transition-normal, border-color $transition-normal, box-shadow $transition-normal;
|
||||||
-webkit-tap-highlight-color: transparent;
|
-webkit-tap-highlight-color: transparent;
|
||||||
transition: border-color $transition-normal, box-shadow $transition-normal, transform $transition-fast;
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
transform: scale(0.99);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.amount-editor--focused .amount-display {
|
.amount-editor--focused .amount-display {
|
||||||
border-color: $primary-light;
|
background: $surface;
|
||||||
box-shadow: $shadow-clay, 0 8rpx 24rpx rgba(255, 140, 105, 0.16);
|
border-color: $border;
|
||||||
|
box-shadow: $shadow-clay;
|
||||||
}
|
}
|
||||||
|
|
||||||
.currency,
|
// ¥ 符号
|
||||||
|
.currency {
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-family: 'Fredoka', sans-serif;
|
||||||
|
font-weight: 500;
|
||||||
|
color: $text-muted;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 单位(元)
|
||||||
.unit {
|
.unit {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
color: $text-sec;
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
color: $text-sec;
|
||||||
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.currency {
|
// 金额字符容器
|
||||||
align-self: flex-start;
|
|
||||||
margin-top: 12rpx;
|
|
||||||
font-family: 'Fredoka', sans-serif;
|
|
||||||
font-size: 40rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.unit {
|
|
||||||
align-self: flex-end;
|
|
||||||
margin-bottom: 14rpx;
|
|
||||||
font-size: $font-md;
|
|
||||||
}
|
|
||||||
|
|
||||||
.amount-value {
|
.amount-value {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: baseline;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
font-family: 'Fredoka', sans-serif;
|
font-family: 'Fredoka', sans-serif;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: $text;
|
color: $text;
|
||||||
line-height: 1.1;
|
line-height: 1.2;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,7 +183,6 @@ defineExpose({ focus, blur })
|
|||||||
.placeholder {
|
.placeholder {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
min-height: 1.1em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.amount-char {
|
.amount-char {
|
||||||
@@ -197,9 +190,10 @@ defineExpose({ focus, blur })
|
|||||||
}
|
}
|
||||||
|
|
||||||
.placeholder {
|
.placeholder {
|
||||||
color: $text-muted;
|
color: #D0C4C4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 光标
|
||||||
.cursor {
|
.cursor {
|
||||||
width: 4rpx;
|
width: 4rpx;
|
||||||
height: 1em;
|
height: 1em;
|
||||||
@@ -209,11 +203,11 @@ defineExpose({ focus, blur })
|
|||||||
animation: cursor-blink 1s steps(2, start) infinite;
|
animation: cursor-blink 1s steps(2, start) infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== 尺寸变体 =====
|
||||||
|
|
||||||
.amount-editor--small {
|
.amount-editor--small {
|
||||||
.amount-display {
|
.amount-display {
|
||||||
min-height: 112rpx;
|
|
||||||
padding: $space-md $space-lg;
|
padding: $space-md $space-lg;
|
||||||
border-radius: $radius-xl;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.amount-value {
|
.amount-value {
|
||||||
@@ -221,14 +215,17 @@ defineExpose({ focus, blur })
|
|||||||
}
|
}
|
||||||
|
|
||||||
.currency {
|
.currency {
|
||||||
margin-top: 6rpx;
|
|
||||||
font-size: 28rpx;
|
font-size: 28rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.unit {
|
||||||
|
font-size: $font-sm;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.amount-editor--medium {
|
.amount-editor--medium {
|
||||||
.amount-display {
|
.amount-display {
|
||||||
min-height: 132rpx;
|
padding: $space-md $space-xl;
|
||||||
}
|
}
|
||||||
|
|
||||||
.amount-value {
|
.amount-value {
|
||||||
@@ -238,12 +235,24 @@ defineExpose({ focus, blur })
|
|||||||
.currency {
|
.currency {
|
||||||
font-size: 34rpx;
|
font-size: 34rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.unit {
|
||||||
|
font-size: $font-md;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.amount-editor--large {
|
.amount-editor--large {
|
||||||
.amount-value {
|
.amount-value {
|
||||||
font-size: 88rpx;
|
font-size: 88rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.currency {
|
||||||
|
font-size: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unit {
|
||||||
|
font-size: $font-lg;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes cursor-blink {
|
@keyframes cursor-blink {
|
||||||
|
|||||||
@@ -48,9 +48,19 @@ export function insertAmountKey(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const normalizedInsert = normalizeInsert(value, safeCursor, key)
|
const normalizedInsert = normalizeInsert(value, safeCursor, key)
|
||||||
const next = value.slice(0, safeCursor) + normalizedInsert + value.slice(safeCursor)
|
let next = value.slice(0, safeCursor) + normalizedInsert + value.slice(safeCursor)
|
||||||
|
|
||||||
if (!isValidAmountInput(next, options)) {
|
if (!isValidAmountInput(next, options)) {
|
||||||
|
// 小数位超出上限时,从右侧挤出一个字符(小数部分左移)
|
||||||
|
const dotIdx = next.indexOf('.')
|
||||||
|
if (dotIdx >= 0 && next.length - dotIdx - 1 > MAX_DECIMAL_PLACES) {
|
||||||
|
next = next.slice(0, -1)
|
||||||
|
const newCursor = Math.min(safeCursor + normalizedInsert.length, next.length)
|
||||||
|
if (!isValidAmountInput(next, options)) {
|
||||||
|
return { value, cursorIndex: safeCursor, accepted: false }
|
||||||
|
}
|
||||||
|
return { value: next, cursorIndex: newCursor, accepted: true }
|
||||||
|
}
|
||||||
return { value, cursorIndex: safeCursor, accepted: false }
|
return { value, cursorIndex: safeCursor, accepted: false }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,8 @@
|
|||||||
:fixed="true"
|
:fixed="true"
|
||||||
size="large"
|
size="large"
|
||||||
:auto-focus="true"
|
:auto-focus="true"
|
||||||
|
@focus="keyboardVisible = true"
|
||||||
|
@blur="keyboardVisible = false"
|
||||||
@confirm="save"
|
@confirm="save"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -55,7 +57,12 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<!-- 底部保存按钮(键盘收起时显示) -->
|
||||||
|
<view class="save-bar" v-if="!showSuccess && !keyboardVisible">
|
||||||
|
<view class="save-btn" @tap="save">
|
||||||
|
<text class="save-btn-text">{{ editId ? '保存修改' : '保存' }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
<SaveSuccess
|
<SaveSuccess
|
||||||
v-model:visible="showSuccess"
|
v-model:visible="showSuccess"
|
||||||
@@ -99,6 +106,8 @@ const showSuccess = ref(false)
|
|||||||
const scrollToCat = ref('')
|
const scrollToCat = ref('')
|
||||||
const todayStr = localToday
|
const todayStr = localToday
|
||||||
const amountEditorRef = ref<InstanceType<typeof AmountEditor> | null>(null)
|
const amountEditorRef = ref<InstanceType<typeof AmountEditor> | null>(null)
|
||||||
|
const keyboardVisible = ref(false)
|
||||||
|
|
||||||
// Remember last selected category per type
|
// Remember last selected category per type
|
||||||
const lastCatByType: Record<string, number | null> = { expense: null, income: null }
|
const lastCatByType: Record<string, number | null> = { expense: null, income: null }
|
||||||
|
|
||||||
@@ -195,6 +204,7 @@ onMounted(async () => {
|
|||||||
// 等待分类列表渲染完成后滚动到选中项
|
// 等待分类列表渲染完成后滚动到选中项
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
setTimeout(() => { scrollToCat.value = 'cat-' + existing.category_id }, 200)
|
setTimeout(() => { scrollToCat.value = 'cat-' + existing.category_id }, 200)
|
||||||
|
amountEditorRef.value?.focus()
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
uni.showToast({ title: '记录不存在', icon: 'none' })
|
uni.showToast({ title: '记录不存在', icon: 'none' })
|
||||||
@@ -300,9 +310,46 @@ function goBack() {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
// AmountEditor fixed Numpad 占位: 4行×96rpx + 3间隙×16rpx + padding×2×16rpx = 464rpx
|
// AmountEditor fixed Numpad 占位: 4行×96rpx + 3间隙×16rpx + padding×2×16rpx = 464rpx
|
||||||
|
// 同时留出保存按钮的位置,确保内容不被遮挡
|
||||||
$numpad-h: 464rpx;
|
$numpad-h: 464rpx;
|
||||||
padding-bottom: calc(#{$numpad-h} + constant(safe-area-inset-bottom));
|
$save-bar-h: 144rpx;
|
||||||
padding-bottom: calc(#{$numpad-h} + env(safe-area-inset-bottom));
|
padding-bottom: calc(#{$numpad-h} + #{$save-bar-h} + constant(safe-area-inset-bottom));
|
||||||
|
padding-bottom: calc(#{$numpad-h} + #{$save-bar-h} + env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 底部保存按钮栏
|
||||||
|
.save-bar {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
padding: $space-md $space-xl;
|
||||||
|
padding-bottom: calc(#{$space-md} + constant(safe-area-inset-bottom));
|
||||||
|
padding-bottom: calc(#{$space-md} + env(safe-area-inset-bottom));
|
||||||
|
background: $surface;
|
||||||
|
border-top: 2rpx solid $border;
|
||||||
|
z-index: 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-btn {
|
||||||
|
height: 96rpx;
|
||||||
|
background: linear-gradient(135deg, $primary, #E67355);
|
||||||
|
border-radius: $radius-xl;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 4rpx 12rpx rgba(255, 140, 105, 0.3);
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
opacity: 0.8;
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-btn-text {
|
||||||
|
font-size: $font-xl;
|
||||||
|
font-weight: 600;
|
||||||
|
color: $surface;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-fixed {
|
.header-fixed {
|
||||||
|
|||||||
@@ -607,4 +607,9 @@ function goBack() { uni.navigateBack() }
|
|||||||
.skip & { color: $text-sec; }
|
.skip & { color: $text-sec; }
|
||||||
.confirm & { color: $surface; }
|
.confirm & { color: $surface; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal-mask {
|
||||||
|
@include modal-mask;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -61,13 +61,19 @@
|
|||||||
<view class="query-section">
|
<view class="query-section">
|
||||||
<view class="query-header">
|
<view class="query-header">
|
||||||
<text class="section-title">事件记录</text>
|
<text class="section-title">事件记录</text>
|
||||||
<scroll-view class="filter-tabs" scroll-x>
|
<scroll-view scroll-x>
|
||||||
|
<view class="filter-tabs">
|
||||||
<view class="filter-tab" :class="{ active: !filterEvent }" @tap="filterEvent = ''">全部</view>
|
<view class="filter-tab" :class="{ active: !filterEvent }" @tap="filterEvent = ''">全部</view>
|
||||||
<view class="filter-tab" :class="{ active: filterEvent === 'page_view' }" @tap="filterEvent = 'page_view'">页面</view>
|
<view class="filter-tab" :class="{ active: filterEvent === 'page_view' }" @tap="filterEvent = 'page_view'">
|
||||||
<view class="filter-tab" :class="{ active: filterEvent === 'action' }" @tap="filterEvent = 'action'">操作</view>
|
页面</view>
|
||||||
|
<view class="filter-tab" :class="{ active: filterEvent === 'action' }" @tap="filterEvent = 'action'">操作
|
||||||
|
</view>
|
||||||
<view class="filter-tab" :class="{ active: filterEvent === 'error' }" @tap="filterEvent = 'error'">错误</view>
|
<view class="filter-tab" :class="{ active: filterEvent === 'error' }" @tap="filterEvent = 'error'">错误</view>
|
||||||
<view class="filter-tab" :class="{ active: filterEvent === 'api_error' }" @tap="filterEvent = 'api_error'">API错误</view>
|
<view class="filter-tab" :class="{ active: filterEvent === 'api_error' }" @tap="filterEvent = 'api_error'">
|
||||||
<view class="filter-tab" :class="{ active: filterEvent === 'app_launch' }" @tap="filterEvent = 'app_launch'">启动</view>
|
API错误</view>
|
||||||
|
<view class="filter-tab" :class="{ active: filterEvent === 'app_launch' }"
|
||||||
|
@tap="filterEvent = 'app_launch'">启动</view>
|
||||||
|
</view>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
</view>
|
</view>
|
||||||
<view class="filter-row">
|
<view class="filter-row">
|
||||||
@@ -95,7 +101,8 @@
|
|||||||
</view>
|
</view>
|
||||||
<text class="record-page" v-if="item.page">{{ item.page }}</text>
|
<text class="record-page" v-if="item.page">{{ item.page }}</text>
|
||||||
<text class="record-user" v-if="item.nickname">{{ item.nickname }}</text>
|
<text class="record-user" v-if="item.nickname">{{ item.nickname }}</text>
|
||||||
<text class="record-data" v-if="item.data && Object.keys(item.data).length > 0">{{ JSON.stringify(item.data) }}</text>
|
<text class="record-data" v-if="item.data && Object.keys(item.data).length > 0">{{ JSON.stringify(item.data)
|
||||||
|
}}</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view v-if="loading" class="loading-box">
|
<view v-if="loading" class="loading-box">
|
||||||
@@ -151,7 +158,7 @@ onPullDownRefresh(async () => {
|
|||||||
async function loadStats() {
|
async function loadStats() {
|
||||||
try {
|
try {
|
||||||
stats.value = await getTrackStats()
|
stats.value = await getTrackStats()
|
||||||
} catch {}
|
} catch { }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadEvents(append = false) {
|
async function loadEvents(append = false) {
|
||||||
@@ -274,7 +281,9 @@ function goBack() {
|
|||||||
@include sticky-header;
|
@include sticky-header;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-bar { @include status-bar; }
|
.status-bar {
|
||||||
|
@include status-bar;
|
||||||
|
}
|
||||||
|
|
||||||
.nav-bar {
|
.nav-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -282,7 +291,9 @@ function goBack() {
|
|||||||
padding: $space-sm $space-xl $space-md;
|
padding: $space-sm $space-xl $space-md;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-back { @include nav-back; }
|
.nav-back {
|
||||||
|
@include nav-back;
|
||||||
|
}
|
||||||
|
|
||||||
.nav-title {
|
.nav-title {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -292,7 +303,9 @@ function goBack() {
|
|||||||
color: $text;
|
color: $text;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-placeholder { width: 88rpx; }
|
.nav-placeholder {
|
||||||
|
width: 88rpx;
|
||||||
|
}
|
||||||
|
|
||||||
.stats-card {
|
.stats-card {
|
||||||
margin: $space-md $space-xl;
|
margin: $space-md $space-xl;
|
||||||
@@ -333,7 +346,9 @@ function goBack() {
|
|||||||
gap: $space-sm;
|
gap: $space-sm;
|
||||||
padding: $space-xs 0;
|
padding: $space-xs 0;
|
||||||
|
|
||||||
&:active { opacity: 0.7; }
|
&:active {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.event-tag {
|
.event-tag {
|
||||||
@@ -341,18 +356,34 @@ function goBack() {
|
|||||||
border-radius: $radius-xs;
|
border-radius: $radius-xs;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|
||||||
&.page { background: #E3F2FD; }
|
&.page {
|
||||||
&.action { background: #E8F5E9; }
|
background: #E3F2FD;
|
||||||
&.error { background: #FFEBEE; }
|
}
|
||||||
|
|
||||||
|
&.action {
|
||||||
|
background: #E8F5E9;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.error {
|
||||||
|
background: #FFEBEE;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.event-tag-text {
|
.event-tag-text {
|
||||||
font-size: $font-sm;
|
font-size: $font-sm;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
|
||||||
.page & { color: #1976D2; }
|
.page & {
|
||||||
.action & { color: #388E3C; }
|
color: #1976D2;
|
||||||
.error & { color: #D32F2F; }
|
}
|
||||||
|
|
||||||
|
.action & {
|
||||||
|
color: #388E3C;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error & {
|
||||||
|
color: #D32F2F;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.event-bar {
|
.event-bar {
|
||||||
@@ -472,7 +503,9 @@ function goBack() {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
&:active { opacity: 0.8; }
|
&:active {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-btn-text {
|
.search-btn-text {
|
||||||
@@ -491,7 +524,6 @@ function goBack() {
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: $space-xs;
|
gap: $space-xs;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
width: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-tab {
|
.filter-tab {
|
||||||
@@ -520,7 +552,9 @@ function goBack() {
|
|||||||
padding: $space-md;
|
padding: $space-md;
|
||||||
border-bottom: 1rpx solid $border;
|
border-bottom: 1rpx solid $border;
|
||||||
|
|
||||||
&:last-child { border-bottom: none; }
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.record-header {
|
.record-header {
|
||||||
@@ -534,18 +568,34 @@ function goBack() {
|
|||||||
padding: 2rpx 12rpx;
|
padding: 2rpx 12rpx;
|
||||||
border-radius: $radius-xs;
|
border-radius: $radius-xs;
|
||||||
|
|
||||||
&.page { background: #E3F2FD; }
|
&.page {
|
||||||
&.action { background: #E8F5E9; }
|
background: #E3F2FD;
|
||||||
&.error { background: #FFEBEE; }
|
}
|
||||||
|
|
||||||
|
&.action {
|
||||||
|
background: #E8F5E9;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.error {
|
||||||
|
background: #FFEBEE;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.record-event-text {
|
.record-event-text {
|
||||||
font-size: $font-xs;
|
font-size: $font-xs;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
|
||||||
.page & { color: #1976D2; }
|
.page & {
|
||||||
.action & { color: #388E3C; }
|
color: #1976D2;
|
||||||
.error & { color: #D32F2F; }
|
}
|
||||||
|
|
||||||
|
.action & {
|
||||||
|
color: #388E3C;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error & {
|
||||||
|
color: #D32F2F;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.record-time {
|
.record-time {
|
||||||
@@ -578,7 +628,8 @@ function goBack() {
|
|||||||
border-radius: $radius-sm;
|
border-radius: $radius-sm;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-box, .empty-box {
|
.loading-box,
|
||||||
|
.empty-box {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -601,6 +652,8 @@ function goBack() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
.bar-fill { transition: none; }
|
.bar-fill {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -242,7 +242,7 @@ async function onDelete(item: any) {
|
|||||||
|
|
||||||
// 触底加载更多
|
// 触底加载更多
|
||||||
onReachBottom(() => {
|
onReachBottom(() => {
|
||||||
if (noMore.value || loading) return
|
if (noMore.value || loading.value) return
|
||||||
page.value++
|
page.value++
|
||||||
loadTx(false)
|
loadTx(false)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -66,10 +66,19 @@
|
|||||||
:fixed="true"
|
:fixed="true"
|
||||||
size="large"
|
size="large"
|
||||||
unit="元"
|
unit="元"
|
||||||
:auto-focus="true"
|
:auto-focus="false"
|
||||||
|
@focus="keyboardVisible = true"
|
||||||
|
@blur="keyboardVisible = false"
|
||||||
@confirm="onConfirm"
|
@confirm="onConfirm"
|
||||||
/>
|
/>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<!-- 底部保存按钮(键盘收起时显示) -->
|
||||||
|
<view class="save-bar" v-if="!keyboardVisible">
|
||||||
|
<view class="save-btn" @tap="onConfirm">
|
||||||
|
<text class="save-btn-text">保存预算</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -98,6 +107,7 @@ const loading = ref(true)
|
|||||||
const initialLoaded = ref(false)
|
const initialLoaded = ref(false)
|
||||||
const amountStr = ref('')
|
const amountStr = ref('')
|
||||||
const amountEditorRef = ref<InstanceType<typeof AmountEditor> | null>(null)
|
const amountEditorRef = ref<InstanceType<typeof AmountEditor> | null>(null)
|
||||||
|
const keyboardVisible = ref(false)
|
||||||
|
|
||||||
const presets = [1000, 2000, 3000, 5000, 10000]
|
const presets = [1000, 2000, 3000, 5000, 10000]
|
||||||
|
|
||||||
@@ -197,7 +207,11 @@ function goBack() {
|
|||||||
.page {
|
.page {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: $bg;
|
background: $bg;
|
||||||
padding-bottom: 120rpx;
|
// AmountEditor fixed Numpad 占位 + 保存按钮占位
|
||||||
|
$numpad-h: 464rpx;
|
||||||
|
$save-bar-h: 144rpx;
|
||||||
|
padding-bottom: calc(#{$numpad-h} + #{$save-bar-h} + constant(safe-area-inset-bottom));
|
||||||
|
padding-bottom: calc(#{$numpad-h} + #{$save-bar-h} + env(safe-area-inset-bottom));
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-fixed {
|
.header-fixed {
|
||||||
@@ -369,6 +383,40 @@ function goBack() {
|
|||||||
color: $text;
|
color: $text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 底部保存按钮栏
|
||||||
|
.save-bar {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
padding: $space-md $space-xl;
|
||||||
|
padding-bottom: calc(#{$space-md} + constant(safe-area-inset-bottom));
|
||||||
|
padding-bottom: calc(#{$space-md} + env(safe-area-inset-bottom));
|
||||||
|
background: $surface;
|
||||||
|
border-top: 2rpx solid $border;
|
||||||
|
z-index: 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-btn {
|
||||||
|
height: 96rpx;
|
||||||
|
background: linear-gradient(135deg, $primary, #E67355);
|
||||||
|
border-radius: $radius-xl;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 4rpx 12rpx rgba(255, 140, 105, 0.3);
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
opacity: 0.8;
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-btn-text {
|
||||||
|
font-size: $font-xl;
|
||||||
|
font-weight: 600;
|
||||||
|
color: $surface;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
|||||||
@@ -153,7 +153,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, nextTick } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { waitForReady } from '@/utils/app-ready'
|
import { waitForReady } from '@/utils/app-ready'
|
||||||
import { statusBarHeight, capsuleRight } from '@/utils/system'
|
import { statusBarHeight, capsuleRight } from '@/utils/system'
|
||||||
import { formatAmount } from '@/utils/format'
|
import { formatAmount } from '@/utils/format'
|
||||||
@@ -233,7 +233,6 @@ function openAddModal() {
|
|||||||
if (cats.length > 0) form.value.category_id = cats[0].id
|
if (cats.length > 0) form.value.category_id = cats[0].id
|
||||||
amountStr.value = ''
|
amountStr.value = ''
|
||||||
showModal.value = true
|
showModal.value = true
|
||||||
nextTick(() => amountEditorRef.value?.focus())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function openEditModal(item: RecurringTemplate) {
|
function openEditModal(item: RecurringTemplate) {
|
||||||
@@ -248,7 +247,6 @@ function openEditModal(item: RecurringTemplate) {
|
|||||||
}
|
}
|
||||||
amountStr.value = (item.amount / 100).toString()
|
amountStr.value = (item.amount / 100).toString()
|
||||||
showModal.value = true
|
showModal.value = true
|
||||||
nextTick(() => amountEditorRef.value?.focus())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function toggleActive(item: RecurringTemplate) {
|
async function toggleActive(item: RecurringTemplate) {
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export const useGroupStore = defineStore('group', () => {
|
|||||||
if (currentGroupId.value !== null) {
|
if (currentGroupId.value !== null) {
|
||||||
const stillValid = groups.value.some(g => g.id === currentGroupId.value)
|
const stillValid = groups.value.some(g => g.id === currentGroupId.value)
|
||||||
if (!stillValid) {
|
if (!stillValid) {
|
||||||
switchToPersonal()
|
await switchToPersonal()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
@@ -83,7 +83,7 @@ export const useGroupStore = defineStore('group', () => {
|
|||||||
async function leaveGroup(groupId: number) {
|
async function leaveGroup(groupId: number) {
|
||||||
await api.leaveGroup(groupId)
|
await api.leaveGroup(groupId)
|
||||||
if (currentGroupId.value === groupId) {
|
if (currentGroupId.value === groupId) {
|
||||||
switchToPersonal()
|
await switchToPersonal()
|
||||||
}
|
}
|
||||||
await fetchGroups()
|
await fetchGroups()
|
||||||
}
|
}
|
||||||
@@ -92,7 +92,7 @@ export const useGroupStore = defineStore('group', () => {
|
|||||||
async function deleteGroup(groupId: number) {
|
async function deleteGroup(groupId: number) {
|
||||||
await api.deleteGroup(groupId)
|
await api.deleteGroup(groupId)
|
||||||
if (currentGroupId.value === groupId) {
|
if (currentGroupId.value === groupId) {
|
||||||
switchToPersonal()
|
await switchToPersonal()
|
||||||
}
|
}
|
||||||
await fetchGroups()
|
await fetchGroups()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,11 @@ export function formatDate(date: string | Date): string {
|
|||||||
if (diff === 1) return '昨天'
|
if (diff === 1) return '昨天'
|
||||||
if (diff === 2) return '前天'
|
if (diff === 2) return '前天'
|
||||||
|
|
||||||
return `${d.getMonth() + 1}月${d.getDate()}日`
|
// 跨年显示年份
|
||||||
|
const isCurrentYear = d.getFullYear() === now.getFullYear()
|
||||||
|
return isCurrentYear
|
||||||
|
? `${d.getMonth() + 1}月${d.getDate()}日`
|
||||||
|
: `${d.getFullYear()}年${d.getMonth() + 1}月${d.getDate()}日`
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatMonth(date: string): string {
|
export function formatMonth(date: string): string {
|
||||||
|
|||||||
@@ -124,9 +124,9 @@ router.post('/:id/migrate', async (req: AuthRequest, res: Response) => {
|
|||||||
|
|
||||||
await conn.beginTransaction()
|
await conn.beginTransaction()
|
||||||
|
|
||||||
// 验证源分类存在且属于当前用户
|
// 验证源分类存在且属于当前用户(仅允许迁移自定义分类)
|
||||||
const [sourceRows] = await conn.query(
|
const [sourceRows] = await conn.query(
|
||||||
'SELECT id, type FROM categories WHERE id = ? AND (user_id = 0 OR user_id = ?)',
|
'SELECT id, type FROM categories WHERE id = ? AND is_custom = 1 AND user_id = ?',
|
||||||
[req.params.id, req.userId]
|
[req.params.id, req.userId]
|
||||||
)
|
)
|
||||||
const sourceCategory = (sourceRows as any[])[0]
|
const sourceCategory = (sourceRows as any[])[0]
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ const upload = multer({
|
|||||||
|
|
||||||
/** 公告编辑权限检查(管理员可编辑所有公告,群主可编辑自己群组的公告) */
|
/** 公告编辑权限检查(管理员可编辑所有公告,群主可编辑自己群组的公告) */
|
||||||
async function requireNotificationEditAuth(req: AuthRequest, res: Response, next: NextFunction) {
|
async function requireNotificationEditAuth(req: AuthRequest, res: Response, next: NextFunction) {
|
||||||
|
try {
|
||||||
const [rows] = await pool.query(
|
const [rows] = await pool.query(
|
||||||
'SELECT type, group_id FROM notifications WHERE id = ?',
|
'SELECT type, group_id FROM notifications WHERE id = ?',
|
||||||
[req.params.id]
|
[req.params.id]
|
||||||
@@ -75,6 +76,10 @@ async function requireNotificationEditAuth(req: AuthRequest, res: Response, next
|
|||||||
}
|
}
|
||||||
|
|
||||||
return res.status(403).json({ code: 40300, message: '需要管理员或群主权限' })
|
return res.status(403).json({ code: 40300, message: '需要管理员或群主权限' })
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Notification] editAuth error:', err)
|
||||||
|
return res.status(500).json({ code: 50000, message: '服务器错误' })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 获取通知列表 */
|
/** 获取通知列表 */
|
||||||
@@ -283,8 +288,8 @@ function isValidUrl(url: string): boolean {
|
|||||||
const parsed = new URL(url)
|
const parsed = new URL(url)
|
||||||
return ['http:', 'https:'].includes(parsed.protocol)
|
return ['http:', 'https:'].includes(parsed.protocol)
|
||||||
} catch {
|
} catch {
|
||||||
// 相对路径也允许
|
// 非标准 URL 格式,小程序不支持相对路径,拒绝
|
||||||
return url.startsWith('/') || url.startsWith('./')
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -177,21 +177,10 @@ router.post('/sync', async (req, res) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新 next_date 到下一周期
|
// 更新 next_date:循环结束后 next 已推进到今天之后的下一周期,直接使用
|
||||||
let newNext: Date
|
|
||||||
if (t.frequency === 'weekly') {
|
|
||||||
newNext = new Date(new Date(t.next_date).getTime() + 7 * 86400000)
|
|
||||||
} else if (t.frequency === 'monthly') {
|
|
||||||
const d = new Date(t.next_date)
|
|
||||||
newNext = new Date(d.getFullYear(), d.getMonth() + 1, d.getDate())
|
|
||||||
} else {
|
|
||||||
const d = new Date(t.next_date)
|
|
||||||
newNext = new Date(d.getFullYear() + 1, d.getMonth(), d.getDate())
|
|
||||||
}
|
|
||||||
|
|
||||||
await pool.execute(
|
await pool.execute(
|
||||||
'UPDATE recurring_templates SET next_date = ? WHERE id = ?',
|
'UPDATE recurring_templates SET next_date = ? WHERE id = ?',
|
||||||
[newNext.toISOString().slice(0, 10), t.id]
|
[next.toISOString().slice(0, 10), t.id]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -132,7 +132,11 @@ router.post('/avatar', (req: AuthRequest, res: Response) => {
|
|||||||
if (oldUrl) {
|
if (oldUrl) {
|
||||||
const oldPath = path.join(AVATAR_DIR, oldUrl)
|
const oldPath = path.join(AVATAR_DIR, oldUrl)
|
||||||
if (fs.existsSync(oldPath)) {
|
if (fs.existsSync(oldPath)) {
|
||||||
|
try {
|
||||||
fs.unlinkSync(oldPath)
|
fs.unlinkSync(oldPath)
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[User] 删除旧头像失败:', e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user