build / build (push) Successful in 4m11s
- SpaceSide:点击「积分」可查看积分来源明细(发布/评论逐条带分值,分页), 4 个空间页(ModuleList/RecordDetail/RecordEdit/Timeline)传入 :points-userid - api:新增 pointsSourceList(/PointsSourceList);ProfessionDetail 改注为纯读 (阅读数由 VisitCreate 记足迹时 +1) - layout/App:管理员判定改用 BoaUser.type(1 超管 / 2 学校管理员)或区局账号, 后台管理入口落地栏目管理 #/module;App.vue 透传 user.type - ModuleBar/profession-ui:栏目条 auto 外边距只对「更多」项生效(上级栏目组项 的下拉包装层留在原位排队),项间距 6px→5px - 页脚备案信息更新为 ©2024-2026 豫ICP
262 lines
10 KiB
Vue
262 lines
10 KiB
Vue
<script setup lang="ts">
|
||
import { ref, watch, onMounted } from "vue";
|
||
import { useRoute, useRouter } from "vue-router";
|
||
import { Message } from "view-ui-plus";
|
||
import { professionApi, mouduleApi, baseApi } from "@/api/module/profession";
|
||
import SpaceSide from "@/components/SpaceSide.vue";
|
||
import CrumbTags from "@/components/CrumbTags.vue";
|
||
|
||
/**
|
||
* 教师个人空间 ·「我的首页」(专业发展足迹,方案D原型 page-timeline):
|
||
* 左侧栏(SpaceSide 完整版:蓝渐变头/统计行/积分进度/栏目菜单/本月概览/最近访客),
|
||
* 右主区 crumbs「我的/他的首页」+ panel 时间轴(year-block 年份圆点折叠 +
|
||
* tl-item:tdot/t-date/t-module【栏目】/t-title/t-excerpt 摘要)。
|
||
* ?userid= 空 = 自己;点条目进详情,点栏目进该教师栏目列表。
|
||
*/
|
||
|
||
const route = useRoute();
|
||
const router = useRouter();
|
||
|
||
const userid = ref<number | null>(null);
|
||
const isMine = ref(true);
|
||
const mineName = ref("");
|
||
const selfAvatar = ref("");
|
||
const himWord = ref("我的");
|
||
const spaceTeacher = ref<{ name: string; points: number }>({ name: "", points: 0 });
|
||
const spaceModules = ref<any[]>([]);
|
||
const spaceVisitors = ref<any[]>([]);
|
||
const space = ref<any>({ teacherName: "", points: 0, recordTotal: 0, visitTotal: 0, moduleCounts: [], month: {} });
|
||
const years = ref<any[]>([]);
|
||
const loading = ref(false);
|
||
|
||
const totalCount = () => years.value.reduce((s: number, y: any) => s + (y.itemlist?.length || 0), 0);
|
||
|
||
async function load() {
|
||
loading.value = true;
|
||
try {
|
||
years.value = (await professionApi.timeline(userid.value!).send()) || [];
|
||
} catch (e: any) {
|
||
Message.error(e.message || "获取时间轴失败");
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
}
|
||
|
||
/** 个人空间侧栏数据(与栏目列表页同源) */
|
||
async function loadSpace(uid: number) {
|
||
try {
|
||
const p: any = await professionApi.myPoints(uid).send();
|
||
spaceTeacher.value = { name: p?.teacherName || "", points: p?.pointsnum ?? 0 };
|
||
} catch {
|
||
spaceTeacher.value = { name: "", points: 0 };
|
||
}
|
||
try {
|
||
spaceModules.value = (await mouduleApi.myModuleList(uid).send()) || [];
|
||
} catch {
|
||
spaceModules.value = [];
|
||
}
|
||
try {
|
||
space.value = (await professionApi.spaceOverview(uid).send()) || space.value;
|
||
} catch {
|
||
/* 概览失败忽略 */
|
||
}
|
||
try {
|
||
spaceVisitors.value = (await professionApi.visitList({ userid: uid, limit: 50 }).send()) || [];
|
||
} catch {
|
||
spaceVisitors.value = [];
|
||
}
|
||
}
|
||
|
||
/* ============ 年份折叠(默认全部展开,对齐老页面) ============ */
|
||
const openYears = ref<number[]>([]);
|
||
watch(years, v => {
|
||
openYears.value = v.map((y: any) => y.year);
|
||
});
|
||
function toggleYear(y: number) {
|
||
const i = openYears.value.indexOf(y);
|
||
if (i >= 0) openYears.value.splice(i, 1);
|
||
else openYears.value.push(y);
|
||
}
|
||
function isOpen(y: number) {
|
||
return openYears.value.includes(y);
|
||
}
|
||
|
||
/** 正文摘要:去 HTML 标签后截 120 字(原型 t-excerpt 两行截断) */
|
||
function excerpt(remark?: string) {
|
||
if (!remark) return "";
|
||
const text = String(remark).replace(/<[^>]+>/g, "").replace(/ /g, " ").trim();
|
||
return text.length > 120 ? text.slice(0, 120) + "…" : text;
|
||
}
|
||
|
||
/** 相对时间标签(时间元素):今天/昨天/N 天前/N 个月前/N 年前 */
|
||
function relTime(y: any, it: any): string {
|
||
const d = new Date(Number(y.year), (Number(it.month) || 1) - 1, Number(it.day) || 1);
|
||
const today = new Date();
|
||
today.setHours(0, 0, 0, 0);
|
||
const diff = Math.round((today.getTime() - d.getTime()) / 86400000);
|
||
if (diff <= 0) return "今天";
|
||
if (diff === 1) return "昨天";
|
||
if (diff < 30) return diff + " 天前";
|
||
if (diff < 365) return Math.round(diff / 30) + " 个月前";
|
||
return Math.round(diff / 365) + " 年前";
|
||
}
|
||
|
||
function goDetail(id: number) {
|
||
router.push({ path: "/record/detail", query: { serid: id } });
|
||
}
|
||
function goModuleList(m: any) {
|
||
if (!m?.id) return;
|
||
router.push({ path: "/module/list", query: { mouid: m.id, ...(userid.value ? { userid: userid.value } : {}) } });
|
||
}
|
||
function goVisitor(v: any) {
|
||
router.push({ path: "/timeline", query: { userid: v.visitorId } });
|
||
}
|
||
|
||
onMounted(async () => {
|
||
// 先取当前登录人,再判定空间归属(与栏目列表页一致:query userid = 登录人 → 我的)
|
||
let mineId: number | null = null;
|
||
try {
|
||
const u: any = await baseApi.getUserInfo().send();
|
||
mineId = u?.id ?? null;
|
||
mineName.value = u?.name || "";
|
||
selfAvatar.value = u?.profilePicture || "";
|
||
} catch {
|
||
mineId = null;
|
||
}
|
||
mineIdCache = mineId;
|
||
await applyRoute(mineId);
|
||
});
|
||
|
||
/** 路由参数应用(onMounted 与 watch 复用) */
|
||
let mineIdCache: number | null = null;
|
||
async function applyRoute(mineId: number | null) {
|
||
const q = Number(route.query.userid || 0);
|
||
userid.value = q || mineId;
|
||
isMine.value = userid.value != null && userid.value === mineId;
|
||
himWord.value = isMine.value ? "我的" : "他的";
|
||
if (!userid.value) {
|
||
Message.warning("未获取到用户信息");
|
||
return;
|
||
}
|
||
loadSpace(userid.value);
|
||
load();
|
||
}
|
||
|
||
/* 同组件路由复用(我的 ↔ 他的)时 onMounted 不重跑,需按 query.userid 重载 */
|
||
watch(
|
||
() => route.query.userid,
|
||
async () => {
|
||
if (route.name !== "teacher_timeline") return; // 已离开本页(组件卸载途中)
|
||
const q = Number(route.query.userid || 0);
|
||
const target = q || mineIdCache;
|
||
if (target === userid.value) return;
|
||
await applyRoute(mineIdCache);
|
||
}
|
||
);
|
||
</script>
|
||
|
||
<template>
|
||
<div class="main-grid">
|
||
<!-- ============ 个人空间侧栏(完整版) ============ -->
|
||
<SpaceSide
|
||
:him-word="himWord"
|
||
:avatar="isMine ? selfAvatar : ''"
|
||
:teacher-name="spaceTeacher.name || (isMine ? mineName : '—')"
|
||
:points="spaceTeacher.points"
|
||
:modules="spaceModules"
|
||
:visitors="spaceVisitors"
|
||
:active-mouid="null"
|
||
:overview="space"
|
||
:home-current="true"
|
||
:points-userid="userid"
|
||
@home="userid && router.push({ path: '/timeline', query: { userid } })"
|
||
@module="goModuleList"
|
||
@visitor="goVisitor"
|
||
/>
|
||
|
||
<!-- ============ 右主区:我的首页 · 发展足迹时间轴 ============ -->
|
||
<div class="col-main">
|
||
<CrumbTags :tags="[{ text: himWord + '首页' }]"></CrumbTags>
|
||
|
||
<section class="panel tl-panel">
|
||
<Spin v-if="loading" size="large" fix></Spin>
|
||
<!-- 面板头:沿用原型 panel-hd(左竖条 h3 + ext),右侧保留发布入口(仅本人) -->
|
||
<div class="panel-hd tl-panel-hd">
|
||
<h3>发展足迹<span class="tl-count">共 {{ totalCount() }} 条</span></h3>
|
||
</div>
|
||
<div class="tl-body">
|
||
<div class="timeline">
|
||
<div v-for="(y, yi) in years" :key="y.year" class="year-block">
|
||
<div class="year-hd" @click="toggleYear(y.year)">
|
||
<span class="ydot">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><rect x="3" y="4.5" width="18" height="16.5" rx="2.5"/><path d="M8 2.5v4M16 2.5v4M3 9.5h18"/></svg>
|
||
</span>
|
||
<h3>{{ y.year }} <i>年</i></h3>
|
||
<span class="ycnt">{{ (y.itemlist || []).length }} 篇</span>
|
||
<span class="yarrow" :class="{ closed: !isOpen(y.year) }">
|
||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>
|
||
</span>
|
||
</div>
|
||
<div v-show="isOpen(y.year)">
|
||
<div v-for="(it, ii) in y.itemlist" :key="it.id" class="tl-item" :style="{ '--i': ii }">
|
||
<span class="tdot"></span>
|
||
<div class="t-head">
|
||
<span class="t-date">
|
||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></svg>
|
||
{{ String(it.month).padStart(2, "0") }}-{{ String(it.day).padStart(2, "0") }}
|
||
<em>· {{ relTime(y, it) }}</em>
|
||
</span>
|
||
<span class="t-module">{{ it.moduleName || "—" }}</span>
|
||
</div>
|
||
<a class="t-title" @click="goDetail(it.id)">{{ it.title }}</a>
|
||
<div v-if="excerpt(it.remark)" class="t-excerpt">{{ excerpt(it.remark) }}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 时间轴终点:「现在」脉冲节点 -->
|
||
<div v-if="years.length" class="tl-now">
|
||
<span class="ndot"></span><span>现在</span>
|
||
</div>
|
||
|
||
<div v-if="!years.length && !loading" class="tl-empty">
|
||
<Icon type="ios-time-outline" size="44"></Icon>
|
||
<p>暂无专业发展记录</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
/* main-grid / col-main / panel / crumbs / timeline / year-block / tl-item 等由全局 profession-ui.css 提供 */
|
||
.tl-panel {
|
||
position: relative;
|
||
}
|
||
.tl-panel-hd h3 {
|
||
display: flex;
|
||
align-items: baseline;
|
||
gap: 8px;
|
||
}
|
||
.tl-count {
|
||
font-family: var(--font-b, sans-serif);
|
||
font-size: 12px;
|
||
font-weight: 400;
|
||
color: var(--muted, #8fa9c4);
|
||
}
|
||
.tl-body {
|
||
padding: 14px 28px 22px;
|
||
}
|
||
.tl-empty {
|
||
padding: 56px 0 48px;
|
||
text-align: center;
|
||
color: #c5c8ce;
|
||
}
|
||
.tl-empty p {
|
||
margin: 10px 0 0;
|
||
font-size: 13px;
|
||
}
|
||
</style>
|