refactor(iter-v2): T02 code quality - shared components, transactions, utils
- C2: ColorPicker and EditModal reusable components extracted - C2: tag-manage and category-manage pages use EditModal (remove ~120 dup lines) - C3: Upload logic extracted to server/src/utils/upload.ts - C4: /recurring/sync wrapped in DB transaction - C7: Backup download uses API_BASE (no hardcoded URLs) - C9: getAvatarUrl() utility function, used in 5 files - C1: getCurrentMonth() documented as aligned with server date.ts
This commit is contained in:
146
client/src/components/EditModal/EditModal.vue
Normal file
146
client/src/components/EditModal/EditModal.vue
Normal file
@@ -0,0 +1,146 @@
|
||||
<template>
|
||||
<view class="modal-mask" v-if="visible" @tap="$emit('update:visible', false)">
|
||||
<view class="modal" @tap.stop>
|
||||
<text class="modal-title">{{ title }}</text>
|
||||
<template v-for="field in fields" :key="field.key">
|
||||
<input
|
||||
v-if="field.type === 'text'"
|
||||
class="modal-input"
|
||||
:value="form[field.key]"
|
||||
:placeholder="field.placeholder || field.label"
|
||||
:maxlength="field.maxlength || 50"
|
||||
@input="form[field.key] = ($event as any).detail.value"
|
||||
/>
|
||||
<ColorPicker
|
||||
v-if="field.type === 'color'"
|
||||
:modelValue="form[field.key] || colors[0]"
|
||||
@update:modelValue="form[field.key] = $event"
|
||||
:colors="colors"
|
||||
/>
|
||||
</template>
|
||||
<view class="modal-btns">
|
||||
<view class="modal-btn cancel" @tap="$emit('update:visible', false)">取消</view>
|
||||
<view class="modal-btn confirm" :class="{ disabled: !canConfirm }" @tap="onConfirm">确定</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, computed, watch } from 'vue'
|
||||
import ColorPicker from '@/components/ColorPicker/ColorPicker.vue'
|
||||
|
||||
export interface EditField {
|
||||
key: string
|
||||
label: string
|
||||
type: 'text' | 'color'
|
||||
placeholder?: string
|
||||
maxlength?: number
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
visible: boolean
|
||||
title: string
|
||||
fields: EditField[]
|
||||
modelValue: Record<string, any>
|
||||
colors?: string[]
|
||||
}>(), {
|
||||
colors: () => ['#FF8C69', '#7BC67E', '#5B9BD5', '#FFD700', '#FF69B4', '#8B5CF6', '#F97316', '#06B6D4']
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:visible': [val: boolean]
|
||||
'confirm': [data: Record<string, any>]
|
||||
}>()
|
||||
|
||||
const form = reactive<Record<string, any>>({})
|
||||
|
||||
// 当 visible 变为 true 时,用 modelValue 初始化 form
|
||||
watch(() => props.visible, (val) => {
|
||||
if (val) {
|
||||
Object.keys(form).forEach(k => delete form[k])
|
||||
Object.assign(form, { ...props.modelValue })
|
||||
}
|
||||
})
|
||||
|
||||
const canConfirm = computed(() => {
|
||||
// 至少有一个 text 字段不为空
|
||||
return props.fields
|
||||
.filter(f => f.type === 'text')
|
||||
.every(f => (form[f.key] || '').toString().trim().length > 0)
|
||||
})
|
||||
|
||||
function onConfirm() {
|
||||
if (!canConfirm.value) return
|
||||
// 只返回 fields 中定义的字段
|
||||
const data: Record<string, any> = {}
|
||||
props.fields.forEach(f => { data[f.key] = form[f.key] || '' })
|
||||
emit('confirm', data)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.modal-mask {
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 200;
|
||||
}
|
||||
|
||||
.modal {
|
||||
width: 600rpx;
|
||||
background: $surface;
|
||||
border-radius: $space-lg;
|
||||
padding: $space-2xl;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: $font-xl;
|
||||
font-weight: 600;
|
||||
color: $text;
|
||||
display: block;
|
||||
text-align: center;
|
||||
margin-bottom: $space-lg;
|
||||
}
|
||||
|
||||
.modal-input {
|
||||
height: 96rpx;
|
||||
background: $bg;
|
||||
border-radius: $space-md;
|
||||
padding: 0 $space-lg;
|
||||
font-size: $font-lg;
|
||||
margin-bottom: $space-md;
|
||||
}
|
||||
|
||||
.modal-btns {
|
||||
display: flex;
|
||||
gap: $space-md;
|
||||
}
|
||||
|
||||
.modal-btn {
|
||||
flex: 1;
|
||||
height: 88rpx;
|
||||
border-radius: $space-md;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: $font-lg;
|
||||
font-weight: 600;
|
||||
|
||||
&.cancel {
|
||||
background: $bg;
|
||||
color: $text-sec;
|
||||
}
|
||||
|
||||
&.confirm {
|
||||
background: linear-gradient(135deg, $primary, #E67355);
|
||||
color: $surface;
|
||||
}
|
||||
|
||||
&.disabled { opacity: 0.5; }
|
||||
&:active { opacity: 0.8; }
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user