Initial commit of akmon project

This commit is contained in:
2026-01-20 08:04:15 +08:00
commit 77a2bab985
1309 changed files with 343305 additions and 0 deletions

View File

@@ -0,0 +1,891 @@
<!-- 养老管理系统 - 财务管理 -->
<template>
<view class="finance-management">
<view class="header">
<text class="title">财务管理</text>
<button class="add-btn" @click="showAddBill">添加账单</button>
</view>
<!-- 统计卡片 -->
<view class="stats-section">
<view class="stat-card">
<view class="stat-icon"></view>
<view class="stat-content">
<text class="stat-number">{{ stats.total_amount.toFixed(2) }}</text>
<text class="stat-label">总金额</text>
</view>
</view>
<view class="stat-card">
<view class="stat-icon">✅</view>
<view class="stat-content">
<text class="stat-number">{{ stats.paid_amount.toFixed(2) }}</text>
<text class="stat-label">已收金额</text>
</view>
</view>
<view class="stat-card">
<view class="stat-icon">⏰</view>
<view class="stat-content">
<text class="stat-number">{{ stats.pending_amount.toFixed(2) }}</text>
<text class="stat-label">待收金额</text>
</view>
</view>
<view class="stat-card">
<view class="stat-icon">⚠️</view>
<view class="stat-content">
<text class="stat-number">{{ stats.overdue_amount.toFixed(2) }}</text>
<text class="stat-label">逾期金额</text>
</view>
</view>
</view>
<!-- 筛选区域 -->
<view class="filter-section">
<view class="filter-item">
<text class="filter-label">老人:</text>
<picker-view class="picker" :value="selectedElderIndex" @change="onElderChange">
<picker-view-column>
<view v-for="(elder, index) in elderOptions" :key="elder.id" class="picker-item">
{{ elder.name }}
</view>
</picker-view-column>
</picker-view>
</view>
<view class="filter-item">
<text class="filter-label">类型:</text>
<picker-view class="picker" :value="selectedTypeIndex" @change="onTypeChange">
<picker-view-column>
<view v-for="(type, index) in typeOptions" :key="index" class="picker-item">
{{ type.label }}
</view>
</picker-view-column>
</picker-view>
</view>
<view class="filter-item">
<text class="filter-label">状态:</text>
<picker-view class="picker" :value="selectedStatusIndex" @change="onStatusChange">
<picker-view-column>
<view v-for="(status, index) in statusOptions" :key="index" class="picker-item">
{{ status.label }}
</view>
</picker-view-column>
</picker-view>
</view>
<button class="search-btn" @click="searchBills">搜索</button>
</view>
<!-- 账单列表 -->
<view class="bills-list">
<view v-for="bill in bills" :key="bill.id" class="bill-item" @click="viewBillDetail(bill)">
<view class="bill-header">
<text class="bill-type">{{ getTypeText(bill.bill_type) }}</text>
<view class="status-badge" :class="getStatusClass(bill.status)">
<text class="status-text">{{ getStatusText(bill.status) }}</text>
</view>
</view>
<view class="bill-info">
<text class="elder-name">{{ getElderName(bill.elder_id) }}</text>
<text class="bill-amount">金额: ¥{{ bill.amount.toFixed(2) }}</text>
<text class="bill-description">{{ bill.description ?? '无描述' }}</text>
</view>
<view class="bill-dates">
<text class="date-text">到期: {{ formatDate(bill.due_date) }}</text>
<text class="date-text" v-if="bill.paid_date">支付: {{ formatDate(bill.paid_date) }}</text>
</view>
<view class="bill-actions">
<button class="action-btn edit-btn" @click.stop="editBill(bill)">编辑</button>
<button class="action-btn pay-btn" v-if="bill.status === 'pending'" @click.stop="markAsPaid(bill)">标记已付</button>
<button class="action-btn print-btn" @click.stop="printBill(bill)">打印</button>
</view>
</view>
</view>
<!-- 添加/编辑账单弹窗 -->
<view v-if="showBillModal" class="modal-overlay" @click="closeBillModal">
<view class="modal-content" @click.stop="">
<view class="modal-header">
<text class="modal-title">{{ isEditMode ? '编辑账单' : '添加账单' }}</text>
<button class="close-btn" @click="closeBillModal">×</button>
</view>
<view class="modal-body">
<view class="form-group">
<text class="form-label">老人:</text>
<picker-view class="form-picker" :value="formData.elderIndex" @change="onFormElderChange">
<picker-view-column>
<view v-for="(elder, index) in elderOptions" :key="elder.id" class="picker-item">
{{ elder.name }}
</view>
</picker-view-column>
</picker-view>
</view>
<view class="form-group">
<text class="form-label">账单类型:</text>
<picker-view class="form-picker" :value="formData.typeIndex" @change="onFormTypeChange">
<picker-view-column>
<view v-for="(type, index) in billTypes" :key="index" class="picker-item">
{{ type.label }}
</view>
</picker-view-column>
</picker-view>
</view>
<view class="form-group">
<text class="form-label">金额:</text>
<input class="form-input" v-model="formData.amount" type="number" placeholder="请输入金额" />
</view>
<view class="form-group">
<text class="form-label">描述:</text>
<textarea class="form-textarea" v-model="formData.description" placeholder="请输入账单描述"></textarea>
</view>
<view class="form-group">
<text class="form-label">到期日期:</text>
<lime-date-time-picker v-model="formData.due_date" type="date" :placeholder="'选择日期'" class="date-picker" />
</view>
<view class="form-group">
<text class="form-label">支付方式:</text>
<picker-view class="form-picker" :value="formData.paymentMethodIndex" @change="onFormPaymentMethodChange">
<picker-view-column>
<view v-for="(method, index) in paymentMethods" :key="index" class="picker-item">
{{ method.label }}
</view>
</picker-view-column>
</picker-view>
</view>
<view class="form-group">
<text class="form-label">备注:</text>
<textarea class="form-textarea" v-model="formData.notes" placeholder="请输入备注"></textarea>
</view>
</view>
<view class="modal-footer">
<button class="cancel-btn" @click="closeBillModal">取消</button>
<button class="save-btn" @click="saveBill">保存</button>
</view>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, onMounted } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import type { Bill, Elder } from '../types.uts'
import { formatDate, getStatusClass } from '../types.uts'
import supa from '@/components/supadb/aksupainstance.uts'
// 统计数据类型
type FinanceStats = {
total_amount: number
paid_amount: number
pending_amount: number
overdue_amount: number
}
// 响应式数据
const bills = ref<Bill[]>([])
const elderOptions = ref<Elder[]>([])
const eldersMap = ref<Map<string, string>>(new Map())
const stats = ref<FinanceStats>({
total_amount: 0,
paid_amount: 0,
pending_amount: 0,
overdue_amount: 0
})
// 筛选相关
const selectedElderIndex = ref([0])
const selectedTypeIndex = ref([0])
const selectedStatusIndex = ref([0])
const typeOptions = [
{ value: 'all', label: '全部类型' },
{ value: 'monthly_fee', label: '月费' },
{ value: 'medical', label: '医疗费' },
{ value: 'nursing', label: '护理费' },
{ value: 'meal', label: '餐费' },
{ value: 'activity', label: '活动费' },
{ value: 'other', label: '其他费用' }
]
const statusOptions = [
{ value: 'all', label: '全部状态' },
{ value: 'pending', label: '待支付' },
{ value: 'paid', label: '已支付' },
{ value: 'overdue', label: '已逾期' },
{ value: 'cancelled', label: '已取消' }
]
const billTypes = [
{ value: 'monthly_fee', label: '月费' },
{ value: 'medical', label: '医疗费' },
{ value: 'nursing', label: '护理费' },
{ value: 'meal', label: '餐费' },
{ value: 'activity', label: '活动费' },
{ value: 'other', label: '其他费用' }
]
const paymentMethods = [
{ value: 'cash', label: '现金' },
{ value: 'card', label: '银行卡' },
{ value: 'transfer', label: '转账' },
{ value: 'wechat', label: '微信支付' },
{ value: 'alipay', label: '支付宝' }
]
// 弹窗相关
const showBillModal = ref(false)
const isEditMode = ref(false)
const currentBillId = ref<string | null>(null)
// 表单数据
const formData = ref({
elderIndex: [0],
typeIndex: [0],
amount: '',
description: '',
due_date: '',
paymentMethodIndex: [0],
notes: ''
})
// 页面加载
onLoad(() => {
loadData()
})
// 加载数据
async function loadData(): Promise<void> {
try {
await Promise.all([
loadElders(),
loadBills(),
loadStats()
])
} catch (error) {
console.error('加载数据失败:', error)
uni.showToast({
title: '加载数据失败',
icon: 'error'
})
}
}
// 加载老人列表
async function loadElders(): Promise<void> {
const result = await supa.executeAs<Elder>('eldercare_admin', `
SELECT id, name FROM ec_elders
WHERE status = 'active'
ORDER BY name
`)
elderOptions.value = [{ id: '', name: '全部老人' } as Elder, ...result]
// 建立映射
const map = new Map<string, string>()
for (let i: Int = 0; i < result.length; i++) {
const elder = result[i]
map.set(elder.id, elder.name)
}
eldersMap.value = map
}
// 加载账单列表
async function loadBills(): Promise<void> {
let whereClause = "WHERE 1=1"
// 老人筛选
if (selectedElderIndex.value[0] > 0) {
const selectedElder = elderOptions.value[selectedElderIndex.value[0]]
whereClause += ` AND elder_id = '${selectedElder.id}'`
}
// 类型筛选
if (selectedTypeIndex.value[0] > 0) {
const selectedType = typeOptions[selectedTypeIndex.value[0]]
whereClause += ` AND bill_type = '${selectedType.value}'`
}
// 状态筛选
if (selectedStatusIndex.value[0] > 0) {
const selectedStatus = statusOptions[selectedStatusIndex.value[0]]
whereClause += ` AND status = '${selectedStatus.value}'`
}
const result = await supa.executeAs<Bill>('eldercare_admin', `
SELECT * FROM ec_bills
${whereClause}
ORDER BY created_at DESC
`)
bills.value = result
}
// 加载统计数据
async function loadStats(): Promise<void> {
const result = await supa.executeAs<FinanceStats>('eldercare_admin', `
SELECT
COALESCE(SUM(amount), 0) as total_amount,
COALESCE(SUM(CASE WHEN status = 'paid' THEN amount ELSE 0 END), 0) as paid_amount,
COALESCE(SUM(CASE WHEN status = 'pending' THEN amount ELSE 0 END), 0) as pending_amount,
COALESCE(SUM(CASE WHEN status = 'overdue' THEN amount ELSE 0 END), 0) as overdue_amount
FROM ec_bills
`)
if (result.length > 0) {
stats.value = result[0]
}
}
// 获取老人姓名
function getElderName(elderId: string): string {
return eldersMap.value.get(elderId) ?? '未知老人'
}
// 获取类型文本
function getTypeText(type: string): string {
const typeMap: Record<string, string> = {
'monthly_fee': '月费',
'medical': '医疗费',
'nursing': '护理费',
'meal': '餐费',
'activity': '活动费',
'other': '其他费用'
}
return typeMap[type] ?? type
}
// 获取状态文本
function getStatusText(status: string): string {
const statusMap: Record<string, string> = {
'pending': '待支付',
'paid': '已支付',
'overdue': '已逾期',
'cancelled': '已取消'
}
return statusMap[status] ?? status
}
// 筛选事件
function onElderChange(e: any): void {
selectedElderIndex.value = e.detail.value
}
function onTypeChange(e: any): void {
selectedTypeIndex.value = e.detail.value
}
function onStatusChange(e: any): void {
selectedStatusIndex.value = e.detail.value
}
// 搜索账单
function searchBills(): void {
loadBills()
loadStats()
}
// 查看账单详情
function viewBillDetail(bill: Bill): void {
uni.navigateTo({
url: `/pages/ec/finance/detail?id=${bill.id}`
})
}
// 编辑账单
function editBill(bill: Bill): void {
isEditMode.value = true
currentBillId.value = bill.id
// 填充表单数据
const elderIndex = elderOptions.value.findIndex(elder => elder.id === bill.elder_id)
const typeIndex = billTypes.findIndex(type => type.value === bill.bill_type)
const paymentMethodIndex = paymentMethods.findIndex(method => method.value === bill.payment_method)
formData.value = {
elderIndex: [elderIndex > 0 ? elderIndex : 0],
typeIndex: [typeIndex > 0 ? typeIndex : 0],
amount: bill.amount.toString(),
description: bill.description ?? '',
due_date: bill.due_date ?? '',
paymentMethodIndex: [paymentMethodIndex > 0 ? paymentMethodIndex : 0],
notes: bill.notes ?? ''
}
showBillModal.value = true
}
// 标记为已支付
async function markAsPaid(bill: Bill): Promise<void> {
uni.showModal({
title: '确认支付',
content: '确定标记此账单为已支付吗?',
success: async (res) => {
if (res.confirm) {
try {
await supa.executeAs('eldercare_admin', `
UPDATE ec_bills
SET status = 'paid', paid_date = CURRENT_DATE, updated_at = NOW()
WHERE id = '${bill.id}'
`)
uni.showToast({
title: '支付成功',
icon: 'success'
})
loadBills()
loadStats()
} catch (error) {
console.error('标记支付失败:', error)
uni.showToast({
title: '操作失败',
icon: 'error'
})
}
}
}
})
}
// 打印账单
function printBill(bill: Bill): void {
// 这里可以实现打印功能
uni.showToast({
title: '打印功能待实现',
icon: 'none'
})
}
// 显示添加账单弹窗
function showAddBill(): void {
isEditMode.value = false
currentBillId.value = null
// 重置表单
const today = new Date()
const nextMonth = new Date(today.getFullYear(), today.getMonth() + 1, today.getDate())
formData.value = {
elderIndex: [0],
typeIndex: [0],
amount: '',
description: '',
due_date: formatDate(nextMonth.toISOString()),
paymentMethodIndex: [0],
notes: ''
}
showBillModal.value = true
}
// 关闭弹窗
function closeBillModal(): void {
showBillModal.value = false
}
// 表单事件
function onFormElderChange(e: any): void {
formData.value.elderIndex = e.detail.value
}
function onFormTypeChange(e: any): void {
formData.value.typeIndex = e.detail.value
}
function onFormPaymentMethodChange(e: any): void {
formData.value.paymentMethodIndex = e.detail.value
}
function onDueDateChange(date: string): void {
formData.value.due_date = date
}
// 保存账单
async function saveBill(): Promise<void> {
// 验证表单
if (formData.value.elderIndex[0] === 0) {
uni.showToast({
title: '请选择老人',
icon: 'error'
})
return
}
if (formData.value.amount.trim() === '' || parseFloat(formData.value.amount) <= 0) {
uni.showToast({
title: '请输入有效金额',
icon: 'error'
})
return
}
try {
const selectedElder = elderOptions.value[formData.value.elderIndex[0]]
const selectedType = billTypes[formData.value.typeIndex[0]]
const selectedPaymentMethod = paymentMethods[formData.value.paymentMethodIndex[0]]
if (isEditMode.value && currentBillId.value !== null) {
// 更新账单
await supa.executeAs('eldercare_admin', `
UPDATE ec_bills SET
elder_id = '${selectedElder.id}',
bill_type = '${selectedType.value}',
amount = ${formData.value.amount},
description = '${formData.value.description}',
due_date = ${formData.value.due_date ? `'${formData.value.due_date}'` : 'NULL'},
payment_method = '${selectedPaymentMethod.value}',
notes = '${formData.value.notes}',
updated_at = NOW()
WHERE id = '${currentBillId.value}'
`)
} else {
// 新增账单
await supa.executeAs('eldercare_admin', `
INSERT INTO ec_bills (
elder_id, bill_type, amount, description, due_date,
payment_method, notes, status
) VALUES (
'${selectedElder.id}',
'${selectedType.value}',
${formData.value.amount},
'${formData.value.description}',
${formData.value.due_date ? `'${formData.value.due_date}'` : 'NULL'},
'${selectedPaymentMethod.value}',
'${formData.value.notes}',
'pending'
)
`)
}
uni.showToast({
title: '保存成功',
icon: 'success'
})
closeBillModal()
loadBills()
loadStats()
} catch (error) {
console.error('保存失败:', error)
uni.showToast({
title: '保存失败',
icon: 'error'
})
}
}
</script>
<style scoped>
/* uts-android 兼容性重构:
1. 移除所有嵌套选择器、伪类(如 :last-child、&.xxx全部 class 扁平化。
2. 所有间距用 margin-right/margin-bottom 控制,禁止 gap、flex-wrap、嵌套。
3. 所有布局 display: flex禁止 grid、gap、伪类。
4. 组件间距、分隔线全部用 border/margin 控制。
5. 新增.is-last、.is-active、.is-overdue 等辅助 class。
*/
.finance-management {
padding: 20px;
background: #f5f5f5;
min-height: 100vh;
}
.header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.title {
font-size: 24px;
font-weight: bold;
color: #333;
}
.add-btn {
padding: 8px 16px;
border-radius: 20px;
border: 1px solid #2196f3;
background-color: #2196f3;
color: white;
}
.stats-section {
display: flex;
flex-direction: row;
margin-bottom: 20px;
}
.stat-card {
flex: 1;
background: #fff;
border-radius: 12px;
margin-right: 15px;
padding: 20px;
display: flex;
flex-direction: row;
align-items: center;
}
.stat-card.is-last {
margin-right: 0;
}
.stat-icon {
font-size: 24px;
margin-right: 10px;
}
.stat-content {
flex: 1;
}
.stat-number {
font-size: 20px;
font-weight: bold;
}
.stat-label {
font-size: 14px;
color: #666;
}
.filter-section {
display: flex;
flex-direction: row;
align-items: center;
background: #fff;
border-radius: 12px;
padding: 16px;
margin-bottom: 20px;
}
.filter-item {
display: flex;
flex-direction: row;
align-items: center;
margin-right: 15px;
}
.filter-item.is-last {
margin-right: 0;
}
.filter-label {
font-size: 14px;
color: #666;
margin-right: 8px;
}
.picker {
width: 150px;
}
.search-btn {
padding: 8px 16px;
border-radius: 8px;
background: #2196f3;
color: white;
border: none;
}
.bills-list {
background: #fff;
border-radius: 12px;
min-height: 300px;
margin-bottom: 20px;
}
.bill-item {
padding: 16px;
border-bottom: 1px solid #f0f0f0;
}
.bill-item.is-last {
border-bottom: none;
}
.bill-header {
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: 8px;
}
.bill-type {
font-size: 16px;
font-weight: bold;
color: #333;
margin-right: 10px;
}
.status-badge {
padding: 4px 10px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
background-color: #f5f5f5;
color: #999;
}
.status-badge.paid {
background-color: #f6ffed;
color: #52c41a;
}
.status-badge.pending {
background-color: #fffbe6;
color: #faad14;
}
.status-badge.overdue {
background-color: #fff2f0;
color: #ff4d4f;
}
.status-text {
font-weight: 500;
}
.bill-info {
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: 8px;
}
.elder-name {
font-size: 14px;
color: #1890ff;
margin-right: 10px;
}
.bill-amount {
font-size: 14px;
color: #333;
margin-right: 10px;
}
.bill-description {
font-size: 14px;
color: #666;
}
.bill-dates {
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: 8px;
}
.date-text {
font-size: 12px;
color: #999;
margin-right: 10px;
}
.bill-actions {
display: flex;
flex-direction: row;
align-items: center;
}
.action-btn {
padding: 6px 12px;
border-radius: 15px;
border: none;
font-size: 12px;
color: white;
margin-right: 8px;
}
.edit-btn {
background-color: #1890ff;
}
.pay-btn {
background-color: #52c41a;
}
.print-btn {
background-color: #faad14;
color: #fff;
}
.action-btn.is-last {
margin-right: 0;
}
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.3);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-content {
background: #fff;
border-radius: 12px;
padding: 24px;
width: 500px;
max-width: 95vw;
}
.modal-header {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
.modal-title {
font-size: 18px;
font-weight: bold;
color: #333;
}
.close-btn {
background: none;
border: none;
font-size: 24px;
color: #999;
}
.modal-body {
margin-bottom: 16px;
}
.form-group {
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: 12px;
}
.form-label {
font-size: 14px;
color: #666;
margin-right: 8px;
width: 80px;
flex-shrink: 0;
}
.form-picker {
width: 150px;
}
.form-input {
flex: 1;
border: 1px solid #ddd;
border-radius: 8px;
padding: 8px 12px;
font-size: 14px;
}
.form-textarea {
flex: 1;
border: 1px solid #ddd;
border-radius: 8px;
padding: 8px 12px;
font-size: 14px;
min-height: 60px;
}
.form-date-picker {
width: 150px;
}
.modal-footer {
display: flex;
flex-direction: row;
justify-content: flex-end;
}
.cancel-btn {
background-color: #f5f5f5;
color: #666;
margin-right: 10px;
}
.save-btn {
background-color: #2196f3;
color: white;
}
@media (max-width: 768px) {
.finance-management {
padding: 15px;
}
.stats-section {
flex-direction: column;
}
.stat-card {
margin: 5px 0;
min-width: auto;
}
.filter-section {
flex-direction: column;
align-items: stretch;
}
.filter-item {
margin-right: 0;
justify-content: space-between;
}
.picker {
width: 150px;
}
.modal-content {
width: 95%;
}
}
</style>