library-picturebook-activity/frontend/src/views/system/tenant-info/Index.vue
aid 9215465bd5 Day5: 租户端全面优化 + 数据统计看板 + 成果发布完善
租户端基础设施:
- 新增工作台首页(欢迎信息/统计/待办/快捷操作/新手引导)
- 新增机构信息管理页(自助查看编辑机构信息)
- 修复403报错(fetchTenants加超管守卫)
- 修复权限(log:read/notice:update/notice:delete/contest:work:read)
- 修复评审规则组件映射

活动管理全模块优化(机构端视角):
- 活动列表:加统计概览+精简列+筛选自动查询+发布弹窗修复+操作逻辑优化
- 创建/编辑活动:重构布局(去card嵌套+栅格响应式+分区卡片)
- 评委管理:统一主色调+冻结确认+导入导出disabled
- 报名管理:去Tab+统计+审核状态列+批量审核接口
- 报名记录:统计概览+去机构列+撤销审核+返回按钮+去参与方式列
- 作品管理:去Tab+统计+递交进度彩色+筛选修复(assignStatus/submitTime)
- 评审进度:去Tab+统计+实际完成率状态+筛选修复
- 评审规则:表格加评委数/计算方式+描述列修复+删除保护
- 成果发布:去Tab+统计+操作文案优化
- 通知公告:统一主色调+发布确认+操作逻辑+状态筛选+时间范围

成果发布详情功能补全:
- 计算得分/排名/设置奖项三步操作流程
- 排名列(金银铜徽章)+奖项列+奖项筛选
- 自定义奖项(动态添加行替代硬编码一二三等奖)
- 后端AutoSetAwardsDto改为awards数组格式

数据统计看板(新模块):
- 后端analytics module(overview+review两个接口)
- 运营概览:6指标卡片+报名转化漏斗+ECharts月度趋势+活动对比表
- 评审分析:4效率卡片+评委工作量表+ECharts奖项分布饼图
- 菜单注册:数据统计→运营概览+评审分析

Bug修复:
- 超管重置其他租户用户密码报"用户不存在"
- gdlib登录快捷标签密码不一致
- 分配评委去掉评审时间限制

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 20:02:24 +08:00

172 lines
6.1 KiB
Vue

