- 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
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
# 规避后端 CAS 过滤器不回 CORS 头导致的跨域拦截;SSO 用本地 CasService
|
||||
# (E:\YHXT\java\user-cas-service\CasService,8443 根路径,java -jar target/cas.war)
|
||||
VITE_BASE_HOST=http://base.yunhaixingtong.com
|
||||
VITE_SSO_HOST=http://localhost:8443
|
||||
VITE_SSO_HOST=http://sso.yunhaixingtong.com
|
||||
# 留空 → serviceHost 走同源相对 /api(经 vite 代理到本地后端 8107);填绝对地址会绕过代理直连该域
|
||||
VITE_SERVICE_HOST=http://base.yunhaixingtong.com/profession/
|
||||
VITE_ZIYUANKU_HOST=https://base.bestsch.com/BschZyk
|
||||
|
||||
+2
-1
@@ -28,8 +28,9 @@ yarn.lock
|
||||
# scratch
|
||||
.temp
|
||||
|
||||
# 本地备份与临时产物(不入公开仓库)
|
||||
# 本地备份与历史构建产物(不入公开仓库)
|
||||
backup/
|
||||
dist_old/
|
||||
*.bak
|
||||
*.orig
|
||||
*.old
|
||||
|
||||
+3
-3
@@ -22,8 +22,8 @@ const layoutData = reactive({
|
||||
async function initShell() {
|
||||
try {
|
||||
const user: any = await baseApi.getUserInfo().send();
|
||||
const { id, name } = user || {};
|
||||
layoutData.userInfo = { id, name: name || "", avatar: "" };
|
||||
const { id, name, type } = user || {};
|
||||
layoutData.userInfo = { id, name: name || "", avatar: "", type };
|
||||
layoutData.schoolName = user?.school?.name || "教师专业发展";
|
||||
isLogin.value = true;
|
||||
} catch (error: any) {
|
||||
@@ -58,7 +58,7 @@ const showHero = computed(
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="app-footer">河南云海星通科技有限公司 版权所有 © 2012-2013 沪ICP备13000443号-1</footer>
|
||||
<footer class="app-footer">河南云海星通科技有限公司 版权所有 ©2024-2026 豫ICP备1234567890号</footer>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -93,7 +93,7 @@ class ProfessionApi {
|
||||
list(params: Record<string, any>) {
|
||||
return get<{ total: number; rows: Record<string, any>[] }>(`${PRO}/ProfessionList`, params);
|
||||
}
|
||||
/** 记录详情(阅读数+1) */
|
||||
/** 记录详情(纯读;阅读数由 VisitCreate 记足迹时 +1) */
|
||||
detail(serId: number) {
|
||||
return get<Record<string, any>>(`${PRO}/ProfessionDetail`, { SerID: serId });
|
||||
}
|
||||
@@ -144,6 +144,14 @@ class ProfessionApi {
|
||||
myPoints(userid?: number) {
|
||||
return get<Record<string, any>>(`${PRO}/MyPoints`, userid ? { userid } : undefined);
|
||||
}
|
||||
/** 积分来源明细分页(发布/评论逐条带分值;合计口径同 MyPoints) */
|
||||
pointsSourceList(userid?: number, pageIndex = 1, pageSize = 10) {
|
||||
return get<Record<string, any>>(`${PRO}/PointsSourceList`, {
|
||||
...(userid ? { userid } : {}),
|
||||
PageIndex: pageIndex,
|
||||
PageSize: pageSize,
|
||||
});
|
||||
}
|
||||
/** 本校发布排行 */
|
||||
releaseRanking(top = 10) {
|
||||
return get<Record<string, any>[]>(`${PRO}/ReleaseRanking`, { top });
|
||||
|
||||
@@ -157,13 +157,14 @@ ul, ol { list-style: none; }
|
||||
/* ===== 栏目条:现代胶囊式 ===== */
|
||||
.module-bar { margin-top: 14px; display: flex; align-items: center; border-radius: 10px; background: var(--card); box-shadow: var(--shadow-sm); padding: 4px 6px; }
|
||||
/* 间距 6px + flex-shrink:0:满配 10 项时留出余量,杜绝 flex 收缩把子像素误差分摊到各项引发切换晃动 */
|
||||
.mb-item { padding: 8px 16px; font-size: 14px; color: var(--ink-2); cursor: pointer; border-radius: 8px; position: relative; white-space: nowrap; transition: color .18s var(--ease), background-color .18s var(--ease); margin: 0 6px; flex-shrink: 0; }
|
||||
.mb-item { padding: 8px 16px; font-size: 14px; color: var(--ink-2); cursor: pointer; border-radius: 8px; position: relative; white-space: nowrap; transition: color .18s var(--ease), background-color .18s var(--ease); margin: 0 5px; flex-shrink: 0; }
|
||||
.mb-item .no { font-family: var(--font-d); font-size: 11px; color: var(--muted); margin-right: 5px; }
|
||||
.mb-item:hover { color: var(--navy); background: var(--sky); }
|
||||
.mb-item.on { background: var(--navy); color: #fff; font-weight: 600; }
|
||||
.mb-item.on .no { color: rgba(255,255,255,.7); }
|
||||
/* 「更多」的 flex 子项是 Dropdown 包装层,auto 外边距须落在它身上才能贴齐右缘 */
|
||||
.module-bar .ivu-dropdown { margin-left: auto; }
|
||||
/* 「更多」的 flex 子项是 Dropdown 包装层,auto 外边距须落在它身上才能贴齐右缘;
|
||||
只对更多项(mb-more-dd)生效——上级栏目组项的下拉包装层须留在原位参与排队 */
|
||||
.module-bar .ivu-dropdown.mb-more-dd { margin-left: auto; }
|
||||
.mb-more { margin-right: 0; color: var(--muted); padding: 8px 25px; }
|
||||
/* 「更多」下拉被 transfer 到 body,序号样式需全局作用 */
|
||||
.ivu-dropdown-transfer .ivu-dropdown-menu .no { font-family: var(--font-d); font-size: 11px; color: var(--muted); margin-right: 5px; }
|
||||
|
||||
+109
-15
@@ -5,24 +5,64 @@ import { professionApi, mouduleApi, baseApi } from "@/api/module/profession";
|
||||
|
||||
/**
|
||||
* 全局栏目条(方案D 蓝天校园:白底圆角卡内嵌条,项带序号,选中态品牌蓝胶囊)。
|
||||
* 「发展首页」+ 各启用栏目(超过 10 个折叠「更多▾」)+ 区级视图项(区局账号)。
|
||||
* 「发展首页」+ 各启用栏目(超过 MENU_MAX 个折叠「更多▾」)+ 区级视图项(区局账号)。
|
||||
* 挂了上级栏目的栏目归成一个组项:组名不可点,悬停展开二级栏目下拉(父项如「教学活动」)。
|
||||
*/
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const MENU_MAX = 8;
|
||||
interface ChildItem { id: number; name: string }
|
||||
type BarEntry =
|
||||
| { kind: "module"; id: number; name: string; no: string }
|
||||
| { kind: "group"; id: number; name: string; no: string; children: ChildItem[] };
|
||||
|
||||
const modules = ref<any[]>([]);
|
||||
const isDistrict = ref(false);
|
||||
|
||||
const menuItems = computed(() => modules.value.map(m => ({ id: m.id, name: m.name })));
|
||||
const menuVisible = computed(() => menuItems.value.slice(0, MENU_MAX));
|
||||
const menuMore = computed(() => menuItems.value.slice(MENU_MAX));
|
||||
// 按上级栏目归组:组项占据首个子栏目的位置;序号按条内位置排(发展首页不计数,组也算一项,
|
||||
// 下拉二级栏目不再带序号——组本身有序号,子项重复编号会撞号)
|
||||
const barEntries = computed<BarEntry[]>(() => {
|
||||
const list: BarEntry[] = [];
|
||||
const groups = new Map<number, Extract<BarEntry, { kind: "group" }>>();
|
||||
modules.value.forEach((m: any) => {
|
||||
const topId = Number(m.topModuleId || 0);
|
||||
const topName = String(m.topModuleName || "").trim();
|
||||
if (!topId || !topName) {
|
||||
list.push({ kind: "module", id: m.id, name: m.name, no: "" });
|
||||
return;
|
||||
}
|
||||
let g = groups.get(topId);
|
||||
if (!g) {
|
||||
g = { kind: "group", id: topId, name: topName, no: "", children: [] };
|
||||
groups.set(topId, g);
|
||||
list.push(g);
|
||||
}
|
||||
g.children.push({ id: m.id, name: m.name });
|
||||
});
|
||||
list.forEach((e, i) => { e.no = no(i); });
|
||||
return list;
|
||||
});
|
||||
const menuVisible = computed(() => barEntries.value.slice(0, MENU_MAX));
|
||||
const menuMore = computed(() => barEntries.value.slice(MENU_MAX));
|
||||
// 「更多」下拉保持一层:组项在其中展开为不带序号的子栏目(嵌套下拉悬停链不可靠)
|
||||
const moreItems = computed<{ id: number; name: string; no: string }[]>(() => {
|
||||
const items: { id: number; name: string; no: string }[] = [];
|
||||
for (const e of menuMore.value) {
|
||||
if (e.kind === "module") items.push({ id: e.id, name: e.name, no: e.no });
|
||||
else for (const c of e.children) items.push({ id: c.id, name: c.name, no: "" });
|
||||
}
|
||||
return items;
|
||||
});
|
||||
|
||||
const onHome = computed(() => route.path === "/home");
|
||||
const onArea = computed(() => route.path === "/area");
|
||||
function isActiveModule(id: number) {
|
||||
return route.path === "/module/list" && Number(route.query.mouid || 0) === id;
|
||||
}
|
||||
function isActiveGroup(e: Extract<BarEntry, { kind: "group" }>) {
|
||||
return e.children.some(c => isActiveModule(c.id));
|
||||
}
|
||||
function no(i: number) {
|
||||
return String(i + 1).padStart(2, "0");
|
||||
}
|
||||
@@ -55,30 +95,45 @@ onMounted(async () => {
|
||||
|
||||
<template>
|
||||
<nav class="module-bar">
|
||||
<a class="mb-item" :class="{ on: onHome }" @click="goHome"><span class="lbl" data-t="发展首页">发展首页</span></a>
|
||||
<a class="mb-item mb-home" :class="{ on: onHome }" @click="goHome"><svg class="home-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 10.5L12 3l9 7.5"/><path d="M5 9.5V21h14V9.5"/><path d="M10 21v-6h4v6"/></svg><span class="lbl" data-t="发展首页">发展首页</span></a>
|
||||
<template v-for="e in menuVisible" :key="e.kind === 'group' ? `g${e.id}` : e.id">
|
||||
<a
|
||||
v-for="(m, i) in menuVisible"
|
||||
:key="m.id"
|
||||
v-if="e.kind === 'module'"
|
||||
class="mb-item"
|
||||
:class="{ on: isActiveModule(m.id) }"
|
||||
@click="goModule(m.id)"
|
||||
><span class="no">{{ no(i) }}</span><span class="lbl" :data-t="m.name">{{ m.name }}</span></a>
|
||||
<a v-if="isDistrict" class="mb-item" :class="{ on: onArea }" @click="goArea"><span class="no">09</span><span class="lbl" data-t="区级视图">区级视图</span></a>
|
||||
<Dropdown v-if="menuMore.length" transfer>
|
||||
:class="{ on: isActiveModule(e.id) }"
|
||||
@click="goModule(e.id)"
|
||||
><span class="no">{{ e.no }}</span><span class="lbl" :data-t="e.name">{{ e.name }}</span></a>
|
||||
<Dropdown v-else transfer placement="bottom-start" class="mb-group-dd">
|
||||
<!-- 锚点内必须连写无空白:.lbl::after 占位是 display:block,会把锚点内联内容按空白切碎,
|
||||
尾部缩进空白自成一行空行盒(胶囊两层高);▾ 放 .lbl 内同理(块占位后的内联会被顶到第二行) -->
|
||||
<a class="mb-item mb-group" :class="{ on: isActiveGroup(e) }"><span class="no">{{ e.no }}</span><span class="lbl" :data-t="e.name">{{ e.name }}<span class="caret">▾</span></span></a>
|
||||
<template #list>
|
||||
<DropdownMenu>
|
||||
<DropdownItem
|
||||
v-for="c in e.children"
|
||||
:key="c.id"
|
||||
@click="goModule(c.id)"
|
||||
>{{ c.name }}</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</template>
|
||||
</Dropdown>
|
||||
</template>
|
||||
<a v-if="isDistrict" class="mb-item" :class="{ on: onArea }" @click="goArea"><span class="no">{{ no(barEntries.length) }}</span><span class="lbl" data-t="区级视图">区级视图</span></a>
|
||||
<Dropdown v-if="moreItems.length" transfer class="mb-more-dd">
|
||||
<a class="mb-item mb-more" href="javascript:void(0)">
|
||||
更多 ▾
|
||||
</a>
|
||||
<template #list>
|
||||
<DropdownMenu>
|
||||
<DropdownItem
|
||||
v-for="(m, i) in menuMore"
|
||||
v-for="m in moreItems"
|
||||
:key="m.id"
|
||||
@click="goModule(m.id)"
|
||||
><span class="no">{{ no(i + MENU_MAX) }}</span>{{ m.name }}</DropdownItem>
|
||||
><span v-if="m.no" class="no">{{ m.no }}</span>{{ m.name }}</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</template>
|
||||
</Dropdown>
|
||||
<span v-if="!menuItems.length" class="mb-empty">暂无启用栏目,请联系管理员在后台配置</span>
|
||||
<span v-if="!barEntries.length" class="mb-empty">暂无启用栏目,请联系管理员在后台配置</span>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
@@ -93,6 +148,38 @@ onMounted(async () => {
|
||||
/* 序号恒定 600:Georgia 粗体数字更宽,若仅选中项加粗会推动右侧栏目(晃动) */
|
||||
font-weight: 600;
|
||||
}
|
||||
/* 发展首页项的小房子图标(与 SpaceSide「我的首页」同款):stroke=currentColor
|
||||
自动跟随常规/hover/选中三态变色;-2px 把 13px 图标压回 14px 文字的视觉中线 */
|
||||
.mb-home .home-ico {
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
margin-right: 5px;
|
||||
vertical-align: -2px;
|
||||
}
|
||||
/* 上级栏目组项:只作悬停触发(下拉),本身不可点进入。
|
||||
必须显式 block:普通项是 flex 直接子项会被块化(22.4 行高+16 内边距=38.4 胶囊),
|
||||
组项在 Dropdown 包装层里保持 inline——inline 的上下 padding 不参与布局,胶囊矮 3.4px;
|
||||
6px 间距随之挪到 Dropdown 根(它是 flex 子项),同时让 rel 与锚点左缘重合、
|
||||
bottom-start 面板才能与组项文字对齐(margin 在锚点上时面板会偏左 6px) */
|
||||
.mb-group {
|
||||
cursor: default;
|
||||
display: block;
|
||||
margin: 0;
|
||||
}
|
||||
.mb-group-dd {
|
||||
margin: 0 5px;
|
||||
}
|
||||
.mb-group .caret {
|
||||
font-size: 10px;
|
||||
color: var(--muted);
|
||||
margin-left: 4px;
|
||||
/* 防晃动:▾ 不随选中态加粗(几何字形加粗变宽会推动右侧栏目) */
|
||||
font-weight: 400;
|
||||
}
|
||||
.mb-item.on .caret {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-weight: 400;
|
||||
}
|
||||
/* 防晃动:内嵌一份隐藏的加粗文本占位,使常规/加粗态宽度一致,
|
||||
否则选中项 font-weight:600 变宽 ~2px,切换时右侧栏目整体横移。
|
||||
注意 .lbl 必须保持纯 inline:若改成 inline-block,其基线会取自
|
||||
@@ -106,4 +193,11 @@ onMounted(async () => {
|
||||
visibility: hidden;
|
||||
font-weight: 600;
|
||||
}
|
||||
/* 组项不参与上面的块占位(必须写在占位规则之后:同优先级后者胜):
|
||||
该 display:block 占位会在组项里把内联内容切成多个行盒(实测锚点 57.4px = 两层胶囊,
|
||||
fragments [35,0,35]),普通项因占位后无内容不受影响。组项序号恒定 600、▾ 恒定 400
|
||||
(晃动的主因是 Georgia 数字加粗 +1.6px,已在 .no 恒定字重里兜住),无需占位 */
|
||||
.mb-group .lbl::after {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
* panel 2 = 本月概览(ov-grid 四色,仅 full 模式)
|
||||
* panel 3 = 最近访客(visitors 圆字章:默认一排,超过一排显示「查看全部访客」就地展开全部)
|
||||
* mode:full=栏目列表·个人空间/我的首页完整版;mini=发布编辑、记录详情精简版(无统计行/进度条/本月概览)。
|
||||
* space-points 可点击 → 弹「积分来源」弹窗(发布/评论逐条带分值,合计口径同 MyPoints)。
|
||||
*/
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@@ -29,11 +30,18 @@ const props = withDefaults(
|
||||
overview?: Record<string, any> | null;
|
||||
/** full=完整版侧栏;mini=精简版(发布/详情页) */
|
||||
mode?: "full" | "mini";
|
||||
/** 积分归属教师 id(他人空间传其 userid;不传 = 当前登录人) */
|
||||
pointsUserid?: number | null;
|
||||
}>(),
|
||||
{ himWord: "我的", teacherName: "", points: 0, avatar: "", modules: () => [], visitors: () => [], visitorRowSize: 7, activeMouid: null, homeCurrent: false, overview: null, mode: "full" }
|
||||
{ himWord: "我的", teacherName: "", points: 0, avatar: "", modules: () => [], visitors: () => [], visitorRowSize: 7, activeMouid: null, homeCurrent: false, overview: null, mode: "full", pointsUserid: null }
|
||||
);
|
||||
|
||||
import { ref, watch } from "vue";
|
||||
import { ref, computed, watch } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { Message } from "view-ui-plus";
|
||||
import { professionApi } from "@/api/module/profession";
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "home"): void;
|
||||
@@ -58,6 +66,59 @@ function pointsWidth(): number {
|
||||
function firstChar(name?: string): string {
|
||||
return (name || "?").replace(/\s/g, "").slice(0, 1) || "?";
|
||||
}
|
||||
|
||||
/* ============ 积分来源弹窗(点击 space-points 打开,分页) ============ */
|
||||
const ptsShow = ref(false);
|
||||
const ptsLoading = ref(false);
|
||||
const ptsData = ref<Record<string, any> | null>(null);
|
||||
const ptsPage = ref(1);
|
||||
const ptsPageSize = 8;
|
||||
|
||||
async function loadPoints() {
|
||||
ptsLoading.value = true;
|
||||
try {
|
||||
ptsData.value =
|
||||
(await professionApi.pointsSourceList(props.pointsUserid ?? undefined, ptsPage.value, ptsPageSize).send()) ||
|
||||
{};
|
||||
} catch (e: any) {
|
||||
Message.error(e?.message || "获取积分来源失败");
|
||||
ptsShow.value = false;
|
||||
} finally {
|
||||
ptsLoading.value = false;
|
||||
}
|
||||
}
|
||||
/** 每次打开都重置到第一页重拉(积分随发布/评论实时变) */
|
||||
watch(ptsShow, v => {
|
||||
if (!v) return;
|
||||
ptsPage.value = 1;
|
||||
loadPoints();
|
||||
});
|
||||
|
||||
const ptsRows = computed<any[]>(() => ptsData.value?.rows || []);
|
||||
const ptsTotal = computed(() => Number(ptsData.value?.total ?? 0));
|
||||
/** 与 ModuleList 同款页码序列:前 5 页 + 尾页(…) */
|
||||
const ptsPages = computed(() => {
|
||||
const tp = Math.max(1, Math.ceil(ptsTotal.value / ptsPageSize));
|
||||
const arr: number[] = [];
|
||||
for (let i = 1; i <= Math.min(tp, 5); i++) arr.push(i);
|
||||
return { arr, tp, hasMore: tp > 5 };
|
||||
});
|
||||
function ptsGo(p: number) {
|
||||
if (p < 1 || p > ptsPages.value.tp || p === ptsPage.value) return;
|
||||
ptsPage.value = p;
|
||||
loadPoints();
|
||||
}
|
||||
|
||||
/** 评论内容摘要(去换行,截 40 字) */
|
||||
function ptsExcerpt(s?: string): string {
|
||||
const text = String(s || "").replace(/\s+/g, " ").trim();
|
||||
return text.length > 40 ? text.slice(0, 40) + "…" : text;
|
||||
}
|
||||
function goRecord(recordId?: number) {
|
||||
if (!recordId) return;
|
||||
ptsShow.value = false;
|
||||
router.push({ path: "/record/detail", query: { serid: recordId } });
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -68,7 +129,7 @@ function firstChar(name?: string): string {
|
||||
<img v-if="avatar" class="space-avatar" :src="avatar" alt="" />
|
||||
<div v-else class="space-avatar">{{ firstChar(teacherName) }}</div>
|
||||
<div class="space-name">{{ teacherName || "—" }}</div>
|
||||
<div class="space-points">积分 <b>{{ points || 0 }}</b> 分</div>
|
||||
<a class="space-points" href="javascript:void(0)" title="查看积分来源" @click.prevent="ptsShow = true">积分 <b>{{ points || 0 }}</b> 分</a>
|
||||
<a class="space-home-link" :class="{ current: homeCurrent }" href="javascript:void(0)" @click.prevent="emit('home')">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 10.5L12 3l9 7.5"/><path d="M5 9.5V21h14V9.5"/><path d="M10 21v-6h4v6"/></svg>
|
||||
{{ himWord }}首页
|
||||
@@ -130,6 +191,66 @@ function firstChar(name?: string): string {
|
||||
@click.prevent="visitorsOpen = !visitorsOpen"
|
||||
>{{ visitorsOpen ? "收起访客 ▴" : `查看全部访客(${visitors.length})▾` }}</a>
|
||||
</section>
|
||||
|
||||
<!-- ============ 积分来源弹窗(点击 space-points 打开;Teleport 挂 body 避侧栏 overflow 裁剪) ============ -->
|
||||
<Teleport to="body">
|
||||
<div v-if="ptsShow" class="pts-overlay" @click.self="ptsShow = false">
|
||||
<div class="pts-modal">
|
||||
<div class="pts-hd">
|
||||
<h3>积分来源</h3>
|
||||
<span class="pts-total"><b>{{ ptsData?.pointsnum ?? (points || 0) }}</b> 分</span>
|
||||
<a class="pts-close" href="javascript:void(0)" @click.prevent="ptsShow = false">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M6 6l12 12M18 6L6 18"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
<div class="pts-sub">
|
||||
<span class="p-item"><i class="dot pub"></i>发布积分 <b>{{ ptsData?.sendPoints ?? 0 }}</b></span>
|
||||
<span class="p-item"><i class="dot com"></i>评论积分 <b>{{ ptsData?.commPoints ?? 0 }}</b></span>
|
||||
<span class="p-item hint">积分随发布与互动累积</span>
|
||||
</div>
|
||||
<div class="pts-body">
|
||||
<div v-if="ptsLoading" class="pts-empty">加载中…</div>
|
||||
<div v-else-if="!ptsRows.length" class="pts-empty">暂无积分来源,发布记录或参与互动即可累积积分</div>
|
||||
<template v-else>
|
||||
<div
|
||||
v-for="r in ptsRows"
|
||||
:key="r.type + '-' + r.id"
|
||||
class="pts-row"
|
||||
:title="'查看记录:' + r.title"
|
||||
@click="goRecord(r.recordId)"
|
||||
>
|
||||
<span class="pts-tag" :class="r.type">{{ r.type === "publish" ? "发布" : "评论" }}</span>
|
||||
<div class="pts-main">
|
||||
<div class="pts-title">
|
||||
{{ r.type === "publish" ? r.title : `评《${r.title}》:${ptsExcerpt(r.contents)}` }}
|
||||
</div>
|
||||
<div class="pts-meta">
|
||||
<span v-if="r.type === 'publish' && r.moduleName" class="mod">{{ r.moduleName }}</span>
|
||||
<span>{{ r.createTime }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<b class="pts-val">+{{ r.points ?? 0 }}</b>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<!-- 分页(复用全局 .pagination/.pbtn;仅超一页显示) -->
|
||||
<div v-if="ptsPages.tp > 1" class="pagination pts-pager">
|
||||
<span>共 {{ ptsTotal }} 条</span>
|
||||
<span class="pbtn" @click="ptsGo(ptsPage - 1)">‹</span>
|
||||
<span
|
||||
v-for="p in ptsPages.arr"
|
||||
:key="p"
|
||||
class="pbtn"
|
||||
:class="{ on: p === ptsPage }"
|
||||
@click="ptsGo(p)"
|
||||
>{{ p }}</span>
|
||||
<span v-if="ptsPages.hasMore" class="pbtn">…</span>
|
||||
<span v-if="ptsPages.hasMore" class="pbtn" :class="{ on: ptsPage === ptsPages.tp }" @click="ptsGo(ptsPages.tp)">{{ ptsPages.tp }}</span>
|
||||
<span class="pbtn" @click="ptsGo(ptsPage + 1)">›</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
@@ -164,4 +285,198 @@ function firstChar(name?: string): string {
|
||||
font-size: 12px;
|
||||
color: var(--muted, #8fa9c4);
|
||||
}
|
||||
/* 积分行可点击(查看积分来源):a 元素继承原 div 版式,hover 提亮 + 数字下划线示意可点 */
|
||||
a.space-points {
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
}
|
||||
a.space-points:hover {
|
||||
color: #fff;
|
||||
}
|
||||
a.space-points:hover b {
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
|
||||
/* ============ 积分来源弹窗(方案D 规范:极细边线 + 极轻阴影 + 衬线数字) ============ */
|
||||
.pts-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1200;
|
||||
background: rgba(28, 40, 60, 0.42);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
animation: pts-fade 0.18s var(--ease);
|
||||
}
|
||||
@keyframes pts-fade {
|
||||
from { opacity: 0; }
|
||||
}
|
||||
.pts-modal {
|
||||
width: 520px;
|
||||
max-width: calc(100vw - 48px);
|
||||
max-height: 74vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--card);
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 18px 50px rgba(16, 40, 80, 0.28);
|
||||
overflow: hidden;
|
||||
animation: pts-pop 0.22s var(--ease);
|
||||
}
|
||||
@keyframes pts-pop {
|
||||
from { opacity: 0; transform: translateY(10px) scale(0.98); }
|
||||
}
|
||||
.pts-hd {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 16px 18px 12px;
|
||||
border-bottom: 1px solid var(--line-2);
|
||||
}
|
||||
.pts-hd h3 {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: var(--ink);
|
||||
padding-left: 9px;
|
||||
border-left: 3px solid var(--navy);
|
||||
line-height: 1.2;
|
||||
}
|
||||
.pts-total {
|
||||
margin-left: auto;
|
||||
font-size: 12px;
|
||||
color: var(--ink-2);
|
||||
}
|
||||
.pts-total b {
|
||||
font-family: var(--font-d);
|
||||
font-size: 19px;
|
||||
color: var(--gold);
|
||||
margin-right: 2px;
|
||||
}
|
||||
.pts-close {
|
||||
color: var(--muted);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 7px;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.pts-close:hover {
|
||||
color: var(--navy);
|
||||
background: var(--sky);
|
||||
}
|
||||
.pts-sub {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 10px 18px;
|
||||
background: var(--bg);
|
||||
border-bottom: 1px solid var(--line-2);
|
||||
font-size: 12px;
|
||||
color: var(--ink-2);
|
||||
}
|
||||
.pts-sub .p-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
.pts-sub .p-item b {
|
||||
font-family: var(--font-d);
|
||||
font-size: 14px;
|
||||
color: var(--navy);
|
||||
}
|
||||
.pts-sub .p-item.hint {
|
||||
margin-left: auto;
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
.pts-sub .dot {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.pts-sub .dot.pub {
|
||||
background: var(--navy);
|
||||
}
|
||||
.pts-sub .dot.com {
|
||||
background: #21b8a6;
|
||||
}
|
||||
.pts-body {
|
||||
overflow-y: auto;
|
||||
padding: 6px 10px 12px;
|
||||
}
|
||||
/* 弹窗内分页条(全局 .pagination 的内嵌变体:贴底、细上边线) */
|
||||
.pts-pager {
|
||||
border-top: 1px solid var(--line-2);
|
||||
padding: 10px 16px;
|
||||
flex: none;
|
||||
}
|
||||
.pts-empty {
|
||||
padding: 34px 16px;
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.pts-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 10px;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s var(--ease);
|
||||
}
|
||||
.pts-row:hover {
|
||||
background: var(--sky);
|
||||
}
|
||||
.pts-row + .pts-row {
|
||||
margin-top: 2px;
|
||||
}
|
||||
.pts-tag {
|
||||
flex: none;
|
||||
width: 38px;
|
||||
text-align: center;
|
||||
font-size: 11px;
|
||||
line-height: 20px;
|
||||
border-radius: 6px;
|
||||
background: var(--sky);
|
||||
color: var(--navy-2);
|
||||
}
|
||||
.pts-tag.comment {
|
||||
background: #e6f7f4;
|
||||
color: #159a89;
|
||||
}
|
||||
.pts-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.pts-title {
|
||||
font-size: 13px;
|
||||
color: var(--ink);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.pts-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 2px;
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.pts-meta .mod {
|
||||
color: var(--navy-2);
|
||||
background: var(--sky);
|
||||
border-radius: 4px;
|
||||
padding: 0 6px;
|
||||
font-size: 10.5px;
|
||||
line-height: 17px;
|
||||
}
|
||||
.pts-val {
|
||||
flex: none;
|
||||
font-family: var(--font-d);
|
||||
font-size: 15px;
|
||||
color: var(--gold);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -4,20 +4,22 @@ import WeatherWidget from "./WeatherWidget.vue";
|
||||
import { baseApi } from "@/api/module/profession";
|
||||
|
||||
/**
|
||||
* 前台顶栏(方案D 蓝天校园):天气胶囊 + 校名(品牌蓝)+ 欢迎 + 心情/应用中心/退出(SVG 线性图标)。
|
||||
* 前台顶栏(方案D 蓝天校园):天气胶囊 + 校名(品牌蓝)+ 欢迎 + 后台管理/心情/应用中心/退出(SVG 线性图标)。
|
||||
* 玻璃拟态样式由 profession-ui.css 的 .topbar 提供;应用面板沿用原逻辑。
|
||||
* 超管/区局账号(IsDistrictAccount)不显示心情笑脸,改为「后台管理」齿轮入口(ProfessionConsole)。
|
||||
* 管理员(GetUserInfo:type=1 超管 / type=2 学校管理员,或区局账号)不显示心情笑脸,
|
||||
* 改为「后台管理」齿轮入口(ProfessionConsole,落地栏目管理 #/module)。
|
||||
*/
|
||||
const props = defineProps<{ layoutData: Record<string, any> }>();
|
||||
|
||||
const isPanel = ref(false);
|
||||
const isAdmin = ref(false);
|
||||
const isDistrict = ref(false);
|
||||
|
||||
/* 后台管理地址:线上 front 与 console 同 contextPath 平级部署(/{ctx}/front/ → /{ctx}/console/);
|
||||
本地 dev(根路径无段)跳 console dev 服务 8082 */
|
||||
本地 dev(根路径无段)跳 console dev 服务 8082,落地栏目管理页 */
|
||||
const consoleUrl = (() => {
|
||||
const seg = window.location.pathname.split("/").filter(Boolean);
|
||||
return seg.length >= 2 ? `${window.location.origin}/${seg[0]}/console/` : "http://localhost:8082";
|
||||
const base = seg.length >= 2 ? `${window.location.origin}/${seg[0]}/console/` : "http://localhost:8082/";
|
||||
return base + "#/module";
|
||||
})();
|
||||
|
||||
const baseHost = computed(() => props.layoutData?.baseHost || "");
|
||||
@@ -26,12 +28,18 @@ const schoolName = computed(() => props.layoutData?.schoolName || "教师专业
|
||||
const userName = computed(() => props.layoutData?.userInfo?.name || "");
|
||||
const appList = computed(() => props.layoutData?.appList || []);
|
||||
|
||||
/* 管理员:平台用户类型(GetUserInfo 返回的 BoaUser.type)1=超管 2=学校管理员,或区局账号 */
|
||||
const isAdmin = computed(() => {
|
||||
const type = props.layoutData?.userInfo?.type;
|
||||
return type === 1 || type === 2 || isDistrict.value;
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const d: any = await baseApi.isDistrictAccount().send();
|
||||
isAdmin.value = !!d?.district;
|
||||
isDistrict.value = !!d?.district;
|
||||
} catch {
|
||||
isAdmin.value = false; /* 判定失败按普通教师显示 */
|
||||
isDistrict.value = false; /* 判定失败按普通教师显示 */
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -304,6 +304,7 @@ onMounted(async () => {
|
||||
:visitors="spaceVisitors"
|
||||
:active-mouid="mouid || null"
|
||||
:overview="space"
|
||||
:points-userid="userid"
|
||||
@home="goSpaceHome"
|
||||
@module="goSpaceModule"
|
||||
@visitor="goVisitor"
|
||||
|
||||
@@ -313,6 +313,7 @@ watch(
|
||||
:visitors="spaceVisitors"
|
||||
:overview="space"
|
||||
:active-mouid="detail.moduleId ?? null"
|
||||
:points-userid="detail.teacherId"
|
||||
@home="goSpaceHome"
|
||||
@module="goSpaceModule"
|
||||
@visitor="goVisitor"
|
||||
|
||||
@@ -337,6 +337,7 @@ watch(
|
||||
:modules="spaceModules"
|
||||
:visitors="spaceVisitors"
|
||||
:active-mouid="mouid || null"
|
||||
:points-userid="spaceUid"
|
||||
@home="goSpaceHome"
|
||||
@module="goSpaceModule"
|
||||
@visitor="goVisitor"
|
||||
|
||||
@@ -168,6 +168,7 @@ watch(
|
||||
:active-mouid="null"
|
||||
:overview="space"
|
||||
:home-current="true"
|
||||
:points-userid="userid"
|
||||
@home="userid && router.push({ path: '/timeline', query: { userid } })"
|
||||
@module="goModuleList"
|
||||
@visitor="goVisitor"
|
||||
|
||||
Reference in New Issue
Block a user