536 lines
15 KiB
Vue
536 lines
15 KiB
Vue
<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>
|