租户端基础设施: - 新增工作台首页(欢迎信息/统计/待办/快捷操作/新手引导) - 新增机构信息管理页(自助查看编辑机构信息) - 修复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>
172 lines
6.1 KiB
Vue
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>
|