Files
community/frontend/src/views/AdminActivities.vue
王子琦 e50715e789 upd
2026-01-21 11:45:59 +08:00

536 lines
15 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="page-shell">
<div class="section-title">活动管理</div>
<div class="section-subtitle">发布节气文化活动管理报名名额与状态</div>
<a-card class="search-card">
<div class="filter-group">
<a-select v-model="status" placeholder="筛选状态" allow-clear style="width: 160px;">
<a-option value="draft">草稿</a-option>
<a-option value="published">已发布</a-option>
<a-option value="closed">已结束</a-option>
</a-select>
<a-input v-model="keyword" placeholder="搜索活动主题" allow-clear />
<a-button type="primary" @click="fetchList">查询</a-button>
</div>
<div class="action-group">
<a-button type="primary" @click="openCreate">新建活动</a-button>
</div>
</a-card>
<a-table class="table-shell" :columns="columns" :data="rows" row-key="id" :pagination="false">
<template #status="{ record }">
<a-tag :color="statusColor(record.status)">{{ statusText(record.status) }}</a-tag>
</template>
<template #action="{ record }">
<a-space>
<a-button size="mini" type="text" @click="openEdit(record)">编辑</a-button>
<a-button size="mini" type="text" @click="publish(record.id)" v-if="record.status !== 'published'">发布</a-button>
<a-button size="mini" type="text" @click="close(record.id)" v-if="record.status === 'published'">结束</a-button>
<a-button size="mini" type="text" @click="goSignups(record)">报名名单</a-button>
</a-space>
</template>
</a-table>
<a-modal v-model:visible="visible" :title="modalTitle" @ok="handleSave" :ok-text="isEdit ? '保存' : '创建'" width="1100px">
<div class="modal-body">
<div class="calendar-panel">
<div class="calendar-head">
<div>
<div class="calendar-title">节气日历</div>
<div class="calendar-tip">点击有节气的日期一键生成活动草稿</div>
</div>
<div class="calendar-actions">
<a-button size="mini" @click="prevMonth">上个月</a-button>
<a-button size="mini" @click="goToday">本月</a-button>
<a-button size="mini" @click="nextMonth">下个月</a-button>
</div>
</div>
<div class="calendar-month">{{ monthLabel }}</div>
<div class="calendar-grid">
<div class="calendar-week" v-for="(week, wIndex) in calendarDays" :key="wIndex">
<div class="calendar-cell" v-for="day in week" :key="day.key" :class="dayClass(day)" @click="selectDay(day)">
<span class="cell-day">{{ day.date }}</span>
<span v-if="day.term" class="term-chip">{{ day.term }}</span>
</div>
</div>
</div>
</div>
<div class="form-panel">
<a-form :model="form" layout="vertical" class="form-shell">
<div class="form-title">活动信息</div>
<a-form-item label="活动标题" required>
<a-input v-model="form.title" placeholder="请输入活动标题" />
</a-form-item>
<a-form-item label="节气" required>
<a-select v-model="form.term" placeholder="请选择节气">
<a-option v-for="item in terms" :key="item" :value="item">{{ item }}</a-option>
</a-select>
</a-form-item>
<a-form-item label="活动简介">
<a-input v-model="form.summary" placeholder="一句话描述活动" />
</a-form-item>
<a-form-item label="活动详情">
<a-textarea v-model="form.content" placeholder="详细介绍活动内容" :auto-size="{ minRows: 3 }" />
</a-form-item>
<a-form-item label="地点" required>
<a-input v-model="form.location" placeholder="活动地点" />
</a-form-item>
<a-form-item label="活动时间" required>
<a-range-picker v-model="form.activityTime" show-time format="YYYY-MM-DD HH:mm" value-format="YYYY-MM-DDTHH:mm:ss" />
</a-form-item>
<a-form-item label="报名时间" required>
<a-range-picker v-model="form.signupTime" show-time format="YYYY-MM-DD HH:mm" value-format="YYYY-MM-DDTHH:mm:ss" />
</a-form-item>
<a-form-item label="报名名额" required>
<a-input-number v-model="form.quota" :min="1" />
</a-form-item>
<a-form-item label="封面图链接">
<a-input v-model="form.coverUrl" placeholder="选填" />
</a-form-item>
<a-form-item label="状态">
<a-select v-model="form.status" placeholder="默认草稿">
<a-option value="draft">草稿</a-option>
<a-option value="published">已发布</a-option>
<a-option value="closed">已结束</a-option>
</a-select>
</a-form-item>
</a-form>
</div>
</div>
</a-modal>
</div>
</template>
<script setup>
import { onMounted, reactive, ref, computed } from 'vue';
import { useRouter } from 'vue-router';
import { Message } from '@arco-design/web-vue';
import { listActivitiesAdmin, getActivityAdmin, createActivity, updateActivity, publishActivity, closeActivity } from '../api/activities';
import { Solar } from 'lunar-javascript';
const router = useRouter();
const rows = ref([]);
const status = ref('');
const keyword = ref('');
const visible = ref(false);
const isEdit = ref(false);
const currentId = ref(null);
const calendarBase = ref(new Date());
const form = reactive({
title: '',
term: '',
summary: '',
content: '',
location: '',
activityTime: [],
signupTime: [],
quota: 20,
coverUrl: '',
status: 'draft'
});
const terms = [
'立春', '雨水', '惊蛰', '春分', '清明', '谷雨',
'立夏', '小满', '芒种', '夏至', '小暑', '大暑',
'立秋', '处暑', '白露', '秋分', '寒露', '霜降',
'立冬', '小雪', '大雪', '冬至', '小寒', '大寒'
];
const columns = [
{ title: '活动标题', dataIndex: 'title' },
{ title: '节气', dataIndex: 'term' },
{ title: '地点', dataIndex: 'location' },
{ title: '开始时间', dataIndex: 'startTime' },
{ title: '名额', dataIndex: 'quota' },
{ title: '已报名', dataIndex: 'signupCount' },
{ title: '状态', slotName: 'status' },
{ title: '操作', slotName: 'action' }
];
const modalTitle = computed(() => (isEdit.value ? '编辑活动' : '新建活动'));
const fetchList = async () => {
const res = await listActivitiesAdmin({ status: status.value, keyword: keyword.value });
rows.value = res.data || [];
};
const resetForm = () => {
form.title = '';
form.term = '';
form.summary = '';
form.content = '';
form.location = '';
form.activityTime = [];
form.signupTime = [];
form.quota = 20;
form.coverUrl = '';
form.status = 'draft';
};
const termCache = new Map();
const termForDate = (date) => {
try {
if (!date) return '';
const key = `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`;
if (termCache.has(key)) {
return termCache.get(key);
}
const solar = Solar.fromDate(date);
const term = solar.getLunar().getJieQi() || '';
termCache.set(key, term);
return term;
} catch (err) {
return '';
}
};
const buildCalendar = (baseDate) => {
const year = baseDate.getFullYear();
const month = baseDate.getMonth();
const first = new Date(year, month, 1);
const startOffset = first.getDay();
const startDate = new Date(year, month, 1 - startOffset);
const weeks = [];
for (let w = 0; w < 6; w += 1) {
const row = [];
for (let d = 0; d < 7; d += 1) {
const current = new Date(startDate);
current.setDate(startDate.getDate() + w * 7 + d);
const term = termForDate(current);
row.push({
key: `${current.getFullYear()}-${current.getMonth() + 1}-${current.getDate()}`,
date: current.getDate(),
fullDate: current,
term,
isCurrentMonth: current.getMonth() === month
});
}
weeks.push(row);
}
return weeks;
};
const calendarDays = computed(() => buildCalendar(calendarBase.value));
const monthLabel = computed(() => {
const year = calendarBase.value.getFullYear();
const month = calendarBase.value.getMonth() + 1;
return `${year}${month}`;
});
const prevMonth = () => {
const date = new Date(calendarBase.value);
date.setMonth(date.getMonth() - 1);
calendarBase.value = date;
};
const nextMonth = () => {
const date = new Date(calendarBase.value);
date.setMonth(date.getMonth() + 1);
calendarBase.value = date;
};
const goToday = () => {
calendarBase.value = new Date();
};
const dayClass = (day) => ({
'is-other': !day.isCurrentMonth,
'is-term': Boolean(day.term)
});
const formatDateTime = (date, time = '09:00:00') => {
const pad = (val) => String(val).padStart(2, '0');
const year = date.getFullYear();
const month = pad(date.getMonth() + 1);
const day = pad(date.getDate());
return `${year}-${month}-${day}T${time}`;
};
const addDays = (date, days) => {
const next = new Date(date);
next.setDate(next.getDate() + days);
return next;
};
const selectDay = (day) => {
if (!day.term) {
Message.warning('这一天不是节气日,可手动填写活动信息');
return;
}
const date = day.fullDate;
form.term = day.term;
form.title = `${day.term}节气文化活动`;
form.summary = `围绕${day.term}开展的社区传统文化活动。`;
form.content = `本次活动以${day.term}为主题,包含节气科普、互动体验与邻里交流等内容。`;
form.activityTime = [formatDateTime(date, '09:00:00'), formatDateTime(date, '11:00:00')];
form.signupTime = [
formatDateTime(addDays(date, -7), '00:00:00'),
formatDateTime(addDays(date, -1), '23:59:59')
];
form.status = 'draft';
Message.success(`已根据${day.term}生成活动草稿`);
};
const openCreate = () => {
resetForm();
isEdit.value = false;
currentId.value = null;
calendarBase.value = new Date();
visible.value = true;
};
const openEdit = async (record) => {
isEdit.value = true;
currentId.value = record.id;
const res = await getActivityAdmin(record.id);
const data = res.data || record;
form.title = data.title;
form.term = data.term;
form.summary = data.summary || '';
form.content = data.content || '';
form.location = data.location;
form.activityTime = [data.startTime, data.endTime];
form.signupTime = [data.signupStart, data.signupEnd];
form.quota = data.quota;
form.coverUrl = data.coverUrl || '';
form.status = data.status;
calendarBase.value = new Date(data.startTime);
visible.value = true;
};
const handleSave = async () => {
if (!form.title || !form.term || !form.location || form.activityTime.length !== 2 || form.signupTime.length !== 2) {
Message.warning('请填写完整的活动信息');
return;
}
const payload = {
title: form.title,
term: form.term,
summary: form.summary,
content: form.content,
location: form.location,
startTime: form.activityTime[0],
endTime: form.activityTime[1],
signupStart: form.signupTime[0],
signupEnd: form.signupTime[1],
quota: form.quota,
status: form.status,
coverUrl: form.coverUrl
};
if (isEdit.value) {
await updateActivity(currentId.value, payload);
Message.success('活动已更新');
} else {
await createActivity(payload);
Message.success('活动已创建');
}
visible.value = false;
await fetchList();
};
const publish = async (id) => {
await publishActivity(id);
Message.success('活动已发布');
await fetchList();
};
const close = async (id) => {
await closeActivity(id);
Message.success('活动已结束');
await fetchList();
};
const goSignups = (record) => {
router.push(`/admin/activities/${record.id}/signups`);
};
const statusText = (value) => {
if (value === 'published') return '已发布';
if (value === 'closed') return '已结束';
return '草稿';
};
const statusColor = (value) => {
if (value === 'published') return 'green';
if (value === 'closed') return 'gray';
return 'orange';
};
onMounted(fetchList);
</script>
<style scoped>
.search-card {
margin-bottom: 18px;
display: flex;
justify-content: flex-start;
align-items: center;
border-radius: var(--radius-lg);
background: rgba(255, 255, 255, 0.94);
border: 1px solid var(--brand-line);
box-shadow: var(--shadow-card);
padding: 18px;
gap: 16px;
flex-wrap: nowrap;
}
.filter-group {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: nowrap;
flex: 1;
}
.action-group {
display: flex;
gap: 12px;
margin-left: 8px;
white-space: nowrap;
}
.filter-group :deep(.arco-input-wrapper) {
width: 260px;
}
.table-shell :deep(.arco-table) {
border-radius: var(--radius-lg);
overflow: hidden;
background: rgba(255, 255, 255, 0.94);
box-shadow: var(--shadow-card);
}
.modal-body {
display: grid;
grid-template-columns: 1.1fr 1fr;
gap: 18px;
}
.calendar-panel {
background: linear-gradient(180deg, rgba(250, 245, 255, 0.95), rgba(255, 247, 237, 0.9));
border-radius: var(--radius-lg);
padding: 16px;
border: 1px solid rgba(124, 58, 237, 0.18);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.6);
}
.calendar-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.calendar-actions {
display: flex;
gap: 8px;
}
.calendar-title {
font-size: 18px;
font-weight: 700;
}
.calendar-tip {
font-size: 12px;
color: rgba(42, 15, 69, 0.7);
margin-top: 4px;
}
.calendar-month {
font-size: 18px;
font-weight: 700;
margin: 10px 0 12px;
color: var(--brand-ink-soft);
}
.calendar-grid {
display: grid;
gap: 8px;
}
.calendar-week {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 8px;
}
.calendar-cell {
background: rgba(255, 255, 255, 0.96);
border-radius: 12px;
padding: 8px;
min-height: 88px;
cursor: pointer;
border: 1px solid rgba(124, 58, 237, 0.12);
display: flex;
flex-direction: column;
gap: 6px;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.calendar-cell:hover {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(44, 16, 91, 0.14);
}
.calendar-cell.is-other {
opacity: 0.45;
}
.calendar-cell.is-term {
border-color: rgba(249, 115, 22, 0.5);
background: rgba(255, 247, 237, 0.92);
}
.cell-day {
font-weight: 700;
font-size: 14px;
}
.term-chip {
font-size: 11px;
padding: 2px 6px;
border-radius: 10px;
background: rgba(249, 115, 22, 0.18);
color: #c2410c;
width: fit-content;
}
.form-panel {
padding: 8px 12px;
background: rgba(255, 255, 255, 0.94);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-card);
border: 1px solid rgba(124, 58, 237, 0.12);
}
.form-shell {
padding: 4px 6px;
}
.form-title {
font-size: 16px;
font-weight: 700;
margin-bottom: 8px;
}
@media (max-width: 960px) {
.modal-body {
grid-template-columns: 1fr;
}
.search-card {
flex-direction: column;
align-items: stretch;
}
.filter-group {
flex-wrap: wrap;
}
}
</style>