<template>
<div class="tenant-info-page">
<a-card class="title-card">
<template #title>机构信息</template>
</a-card>
<a-spin :spinning="loading">
<div v-if="tenant" class="info-content">
<!-- 基本信息卡片 -->
<a-card title="基本信息" :bordered="false" class="section-card">
<a-descriptions :column="2" bordered size="small">
<a-descriptions-item label="机构名称" :span="2">
<div class="editable-field">
<span v-if="!editing">{{ tenant.name }}</span>
<a-input v-else v-model:value="editForm.name" style="max-width: 300px" />
</div>
</a-descriptions-item>
<a-descriptions-item label="机构编码">
<a-tag color="blue">{{ tenant.code }}</a-tag>
</a-descriptions-item>
<a-descriptions-item label="机构类型">
<a-tag :color="tenantTypeColor(tenant.tenantType)">{{ tenantTypeLabel(tenant.tenantType) }}</a-tag>
</a-descriptions-item>
<a-descriptions-item label="登录地址" :span="2">
<span class="url-text">/{{ tenant.code }}/login</span>
<a-button type="link" size="small" @click="copyLoginUrl">
<copy-outlined /> 复制
</a-button>
</a-descriptions-item>
<a-descriptions-item label="状态">
<a-badge :status="tenant.validState === 1 ? 'success' : 'error'" :text="tenant.validState === 1 ? '正常' : '停用'" />
</a-descriptions-item>
<a-descriptions-item label="创建时间">{{ formatDate(tenant.createTime) }}</a-descriptions-item>
<a-descriptions-item label="机构描述" :span="2">
<div class="editable-field">
<span v-if="!editing">{{ tenant.description || '暂无描述' }}</span>
<a-textarea v-else v-model:value="editForm.description" :rows="3" style="max-width: 400px" placeholder="机构描述" />
</div>
</a-descriptions-item>
</a-descriptions>
<div class="edit-actions" style="margin-top: 16px">
<template v-if="!editing">
<a-button type="primary" @click="startEdit">
<template #icon><edit-outlined /></template>
编辑信息
</a-button>
</template>
<template v-else>
<a-space>
<a-button type="primary" :loading="saving" @click="handleSave">保存</a-button>
<a-button @click="cancelEdit">取消</a-button>
</a-space>
</template>
</div>
</a-card>
<!-- 统计信息 -->
<a-card title="数据概况" :bordered="false" class="section-card" style="margin-top: 16px">
<div class="stats-grid">
<div class="stat-item">
<span class="stat-value">{{ tenant._count?.users || 0 }}</span>
<span class="stat-label">用户数</span>
</div>
<div class="stat-item">
<span class="stat-value">{{ tenant._count?.roles || 0 }}</span>
<span class="stat-label">角色数</span>
</div>
</div>
</a-card>
</div>
</a-spin>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import { EditOutlined, CopyOutlined } from '@ant-design/icons-vue'
import request from '@/utils/request'
import dayjs from 'dayjs'
const loading = ref(true)
const saving = ref(false)
const editing = ref(false)
const tenant = ref<any>(null)
const editForm = reactive({ name: '', description: '' })
const tenantTypeLabel = (type: string) => {
const map: Record<string, string> = { library: '图书馆', kindergarten: '幼儿园', school: '学校', institution: '社会机构', other: '其他' }
return map[type] || type
}
const tenantTypeColor = (type: string) => {
const map: Record<string, string> = { library: 'purple', kindergarten: 'green', school: 'blue', institution: 'orange', other: 'default' }
return map[type] || 'default'
}
const formatDate = (d: string) => d ? dayjs(d).format('YYYY-MM-DD') : '-'
const copyLoginUrl = () => {
const url = `${window.location.origin}/${tenant.value.code}/login`
navigator.clipboard.writeText(url).then(() => message.success('已复制')).catch(() => message.info(url))
}
const fetchTenant = async () => {
loading.value = true
try {
tenant.value = await request.get('/tenants/my-tenant')
} catch {
message.error('获取机构信息失败')
} finally {
loading.value = false
}
}
const startEdit = () => {
editForm.name = tenant.value.name
editForm.description = tenant.value.description || ''
editing.value = true
}
const cancelEdit = () => { editing.value = false }
const handleSave = async () => {
if (!editForm.name.trim()) { message.warning('机构名称不能为空'); return }
saving.value = true
try {
await request.patch('/tenants/my-tenant', {
name: editForm.name,
description: editForm.description || undefined,
})
message.success('保存成功')
editing.value = false
fetchTenant()
} catch {
message.error('保存失败')
} finally {
saving.value = false
}
}
onMounted(fetchTenant)
</script>
<style scoped lang="scss">
$primary: #6366f1;
.title-card { margin-bottom: 16px; border: none; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.06);
:deep(.ant-card-head) { border-bottom: none; .ant-card-head-title { font-size: 18px; font-weight: 600; } }
:deep(.ant-card-body) { padding: 0; }
}
.section-card {
border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.06);
:deep(.ant-card-head) { .ant-card-head-title { font-size: 15px; font-weight: 600; } }
}
.url-text { font-family: monospace; font-size: 13px; color: #6b7280; }
.editable-field { min-height: 22px; }
.stats-grid {
display: flex; gap: 32px;
.stat-item {
display: flex; flex-direction: column; align-items: center;
.stat-value { font-size: 28px; font-weight: 700; color: #1e1b4b; }
.stat-label { font-size: 13px; color: #9ca3af; }
}
}
</style>