Files
gamegroup2/frontend/src/components/ledger/CreateLedgerDialog.vue
T
congsh 19bf317d85 fix(phase3): description optional, restore nginx to host IP
- Make ledger description field optional (was required, caused 400)
- Revert nginx.conf back to 192.168.1.14:8090 (host IP, reliable)
- Keep docker-compose port mapping as 8090:8090
- Add $autoCancel:false to ledger/asset API calls

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-18 20:32:06 +08:00

332 lines
7.3 KiB
Vue

<script setup lang="ts">
import { ref, computed } from 'vue'
import { ElMessage } from 'element-plus'
import { useLedgerStore } from '@/stores/ledger'
import { useGroupStore } from '@/stores/group'
import type { Ledger, LedgerType, LedgerCategory } from '@/types'
import { LedgerCategoryMap } from '@/types'
import { displayName } from '@/types'
const props = defineProps<{
groupId: string
editLedger?: Ledger
}>()
const visible = defineModel<boolean>({ default: false })
const emit = defineEmits<{
saved: []
}>()
const ledgerStore = useLedgerStore()
const groupStore = useGroupStore()
const loading = ref(false)
const form = ref({
type: 'expense' as LedgerType,
amount: 0,
category: 'other' as LedgerCategory,
description: '',
relatedMembers: [] as string[],
occurredAt: '' as string | Date,
})
const isEditing = computed(() => !!props.editLedger)
const dialogTitle = computed(() => (isEditing.value ? '编辑账目' : '新建账目'))
// 分类选项
const categoryOptions = Object.entries(LedgerCategoryMap).map(([value, label]) => ({
label,
value,
}))
// 群组成员选项
const memberOptions = computed(() => {
return groupStore.currentMembers.map((m) => ({
label: displayName(m),
value: m.id,
}))
})
// 格式化今天日期
function getTodayStr(): string {
const d = new Date()
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
}
// 重置表单
function resetForm() {
form.value = {
type: 'expense',
amount: 0,
category: 'other',
description: '',
relatedMembers: [],
occurredAt: getTodayStr(),
}
}
// 填充编辑数据
function fillForm(ledger: Ledger) {
form.value = {
type: ledger.type,
amount: ledger.amount,
category: ledger.category,
description: ledger.description || '',
relatedMembers: [...(ledger.relatedMembers || [])],
occurredAt: ledger.occurredAt?.slice(0, 10) || getTodayStr(),
}
}
// 对话框打开
function handleOpen() {
if (props.editLedger) {
fillForm(props.editLedger)
} else {
resetForm()
}
}
// 提交
async function handleSubmit() {
if (!props.groupId) {
ElMessage.error('缺少群组信息')
return
}
if (form.value.amount <= 0) {
ElMessage.warning('金额必须大于0')
return
}
loading.value = true
try {
const occurredAt = form.value.occurredAt
? new Date(String(form.value.occurredAt)).toISOString()
: new Date().toISOString()
const submitData = {
type: form.value.type,
amount: form.value.amount,
category: form.value.category,
description: form.value.description || '',
relatedMembers: form.value.relatedMembers,
occurredAt,
}
if (isEditing.value && props.editLedger) {
await ledgerStore.editLedger(props.editLedger.id, submitData)
ElMessage.success('账目更新成功')
} else {
await ledgerStore.addLedger({
group: props.groupId,
...submitData,
})
ElMessage.success('账目创建成功')
}
visible.value = false
emit('saved')
} catch (error: any) {
ElMessage.error(error.message || '操作失败')
} finally {
loading.value = false
}
}
</script>
<template>
<el-dialog
v-model="visible"
:title="dialogTitle"
width="480px"
@open="handleOpen"
>
<div class="create-form">
<!-- 类型 -->
<div class="form-field">
<label>类型 <span class="required">*</span></label>
<div class="type-switch">
<button
:class="['type-btn', { active: form.type === 'income' }]"
@click="form.type = 'income'"
>
收入
</button>
<button
:class="['type-btn', { active: form.type === 'expense' }]"
@click="form.type = 'expense'"
>
支出
</button>
</div>
</div>
<!-- 金额 -->
<div class="form-field">
<label>金额 <span class="required">*</span></label>
<el-input
v-model.number="form.amount"
type="number"
:min="0.01"
:step="0.01"
placeholder="请输入金额"
>
<template #prefix>
<span class="amount-prefix">¥</span>
</template>
</el-input>
</div>
<!-- 分类 -->
<div class="form-field">
<label>分类 <span class="required">*</span></label>
<el-select
v-model="form.category"
placeholder="请选择分类"
style="width: 100%"
>
<el-option
v-for="opt in categoryOptions"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
</div>
<!-- 描述 -->
<div class="form-field">
<label>描述</label>
<el-input
v-model="form.description"
type="textarea"
:rows="3"
placeholder="请输入账目描述(可选)"
maxlength="200"
show-word-limit
/>
</div>
<!-- 关联成员 -->
<div class="form-field">
<label>关联成员</label>
<el-select
v-model="form.relatedMembers"
multiple
placeholder="选择关联成员(可选)"
style="width: 100%"
collapse-tags
collapse-tags-tooltip
>
<el-option
v-for="opt in memberOptions"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
</div>
<!-- 发生日期 -->
<div class="form-field">
<label>发生日期</label>
<el-date-picker
v-model="form.occurredAt"
type="date"
placeholder="选择日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</div>
</div>
<template #footer>
<el-button @click="visible = false">取消</el-button>
<button class="submit-btn" :disabled="loading" @click="handleSubmit">
{{ loading ? '保存中...' : '保存' }}
</button>
</template>
</el-dialog>
</template>
<style scoped>
.create-form {
display: flex;
flex-direction: column;
gap: 20px;
}
.form-field {
display: flex;
flex-direction: column;
gap: 8px;
}
.form-field label {
font-size: 14px;
font-weight: 500;
color: var(--gg-text);
}
.required {
color: var(--gg-danger);
}
.type-switch {
display: flex;
gap: 8px;
}
.type-btn {
flex: 1;
padding: 8px 16px;
border: 1px solid var(--gg-border, #dcdfe6);
border-radius: 6px;
background: var(--gg-bg, #fff);
color: var(--gg-text-secondary, #909399);
font-size: 14px;
cursor: pointer;
transition: all 0.2s;
}
.type-btn:hover {
border-color: var(--gg-primary, #67c23a);
color: var(--gg-primary, #67c23a);
}
.type-btn.active {
background: var(--gg-primary, #67c23a);
border-color: var(--gg-primary, #67c23a);
color: #fff;
}
.amount-prefix {
color: var(--gg-text-secondary);
font-weight: 500;
}
.submit-btn {
padding: 8px 20px;
border: none;
border-radius: var(--gg-radius-sm);
background: var(--gg-gradient-green);
color: #fff;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: opacity 0.2s;
}
.submit-btn:hover {
opacity: 0.85;
}
.submit-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
</style>