Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
221 changes: 221 additions & 0 deletions src/api/department.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
import type { UserEntity } from './auth'
import { api } from './api'

export interface DepartmentMember extends UserEntity {
isLeader: boolean
}

export interface Department {
id: string
name: string
description: string
icon?: string
sortOrder: number
members: DepartmentMember[]
}

const DEFAULT_AVATAR = '/nmo-logo-large.png'

export function toAssetUrl(url?: string): string {
if (!url) return ''
if (url.startsWith('/contents/')) return `/necore${url}`
return url
}

export function getMemberAvatar(member: DepartmentMember): string {
if (member.avatar) return toAssetUrl(member.avatar)

return DEFAULT_AVATAR
}

export function getMemberTitle(member: DepartmentMember): string {
if (member.isLeader) return '负责人'
return member.tags?.[0]?.text ?? ''
}

function readApiError(err: unknown): string | null {
if (
typeof err === 'object' &&
err !== null &&
'response' in err &&
typeof (err as { response?: { data?: { error?: string } } }).response?.data?.error === 'string'
) {
return (err as { response: { data: { error: string } } }).response.data.error
}
return null
}

export async function GetDepartmentList(): Promise<Department[]> {
let result: Department[] = []
await api
.get<{ departments: Department[] }>('/department/')
.then((res) => {
result = (res.data.departments ?? [])
.sort((a, b) => a.sortOrder - b.sortOrder)
.map((department) => ({
...department,
members: (department.members ?? []).map((member) => ({
...member,
isLeader: Boolean(member.isLeader),
})),
}))
})
.catch(() => {})
return result
}

export async function CreateDepartment(payload: {
name: string
description: string
icon?: string
sortOrder?: number
}): Promise<{ id: string | null; error: string | null }> {
let id: string | null = null
let error: string | null = null
await api
.post<{ id?: string; error?: string }>('/department/create', payload)
.then((res) => {
if (res.data.error) {
error = res.data.error
} else {
id = res.data.id ?? null
}
})
.catch((err) => {
error = readApiError(err) || '创建部门失败'
})
return { id, error }
}

export async function UpdateDepartment(department: Department): Promise<string | null> {
let result: string | null = null
await api
.patch('/department/', department)
.then((res) => {
if (res.data?.error) {
result = res.data.error as string
}
})
.catch((err) => {
result = readApiError(err) || '保存部门失败'
})
return result
}

export async function UpdateDepartmentOrder(
orders: Array<{ id: string; sortOrder: number }>,
): Promise<string | null> {
let result: string | null = null
await api
.patch('/department/order', { orders })
.then((res) => {
if (res.data?.error) {
result = res.data.error as string
}
})
.catch((err) => {
result = readApiError(err) || '更新部门顺序失败'
})
return result
}

export async function DeleteDepartment(id: string): Promise<string | null> {
let result: string | null = null
await api
.delete(`/department/${encodeURIComponent(id)}`)
.then((res) => {
if (res.data?.error) {
result = res.data.error as string
}
})
.catch((err) => {
result = readApiError(err) || '删除部门失败'
})
return result
}

export async function AddDepartmentMember(
departmentId: string,
username: string,
sortOrder = 0,
isLeader = false,
): Promise<string | null> {
let result: string | null = null
await api
.post(`/department/${encodeURIComponent(departmentId)}/member`, {
username,
sortOrder,
isLeader,
})
.then((res) => {
if (res.data?.error) {
result = res.data.error as string
}
})
.catch((err) => {
result = readApiError(err) || '添加成员失败'
})
return result
}

export async function RemoveDepartmentMember(
departmentId: string,
username: string,
): Promise<string | null> {
let result: string | null = null
await api
.delete(
`/department/${encodeURIComponent(departmentId)}/member/${encodeURIComponent(username)}`,
)
.then((res) => {
if (res.data?.error) {
result = res.data.error as string
}
})
.catch((err) => {
result = readApiError(err) || '移除成员失败'
})
return result
}

export async function UpdateDepartmentMemberLeader(
departmentId: string,
username: string,
isLeader: boolean,
): Promise<string | null> {
let result: string | null = null
await api
.patch(
`/department/${encodeURIComponent(departmentId)}/member/${encodeURIComponent(username)}/leader`,
{ isLeader: Boolean(isLeader) },
)
.then((res) => {
if (res.data?.error) {
result = res.data.error as string
}
})
.catch((err) => {
result = readApiError(err) || '更新负责人状态失败'
})
return result
}

