|
- {% extends "../layout.html" %}
-
- {% block title %}{{ columnInfo.name if columnInfo else '捐赠收支' }}{% endblock %}
-
- {% block content %}
- <div id="donationApp">
- <!-- 统计卡片 -->
- <div class="stat-row">
- <div class="stat-card">
- <div class="stat-label">累计捐赠收入</div>
- <div class="stat-value" style="color:#ff7800;">${ formatMoney(stat.total_income) }</div>
- </div>
- <div class="stat-card">
- <div class="stat-label">累计公益支出</div>
- <div class="stat-value" style="color:#1A3550;">${ formatMoney(stat.total_expense) }</div>
- </div>
- <div class="stat-card">
- <div class="stat-label">本年度收入</div>
- <div class="stat-value" style="color:#67c23a;">${ formatMoney(stat.year_income) }</div>
- </div>
- <div class="stat-card">
- <div class="stat-label">本年度支出</div>
- <div class="stat-value" style="color:#e6a23c;">${ formatMoney(stat.year_expense) }</div>
- </div>
- </div>
-
- <!-- 统计操作栏 -->
- <el-card shadow="never" class="mb-4">
- <div class="flex items-center gap-3">
- <span class="text-sm">统计数据管理</span>
- <el-button size="small" @click="openStatDialog">编辑统计</el-button>
- <el-button size="small" type="primary" @click="syncStat" :loading="syncing">从明细同步</el-button>
- <span v-if="stat.sync_time" class="text-xs text-gray-400 ml-2">上次同步: ${ stat.sync_time?.slice(0,16).replace('T',' ') }</span>
- </div>
- </el-card>
-
- <!-- Tab 切换 -->
- <div class="content-tabs mb-4">
- <div class="tab-item" :class="{ active: activeTab === 'income' }" @click="switchTab('income')">捐赠收入</div>
- <div class="tab-item" :class="{ active: activeTab === 'expense' }" @click="switchTab('expense')">公益支出</div>
- </div>
-
- <el-card shadow="never">
- <!-- 筛选栏 -->
- <div class="flex items-center gap-4 mb-4 flex-wrap">
- <el-input v-model="query.keyword" :placeholder="activeTab === 'income' ? '捐赠人/单位...' : '支出项目...'" style="width:180px;" clearable @keyup.enter="loadList"></el-input>
- <el-select v-model="query.source" placeholder="来源" style="width:120px;" clearable>
- <el-option label="金蝶同步" value="kingdee"></el-option>
- <el-option label="手动录入" value="manual"></el-option>
- </el-select>
- <el-select v-model="query.status" placeholder="状态" style="width:100px;" clearable>
- <el-option label="已公示" :value="1"></el-option>
- <el-option label="待公示" :value="0"></el-option>
- </el-select>
- <el-button type="primary" @click="loadList">搜索</el-button>
- <el-button @click="resetQuery">重置</el-button>
- <div class="flex-1"></div>
- <el-button type="primary" @click="openDialog()">+ 手动录入</el-button>
- <el-button type="success" @click="exportData">导出</el-button>
- </div>
-
- <!-- 表格 -->
- <el-table :data="list" v-loading="loading" border>
- <el-table-column :label="activeTab === 'income' ? '捐赠人/单位' : '支出项目'" prop="name" min-width="200"></el-table-column>
- <el-table-column label="金额 (元)" width="160">
- <template #default="{ row }">
- <span :style="{ fontWeight: 600, color: activeTab === 'income' ? '#ff7800' : '#1A3550' }">${ formatMoney(row.amount) }</span>
- </template>
- </el-table-column>
- <el-table-column :label="activeTab === 'income' ? '用途/项目' : '受益对象'" prop="purpose" min-width="140"></el-table-column>
- <el-table-column label="来源" width="100" align="center">
- <template #default="{ row }">
- <el-tag :type="row.source === 'kingdee' ? 'primary' : 'info'" size="small">${ row.source === 'kingdee' ? '金蝶同步' : '手动录入' }</el-tag>
- </template>
- </el-table-column>
- <el-table-column label="日期" width="120" prop="record_date"></el-table-column>
- <el-table-column label="状态" width="90" align="center">
- <template #default="{ row }">
- <el-tag :type="row.status ? 'success' : 'warning'" size="small">${ row.status ? '已公示' : '待公示' }</el-tag>
- </template>
- </el-table-column>
- <el-table-column label="操作" width="120" fixed="right">
- <template #default="{ row }">
- <el-button type="primary" link size="small" @click="openDialog(row)">编辑</el-button>
- <el-button type="danger" link size="small" @click="deleteItem(row)">删除</el-button>
- </template>
- </el-table-column>
- </el-table>
-
- <!-- 分页 -->
- <div class="flex justify-end mt-4" v-if="total > pageSize">
- <el-pagination background layout="prev, pager, next" :total="total" :page-size="pageSize" v-model:current-page="page" @current-change="loadList"></el-pagination>
- </div>
- </el-card>
-
- <!-- 新增/编辑弹窗 -->
- <el-dialog v-model="dialogVisible" :title="dialogTitle" width="560px" destroy-on-close draggable top="5vh">
- <el-form :model="form" label-width="100px">
- <el-form-item label="类型">
- <el-radio-group v-model="form.type">
- <el-radio value="income">捐赠收入</el-radio>
- <el-radio value="expense">公益支出</el-radio>
- </el-radio-group>
- </el-form-item>
- <el-form-item :label="form.type === 'income' ? '捐赠人/单位' : '支出项目'" required>
- <el-input v-model="form.name" placeholder="请输入"></el-input>
- </el-form-item>
- <div class="flex gap-4">
- <el-form-item label="金额 (元)" required class="flex-1">
- <el-input-number v-model="form.amount" :min="0" :precision="2" :controls="false" style="width:100%;"></el-input-number>
- </el-form-item>
- <el-form-item label="日期" class="flex-1">
- <el-date-picker v-model="form.record_date" type="date" value-format="YYYY-MM-DD" placeholder="选择日期" style="width:100%;"></el-date-picker>
- </el-form-item>
- </div>
- <el-form-item :label="form.type === 'income' ? '用途/项目' : '受益对象'">
- <el-input v-model="form.purpose" placeholder="请输入"></el-input>
- </el-form-item>
- <div class="flex gap-4">
- <el-form-item label="来源" class="flex-1">
- <el-select v-model="form.source" style="width:100%;">
- <el-option label="手动录入" value="manual"></el-option>
- <el-option label="金蝶同步" value="kingdee"></el-option>
- </el-select>
- </el-form-item>
- <el-form-item label="状态" class="flex-1">
- <el-select v-model="form.status" style="width:100%;">
- <el-option label="已公示" :value="1"></el-option>
- <el-option label="待公示" :value="0"></el-option>
- </el-select>
- </el-form-item>
- </div>
- </el-form>
- <template #footer>
- <el-button @click="dialogVisible = false">取消</el-button>
- <el-button type="primary" @click="saveItem" :loading="saving">保存</el-button>
- </template>
- </el-dialog>
-
- <!-- 统计编辑弹窗 -->
- <el-dialog v-model="statDialogVisible" title="编辑统计数据" width="480px" destroy-on-close draggable>
- <el-form :model="statForm" label-width="120px">
- <el-form-item label="累计捐赠收入">
- <el-input-number v-model="statForm.total_income" :min="0" :precision="2" :controls="false" style="width:100%;"></el-input-number>
- </el-form-item>
- <el-form-item label="累计公益支出">
- <el-input-number v-model="statForm.total_expense" :min="0" :precision="2" :controls="false" style="width:100%;"></el-input-number>
- </el-form-item>
- <el-form-item label="本年度收入">
- <el-input-number v-model="statForm.year_income" :min="0" :precision="2" :controls="false" style="width:100%;"></el-input-number>
- </el-form-item>
- <el-form-item label="本年度支出">
- <el-input-number v-model="statForm.year_expense" :min="0" :precision="2" :controls="false" style="width:100%;"></el-input-number>
- </el-form-item>
- </el-form>
- <template #footer>
- <el-button @click="statDialogVisible = false">取消</el-button>
- <el-button type="primary" @click="saveStat" :loading="statSaving">保存</el-button>
- </template>
- </el-dialog>
- </div>
-
- <style>
- .stat-row { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-bottom: 16px; }
- .stat-card { background: #fff; border-radius: 4px; box-shadow: 0 2px 12px rgba(0,0,0,.06); padding: 20px; text-align: center; }
- .stat-label { font-size: 13px; color: #909399; margin-bottom: 8px; }
- .stat-value { font-size: 22px; font-weight: 700; font-family: "Roboto", sans-serif; }
- .content-tabs { display: flex; gap: 0; background: #fff; border-radius: 4px; overflow: hidden; box-shadow: 0 2px 12px rgba(0,0,0,.06); }
- .tab-item { padding: 12px 24px; font-size: 14px; cursor: pointer; color: #606266; transition: .2s; border-bottom: 2px solid transparent; }
- .tab-item:hover { color: #ff7800; }
- .tab-item.active { color: #ff7800; font-weight: 600; border-bottom-color: #ff7800; background: #fff7f0; }
- </style>
- {% endblock %}
-
- {% block js %}
- <!-- SheetJS for Excel export -->
- <script src="https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js"></script>
-
- <script>
- const col = '{{ col }}';
- const { createApp, ref, reactive, onMounted } = Vue;
-
- const app = createApp({
- delimiters: ['${', '}'],
- setup() {
- const loading = ref(false);
- const list = ref([]);
- const total = ref(0);
- const page = ref(1);
- const pageSize = ref(20);
- const activeTab = ref('income');
- const query = reactive({ keyword: '', source: '', status: '' });
-
- const stat = reactive({ total_income: 0, total_expense: 0, year_income: 0, year_expense: 0, sync_time: null });
- const syncing = ref(false);
-
- const dialogVisible = ref(false);
- const dialogTitle = ref('手动录入');
- const saving = ref(false);
- const defaultForm = { id: null, type: 'income', name: '', amount: 0, purpose: '', source: 'manual', record_date: '', status: 1 };
- const form = reactive({ ...defaultForm });
-
- const statDialogVisible = ref(false);
- const statSaving = ref(false);
- const statForm = reactive({ total_income: 0, total_expense: 0, year_income: 0, year_expense: 0 });
-
- function formatMoney(n) {
- return '¥ ' + Number(n || 0).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
- }
-
- async function loadStat() {
- try {
- const res = await fetch('/admin/content/donation/stat?col=' + col).then(r => r.json());
- if (res.code === 0 && res.data) {
- Object.assign(stat, res.data);
- }
- } catch {}
- }
-
- async function loadList() {
- loading.value = true;
- try {
- const params = new URLSearchParams({ col, type: activeTab.value, page: page.value, pageSize: pageSize.value, ...query });
- const res = await fetch('/admin/content/donation/list?' + params).then(r => r.json());
- if (res.code === 0) {
- list.value = res.data.data || [];
- total.value = res.data.count || 0;
- }
- } finally { loading.value = false; }
- }
-
- function switchTab(tab) {
- activeTab.value = tab;
- page.value = 1;
- loadList();
- }
-
- function resetQuery() {
- query.keyword = '';
- query.source = '';
- query.status = '';
- page.value = 1;
- loadList();
- }
-
- function openDialog(item) {
- if (item) {
- dialogTitle.value = '编辑记录';
- Object.assign(form, {
- id: item.id, type: item.type, name: item.name,
- amount: parseFloat(item.amount) || 0, purpose: item.purpose || '',
- source: item.source || 'manual', record_date: item.record_date || '',
- status: item.status
- });
- } else {
- dialogTitle.value = '手动录入';
- Object.assign(form, { ...defaultForm, type: activeTab.value });
- }
- dialogVisible.value = true;
- }
-
- async function saveItem() {
- if (!form.name.trim()) { ElementPlus.ElMessage.warning('请输入名称'); return; }
- if (!form.amount || form.amount <= 0) { ElementPlus.ElMessage.warning('请输入有效金额'); return; }
-
- saving.value = true;
- try {
- const url = form.id ? '/admin/content/donation/edit' : '/admin/content/donation/add';
- const body = { ...form, col };
- const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }).then(r => r.json());
- if (res.code === 0) {
- ElementPlus.ElMessage.success('保存成功');
- dialogVisible.value = false;
- loadList();
- } else {
- ElementPlus.ElMessage.error(res.msg || '保存失败');
- }
- } finally { saving.value = false; }
- }
-
- async function deleteItem(item) {
- try {
- await ElementPlus.ElMessageBox.confirm('确定删除"' + item.name + '"?', '提示', { type: 'warning' });
- const res = await fetch('/admin/content/donation/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: item.id }) }).then(r => r.json());
- if (res.code === 0) {
- ElementPlus.ElMessage.success('删除成功');
- loadList();
- } else {
- ElementPlus.ElMessage.error(res.msg || '删除失败');
- }
- } catch {}
- }
-
- function openStatDialog() {
- Object.assign(statForm, {
- total_income: parseFloat(stat.total_income) || 0,
- total_expense: parseFloat(stat.total_expense) || 0,
- year_income: parseFloat(stat.year_income) || 0,
- year_expense: parseFloat(stat.year_expense) || 0
- });
- statDialogVisible.value = true;
- }
-
- async function saveStat() {
- statSaving.value = true;
- try {
- const res = await fetch('/admin/content/donation/saveStat', {
- method: 'POST', headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ ...statForm, col })
- }).then(r => r.json());
- if (res.code === 0) {
- ElementPlus.ElMessage.success('保存成功');
- Object.assign(stat, statForm);
- statDialogVisible.value = false;
- } else {
- ElementPlus.ElMessage.error(res.msg || '保存失败');
- }
- } finally { statSaving.value = false; }
- }
-
- async function syncStat() {
- try {
- await ElementPlus.ElMessageBox.confirm('确定从明细数据同步统计?这将覆盖当前统计数据。', '提示', { type: 'warning' });
- syncing.value = true;
- const res = await fetch('/admin/content/donation/syncStat', {
- method: 'POST', headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ col })
- }).then(r => r.json());
- if (res.code === 0) {
- ElementPlus.ElMessage.success('同步成功');
- Object.assign(stat, res.data);
- } else {
- ElementPlus.ElMessage.error(res.msg || '同步失败');
- }
- } catch {} finally { syncing.value = false; }
- }
-
- async function exportData() {
- try {
- const res = await fetch('/admin/content/donation/export?col=' + col + '&type=' + activeTab.value).then(r => r.json());
- if (res.code === 0) {
- const data = res.data || [];
- const isIncome = activeTab.value === 'income';
- const headers = isIncome
- ? ['捐赠人/单位', '金额', '用途/项目', '来源', '日期', '状态']
- : ['支出项目', '金额', '受益对象', '来源', '日期', '状态'];
- const rows = data.map(item => [
- item.name,
- parseFloat(item.amount) || 0,
- item.purpose,
- item.source === 'kingdee' ? '金蝶同步' : '手动录入',
- item.record_date,
- item.status ? '已公示' : '待公示'
- ]);
- const ws = XLSX.utils.aoa_to_sheet([headers, ...rows]);
- const wb = XLSX.utils.book_new();
- XLSX.utils.book_append_sheet(wb, ws, isIncome ? '捐赠收入' : '公益支出');
- XLSX.writeFile(wb, (isIncome ? '捐赠收入' : '公益支出') + '_' + new Date().toISOString().slice(0,10) + '.xlsx');
- ElementPlus.ElMessage.success('导出成功');
- }
- } catch (err) {
- ElementPlus.ElMessage.error('导出失败');
- }
- }
-
- onMounted(() => {
- loadStat();
- loadList();
- });
-
- return {
- loading, list, total, page, pageSize, activeTab, query, stat, syncing,
- dialogVisible, dialogTitle, saving, form,
- statDialogVisible, statSaving, statForm,
- formatMoney, loadList, switchTab, resetQuery, openDialog, saveItem, deleteItem,
- openStatDialog, saveStat, syncStat, exportData
- };
- }
- });
-
- app.use(ElementPlus, { locale: ElementPlusLocaleZhCn });
- app.mount('#donationApp');
- </script>
- {% endblock %}
|