- 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
147 lines
3.4 KiB
Vue
147 lines
3.4 KiB
Vue
<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>
|