fix(activity): close Phase 14 KEEP-VERIFY/EXTEND — 11 backend + 3 frontend-contract + 2 test-fidelity defects
Phase 14 (multi-type public-welfare activities) verification closure. Backend (D1-D11): - D1 CRITICAL: ActivityMapper.countRemainingCapacity had no @Select and no XML -> activity detail / register / approve all threw Invalid bound statement. Removed the shell; unified on ActivityRegistrationMapper. - D2 CRITICAL: register & approve were read-then-write -> oversold under concurrency. Replaced with single atomic SQL (insertIfCapacityAvailable / approveIfCapacityAvailable, affectedRows==0 => full). - D3 HIGH: activityType / orgRole never validated against production chk_activity_type / chk_org_role -> 500 only on PostgreSQL. - D4 HIGH: ActivityListItemDTO had no activityType -> miniapp category tabs always empty (Gate failure). - D5: male/female counts were fabricated as approvedCount/2; now real aggregation over user_profiles.gender. - D6: minAge/maxAge was a TODO no-op; added assertAgeInRange. - D7: registration_policy write-only; per-type eligibility now enforced (identity / local eligibility / gender quota). - D8: POST /operator/activities dropped type, multi-org and capacity groups and set organizer_org_id to the operator user id; now delegates to createActivityFromAdmin (single implementation). - D9: three operator write endpoints were mounted on the user-side ActivityController (dual implementation, zero callers) -> moved. - D10: NPE on soft-deleted activity in getMyActivities; NPE/500 on missing admin form fields (Instant.parse(null)); tolerant time parsing added. - D11: DTO lacked registeredCount / status / coverFileId / description -> card always showed 0 registered, "view detail", no cover, no summary. Frontend contract (F1-F3): - F1: miniapp category code "classroom" is not in the production whitelist (should be relationship_education); three types had no tab at all. - F2: ui-activity-card read 7 fields that do not exist on the backend; now reads only real DTO fields, plus per-type badge and eligibility hint. - F3: admin-web had no activity management page at all; added ActivityManagementView.vue (type-driven dynamic form, multi-org, capacity groups, publish/cancel) + /activities route and nav entry. Test fidelity (T1-T2): - T1: deleted two assertTrue(true) placeholder tests (testNoOverbooking, testNoDuplicateRegistration) that covered the exact broken invariants. - T2: test schema lacked V023 columns/constraints and three tables; the activity tables were only in the OPTIONAL set (zero coverage) -> schema completed and moved to REQUIRED_IN_TEST_SCHEMA. Tests: ActivityPhase14Test (23 cases), ActivityConstraintGuardTest (9 cases, parses real db/migration/V*.sql plus miniapp source). Two negative-verification rounds proved both guard families actually turn red. Regression: 450 tests / 0 failures / 0 errors / 3 skipped / BUILD SUCCESS (baseline 420). admin-web vue-tsc 0 error, vite build OK.
This commit is contained in:
@@ -170,7 +170,7 @@ const navigation: NavigationItem[] = [
|
||||
{ label: '匹配管理', path: '/activities/interests', permissions: ['activity.review_registration'], icon: StarFilled },
|
||||
{ label: '公益红娘', path: '/referral', permissions: ['referral.manage'], icon: Connection },
|
||||
{ label: '公益咨询', path: '/counseling', permissions: ['counseling.service.view'], icon: ChatDotRound },
|
||||
{ label: '活动管理', path: '/activities/interests', permissions: ['activity.review_registration'], icon: Calendar },
|
||||
{ label: '活动管理', path: '/activities', permissions: ['activity.view'], icon: Calendar },
|
||||
{ label: '内容管理', path: '/content', permissions: ['content.view'], icon: Reading },
|
||||
{ label: '善缘服务站', path: '/service-station', permissions: ['service_location.view'], icon: Location },
|
||||
{ label: '举报风控', path: '/risk/reports', permissions: ['risk.report.view'], icon: Warning },
|
||||
|
||||
@@ -0,0 +1,453 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>活动管理</span>
|
||||
<el-button type="primary" @click="openCreate">新建活动</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table :data="tableData" v-loading="loading" border stripe>
|
||||
<el-table-column prop="title" label="活动名称" min-width="180" />
|
||||
<el-table-column prop="activityType" label="活动类型" width="130">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small">{{ typeText(row.activityType) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="110">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 'registration_open' ? 'success' : 'info'">
|
||||
{{ statusText(row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="totalLimit" label="名额" width="80" />
|
||||
<el-table-column prop="startAt" label="开始时间" width="170">
|
||||
<template #default="{ row }">{{ formatTime(row.startAt) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="260">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="success" :disabled="row.status !== 'draft'"
|
||||
@click="publish(row)">发布</el-button>
|
||||
<el-button size="small" @click="openOrgs(row)">组织/分组</el-button>
|
||||
<el-button size="small" type="danger"
|
||||
:disabled="row.status !== 'draft' && row.status !== 'registration_open'"
|
||||
@click="cancel(row)">取消</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<!-- ==================== 动态表单:字段随活动类型变化 ==================== -->
|
||||
<el-dialog v-model="formVisible" :title="editId ? '编辑活动' : '新建活动'" width="720px">
|
||||
<el-form :model="form" label-width="120px">
|
||||
<el-form-item label="活动类型" required>
|
||||
<el-select v-model="form.activityType" style="width: 100%">
|
||||
<el-option v-for="t in ACTIVITY_TYPES" :key="t.code" :label="t.label" :value="t.code" />
|
||||
</el-select>
|
||||
<div class="form-hint">{{ typeHint(form.activityType) }}</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="活动名称" required>
|
||||
<el-input v-model="form.title" placeholder="例如:丹阳市青年公益联谊第 12 期" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="活动简介">
|
||||
<el-input v-model="form.description" type="textarea" :rows="2" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="开始时间" required>
|
||||
<el-date-picker v-model="form.startAt" type="datetime" placeholder="选择开始时间"
|
||||
value-format="YYYY-MM-DDTHH:mm:ss" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="结束时间" required>
|
||||
<el-date-picker v-model="form.endAt" type="datetime" placeholder="选择结束时间"
|
||||
value-format="YYYY-MM-DDTHH:mm:ss" style="width: 100%" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="地点名称"><el-input v-model="form.locationName" /></el-form-item>
|
||||
<el-form-item label="详细地址"><el-input v-model="form.locationAddress" /></el-form-item>
|
||||
|
||||
<!-- 动态区 1:仅相亲 / 兴趣社交按性别名额 -->
|
||||
<template v-if="usesGenderQuota">
|
||||
<el-form-item label="男性名额"><el-input-number v-model="form.maleLimit" :min="0" /></el-form-item>
|
||||
<el-form-item label="女性名额"><el-input-number v-model="form.femaleLimit" :min="0" /></el-form-item>
|
||||
</template>
|
||||
|
||||
<el-form-item label="总名额"><el-input-number v-model="form.totalLimit" :min="1" /></el-form-item>
|
||||
|
||||
<!-- 动态区 2:年龄门槛(选填,留空=不限) -->
|
||||
<el-form-item label="年龄下限">
|
||||
<el-input-number v-model="form.minAge" :min="18" :max="80" :value-on-clear="null" placeholder="不限" />
|
||||
</el-form-item>
|
||||
<el-form-item label="年龄上限">
|
||||
<el-input-number v-model="form.maxAge" :min="18" :max="80" :value-on-clear="null" placeholder="不限" />
|
||||
</el-form-item>
|
||||
|
||||
<!-- 动态区 3:仅相亲 / 兴趣社交有互选环节 -->
|
||||
<el-form-item v-if="form.postEventInterestEnabled !== undefined && hasPostEventInterest" label="活动后互选">
|
||||
<el-switch v-model="form.postEventInterestEnabled" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="报名需审核">
|
||||
<el-switch v-model="form.needReview" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="目标群体">
|
||||
<el-input v-model="form.targetGroup" placeholder="例如:丹阳市机关企事业单位青年" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="主办组织">
|
||||
<el-select v-model="form.organizerOrgId" clearable style="width: 100%" placeholder="选择主办组织">
|
||||
<el-option v-for="o in organizations" :key="o.id" :label="o.name" :value="o.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="协作组织">
|
||||
<div v-for="(p, i) in form.partnerOrganizations" :key="i" class="partner-row">
|
||||
<el-select v-model="p.organizationId" placeholder="组织" style="width: 220px">
|
||||
<el-option v-for="o in organizations" :key="o.id" :label="o.name" :value="o.id" />
|
||||
</el-select>
|
||||
<el-select v-model="p.role" style="width: 150px">
|
||||
<el-option v-for="r in ORG_ROLES" :key="r.code" :label="r.label" :value="r.code" />
|
||||
</el-select>
|
||||
<el-button size="small" type="danger" @click="removePartner(i)">删除</el-button>
|
||||
</div>
|
||||
<el-button size="small" @click="addPartner">+ 添加协作组织</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="formVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="save">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- ==================== 多组织 / 容量分组 ==================== -->
|
||||
<el-dialog v-model="orgVisible" title="活动组织与容量分组" width="640px">
|
||||
<h4>组织关系</h4>
|
||||
<el-table :data="orgs" border size="small">
|
||||
<el-table-column prop="organizationId" label="组织ID" min-width="160" />
|
||||
<el-table-column prop="name" label="名称" min-width="140" />
|
||||
<el-table-column prop="role" label="角色" width="120">
|
||||
<template #default="{ row }">{{ roleText(row.role) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="inline-form">
|
||||
<el-select v-model="newOrg.organizationId" placeholder="组织" style="width: 200px">
|
||||
<el-option v-for="o in organizations" :key="o.id" :label="o.name" :value="o.id" />
|
||||
</el-select>
|
||||
<el-select v-model="newOrg.role" style="width: 140px">
|
||||
<el-option v-for="r in ORG_ROLES" :key="r.code" :label="r.label" :value="r.code" />
|
||||
</el-select>
|
||||
<el-button size="small" type="primary" @click="addOrg">添加组织</el-button>
|
||||
</div>
|
||||
|
||||
<h4 style="margin-top: 18px">容量分组</h4>
|
||||
<el-table :data="groups" border size="small">
|
||||
<el-table-column prop="groupCode" label="分组代码" min-width="140" />
|
||||
<el-table-column prop="capacity" label="容量" width="80" />
|
||||
<el-table-column prop="approvedCount" label="已通过" width="80" />
|
||||
<el-table-column prop="ruleDefinition" label="规则" min-width="180" />
|
||||
</el-table>
|
||||
|
||||
<div class="inline-form">
|
||||
<el-input v-model="newGroup.groupCode" placeholder="如 male_30_40" style="width: 180px" />
|
||||
<el-input-number v-model="newGroup.capacity" :min="1" style="width: 120px" />
|
||||
<el-input v-model="newGroup.ruleDefinition" placeholder='如 {"gender":"male"}' style="width: 200px" />
|
||||
<el-button size="small" type="primary" @click="addGroup">添加分组</el-button>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="orgVisible = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 活动管理(V1.1 Phase 14)
|
||||
*
|
||||
* 关键点:**动态表单** —— 字段随 activityType 变化:
|
||||
* - matchmaking / interest_social:显示男女名额 + 活动后互选(有互选环节)
|
||||
* - relationship_education / marriage_custom_culture / anti_fraud:公益科普,
|
||||
* 不显示性别名额与互选,资格门槛也不同(见 typeHint)
|
||||
* - premarital_guidance:需实名,无互选
|
||||
*
|
||||
* 类型与角色枚举必须与后端生产 CHECK 白名单一致:
|
||||
* ActivityConstraintGuardTest 会解析 db/migration/V*.sql 校验后端常量,
|
||||
* 本页的下拉项同样不得偏离该白名单。
|
||||
*/
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { get, post, put } from '@/api/client'
|
||||
|
||||
const ACTIVITY_TYPES = [
|
||||
{ code: 'matchmaking', label: '公益联谊' },
|
||||
{ code: 'interest_social', label: '兴趣社交' },
|
||||
{ code: 'relationship_education', label: '婚恋课堂' },
|
||||
{ code: 'premarital_guidance', label: '婚前辅导' },
|
||||
{ code: 'marriage_custom_culture', label: '婚俗文化' },
|
||||
{ code: 'anti_fraud', label: '反诈宣传' },
|
||||
]
|
||||
|
||||
const ORG_ROLES = [
|
||||
{ code: 'host', label: '主办' },
|
||||
{ code: 'organizer', label: '承办' },
|
||||
{ code: 'co_organizer', label: '协办' },
|
||||
{ code: 'partner', label: '合作方' },
|
||||
{ code: 'venue_partner', label: '场地合作方' },
|
||||
]
|
||||
|
||||
const TYPE_TEXT: Record<string, string> = Object.fromEntries(ACTIVITY_TYPES.map((t) => [t.code, t.label]))
|
||||
const ROLE_TEXT: Record<string, string> = Object.fromEntries(ORG_ROLES.map((r) => [r.code, r.label]))
|
||||
|
||||
/** 资格策略提示:与后端 ActivityService#resolveEligibility 保持一致 */
|
||||
const TYPE_HINT: Record<string, string> = {
|
||||
matchmaking: '需实名认证 + 丹阳本地资格核验;按男女名额分别扣减;活动后开放互选',
|
||||
interest_social: '按男女名额分别扣减;活动后开放互选',
|
||||
premarital_guidance: '需实名认证;只按总名额',
|
||||
relationship_education: '公益科普,不设实名与本地资格门槛;只按总名额',
|
||||
marriage_custom_culture: '公益科普,不设实名与本地资格门槛;只按总名额',
|
||||
anti_fraud: '公益科普,不设实名与本地资格门槛;只按总名额',
|
||||
}
|
||||
|
||||
const POST_EVENT_TYPES = ['matchmaking', 'interest_social']
|
||||
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const tableData = ref<any[]>([])
|
||||
const organizations = ref<any[]>([])
|
||||
|
||||
const formVisible = ref(false)
|
||||
const editId = ref('')
|
||||
const form = ref(emptyForm())
|
||||
|
||||
const orgVisible = ref(false)
|
||||
const currentId = ref('')
|
||||
const orgs = ref<any[]>([])
|
||||
const groups = ref<any[]>([])
|
||||
const newOrg = ref({ organizationId: '', role: 'partner' })
|
||||
const newGroup = ref({ groupCode: '', capacity: 20, ruleDefinition: '' })
|
||||
|
||||
const usesGenderQuota = computed(() => ['matchmaking', 'interest_social'].includes(form.value.activityType))
|
||||
const hasPostEventInterest = computed(() => POST_EVENT_TYPES.includes(form.value.activityType))
|
||||
|
||||
function emptyForm() {
|
||||
return {
|
||||
activityType: 'matchmaking',
|
||||
title: '',
|
||||
description: '',
|
||||
startAt: '',
|
||||
endAt: '',
|
||||
locationName: '',
|
||||
locationAddress: '',
|
||||
totalLimit: 50,
|
||||
maleLimit: 25,
|
||||
femaleLimit: 25,
|
||||
minAge: undefined as number | undefined,
|
||||
maxAge: undefined as number | undefined,
|
||||
needReview: true,
|
||||
postEventInterestEnabled: false,
|
||||
targetGroup: '',
|
||||
organizerOrgId: '',
|
||||
partnerOrganizations: [] as Array<{ organizationId: string; role: string }>,
|
||||
}
|
||||
}
|
||||
|
||||
function authHeader() {
|
||||
return { Authorization: `Bearer ${localStorage.getItem('accessToken')}` }
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res: any = await get('/operator/activities', {}, authHeader())
|
||||
tableData.value = res.data || []
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadOrganizations() {
|
||||
try {
|
||||
const res: any = await get('/operator/organizations', {}, authHeader())
|
||||
organizations.value = res.data || []
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
editId.value = ''
|
||||
form.value = emptyForm()
|
||||
formVisible.value = true
|
||||
}
|
||||
|
||||
function addPartner() {
|
||||
form.value.partnerOrganizations.push({ organizationId: '', role: 'partner' })
|
||||
}
|
||||
function removePartner(i: number) {
|
||||
form.value.partnerOrganizations.splice(i, 1)
|
||||
}
|
||||
|
||||
async function save() {
|
||||
if (!form.value.title) {
|
||||
ElMessage.warning('请填写活动名称')
|
||||
return
|
||||
}
|
||||
if (!form.value.startAt || !form.value.endAt) {
|
||||
ElMessage.warning('请填写开始与结束时间')
|
||||
return
|
||||
}
|
||||
saving.value = true
|
||||
try {
|
||||
const body: Record<string, unknown> = { ...form.value }
|
||||
// 非性别名额类型不下发男女名额,避免后端误按性别限制
|
||||
if (!usesGenderQuota.value) {
|
||||
delete body.maleLimit
|
||||
delete body.femaleLimit
|
||||
}
|
||||
await post('/operator/activities', body, authHeader())
|
||||
ElMessage.success('活动已创建')
|
||||
formVisible.value = false
|
||||
loadData()
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || '创建失败')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function publish(row: any) {
|
||||
try {
|
||||
await put(`/operator/activities/${row.id}/publish`, {}, authHeader())
|
||||
ElMessage.success('已发布')
|
||||
loadData()
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || '发布失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function cancel(row: any) {
|
||||
try {
|
||||
await put(`/operator/activities/${row.id}/cancel`, {}, authHeader())
|
||||
ElMessage.success('已取消')
|
||||
loadData()
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || '取消失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function openOrgs(row: any) {
|
||||
currentId.value = row.id
|
||||
orgVisible.value = true
|
||||
await Promise.all([loadOrgs(), loadGroups()])
|
||||
}
|
||||
|
||||
async function loadOrgs() {
|
||||
try {
|
||||
const res: any = await get(`/operator/activities/${currentId.value}/organizations`, {}, authHeader())
|
||||
orgs.value = res.data || []
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
async function loadGroups() {
|
||||
try {
|
||||
const res: any = await get(`/operator/activities/${currentId.value}/capacity-groups`, {}, authHeader())
|
||||
groups.value = res.data || []
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
async function addOrg() {
|
||||
if (!newOrg.value.organizationId) {
|
||||
ElMessage.warning('请选择组织')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await post(`/operator/activities/${currentId.value}/organizations`, newOrg.value, authHeader())
|
||||
ElMessage.success('已添加')
|
||||
await loadOrgs()
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || '添加失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function addGroup() {
|
||||
if (!newGroup.value.groupCode) {
|
||||
ElMessage.warning('请填写分组代码')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await post(`/operator/activities/${currentId.value}/capacity-groups`, newGroup.value, authHeader())
|
||||
ElMessage.success('已添加')
|
||||
await loadGroups()
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || '添加失败')
|
||||
}
|
||||
}
|
||||
|
||||
function typeText(t?: string) {
|
||||
return (t && TYPE_TEXT[t]) || t || '-'
|
||||
}
|
||||
function roleText(r?: string) {
|
||||
return (r && ROLE_TEXT[r]) || r || '-'
|
||||
}
|
||||
function typeHint(t?: string) {
|
||||
return (t && TYPE_HINT[t]) || ''
|
||||
}
|
||||
function statusText(s?: string) {
|
||||
const map: Record<string, string> = {
|
||||
draft: '草稿',
|
||||
registration_open: '报名中',
|
||||
registration_closed: '报名结束',
|
||||
ongoing: '进行中',
|
||||
ended: '已结束',
|
||||
cancelled: '已取消',
|
||||
}
|
||||
return (s && map[s]) || s || '-'
|
||||
}
|
||||
function formatTime(t?: string) {
|
||||
return t ? new Date(t).toLocaleString('zh-CN') : '-'
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
loadOrganizations()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.form-hint {
|
||||
margin-top: 4px;
|
||||
color: #8a90a2;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.partner-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.inline-form {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -18,6 +18,8 @@ const routes: RouteRecordRaw[] = [
|
||||
children: [
|
||||
{ path: 'dashboard', name: 'Dashboard', component: () => import('@/views/DashboardView.vue'), meta: { title: '工作台' } },
|
||||
{ path: 'review', name: 'Review', component: () => import('@/modules/review/ReviewView.vue'), meta: { title: '资料审核', permissions: ['review.view'] } },
|
||||
// V1.1 Phase 14:活动管理(类型化活动 + 动态表单 + 多组织 + 容量分组)
|
||||
{ path: 'activities', name: 'ActivityManagement', component: () => import('@/modules/activity/ActivityManagementView.vue'), meta: { title: '活动管理', permissions: ['activity.view'] } },
|
||||
{ path: 'activities/interests', name: 'ActivityInterests', component: () => import('@/modules/activity/ActivityInterestView.vue'), meta: { title: '活动互选管理', permissions: ['activity.review_registration'] } },
|
||||
{ path: 'risk/reports', name: 'ReportCenter', component: () => import('@/modules/risk/ReportCenterView.vue'), meta: { title: '举报中心', permissions: ['risk.report.view'] } },
|
||||
{ path: 'appeals', name: 'AppealCenter', component: () => import('@/modules/appeal/AppealCenterView.vue'), meta: { title: '申诉管理', permissions: ['risk.report.process'] } },
|
||||
|
||||
Reference in New Issue
Block a user