export async function UpdateDepartmentMemberOrder(
departmentId: string,
members: Array<{ username: string; sortOrder: number; isLeader: boolean }>,
): Promise<string | null> {
let result: string | null = null
await api
.patch(`/department/${encodeURIComponent(departmentId)}/member/order`, {
members,
})
.then((res) => {
if (res.data?.error) {
result = res.data.error as string
}
})
.catch((err) => {
result = readApiError(err) || '更新成员顺序失败'
})
return result
}
6 changes: 6 additions & 0 deletions src/router/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,12 @@ const router = createRouter({
component: () => import('../views/Management/Components/ClubManagementView.vue'),
meta: { title: 'NMO | 社团管理' },
},
{
path: '/management/department',
name: 'department management',
component: () => import('../views/Management/Components/DepartmentManagementView.vue'),
meta: { title: 'NMO | 部门管理' },
},
{
path: '/management/server',
name: 'server management',
Expand Down
113 changes: 94 additions & 19 deletions src/views/About/AboutView.vue
Original file line number Diff line number Diff line change
@@ -1,31 +1,62 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue'
import { GetLinkList, type LinkEntity } from '@/api/linklist'
import LinkItem from './LinkItem.vue'
import { GetDetailedIntroList, type IntroEntity } from '@/api/introlist'
import IntroItem from '@/components/IntroItem.vue'
import { GetDepartmentList, type Department } from '@/api/department'
import DepartmentSection from './DepartmentSection.vue'
import { useToast } from 'vue-toastification'

const linkList = ref<LinkEntity[]>([])
const toast = useToast()
const intros = ref<IntroEntity[]>([])
onMounted(() => {
linkList.value = GetLinkList()
const departments = ref<Department[]>([])
const loadingDepartments = ref(true)
const loadFailed = ref(false)

onMounted(async () => {
intros.value = GetDetailedIntroList()

loadingDepartments.value = true
loadFailed.value = false
try {
departments.value = await GetDepartmentList()
} catch {
loadFailed.value = true
toast.error('部门信息加载失败,请稍后重试。')
} finally {
loadingDepartments.value = false
}
})
</script>

<template>
<div class="main-area">
<div class="links-area">
<LinkItem
class="intro-item"
v-for="item in linkList"
:style="{
'--delay': `${linkList.indexOf(item) * 0.2}s`,
}"
:key="item.name"
:link="item"
/>
<div class="department-area">
<h1 id="department-area-title" class="department-area-title">部门与成员</h1>

<p v-if="loadingDepartments" class="department-status" aria-live="polite">
正在加载部门信息...
</p>
<p
v-else-if="loadFailed"
class="department-status department-status-error"
aria-live="polite"
>
部门信息加载失败。
</p>
<div v-else-if="departments.length === 0" class="department-empty-state">
<strong>暂无部门信息</strong>
<span>管理员可在后台「部门管理」中添加部门与成员。</span>
</div>

<DepartmentSection
v-for="department in departments"
:key="department.id"
:department="department"
/>
</div>
</div>

<div class="intro-area">
<h1 style="opacity: 0; animation: fade-in-down 1s ease-out forwards">
更多关于我们的事情...
Expand All @@ -45,14 +76,11 @@ onMounted(() => {
padding-top: 5rem;
padding-bottom: 5rem;
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
align-items: flex-start;
flex-direction: column;
align-items: center;
background-image: url('/background/links-background.jpg');
background-size: cover;
background-position: center;

position: relative;
}

Expand Down Expand Up @@ -83,4 +111,51 @@ onMounted(() => {
text-align: center;
font-size: clamp(1.6rem, 3vw, 2.4rem);
}

.department-area {
position: relative;
z-index: 1;
width: min(100%, 88rem);
margin: 0 auto;
padding: 0 clamp(1rem, 4vw, 3rem);
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: stretch;
}

.department-area-title {
margin: 0 0 1rem;
color: #fff;
text-align: center;
font-size: clamp(1.6rem, 3vw, 2.4rem);
}

.department-status {
margin: 0 0 1.5rem;
text-align: center;
color: rgba(255, 255, 255, 0.78);
}

.department-status-error {
color: #f0c36a;
}

.department-empty-state {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.75rem;
margin-bottom: 2rem;
padding: 2rem 1rem;
color: rgba(255, 255, 255, 0.72);
text-align: center;
background-color: rgba(0, 0, 0, 0.18);
border: 2px dashed #666;
}

.department-empty-state strong {
color: #fff;
font-size: 1.1rem;
}
</style>
Loading