diff --git a/.env.dev b/.env.dev index c3fd97e..c83c013 100644 --- a/.env.dev +++ b/.env.dev @@ -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 diff --git a/.gitignore b/.gitignore index 05cb578..b1d089e 100644 --- a/.gitignore +++ b/.gitignore @@ -28,8 +28,9 @@ yarn.lock # scratch .temp -# 本地备份与临时产物(不入公开仓库) +# 本地备份与历史构建产物(不入公开仓库) backup/ +dist_old/ *.bak *.orig *.old diff --git a/src/App.vue b/src/App.vue index 2309593..b776e06 100644 --- a/src/App.vue +++ b/src/App.vue @@ -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( - + diff --git a/src/components/SpaceSide.vue b/src/components/SpaceSide.vue index 5716749..1d40d19 100644 --- a/src/components/SpaceSide.vue +++ b/src/components/SpaceSide.vue @@ -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 | 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 | 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(() => 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 } }); +} @@ -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); +} diff --git a/src/components/layout.vue b/src/components/layout.vue index 42cf14c..6a954d9 100644 --- a/src/components/layout.vue +++ b/src/components/layout.vue @@ -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 }>(); 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; /* 判定失败按普通教师显示 */ } }); diff --git a/src/views/profession/ModuleList.vue b/src/views/profession/ModuleList.vue index ac71d72..29b1afc 100644 --- a/src/views/profession/ModuleList.vue +++ b/src/views/profession/ModuleList.vue @@ -304,6 +304,7 @@ onMounted(async () => { :visitors="spaceVisitors" :active-mouid="mouid || null" :overview="space" + :points-userid="userid" @home="goSpaceHome" @module="goSpaceModule" @visitor="goVisitor" diff --git a/src/views/profession/RecordDetail.vue b/src/views/profession/RecordDetail.vue index e398163..ba8d498 100644 --- a/src/views/profession/RecordDetail.vue +++ b/src/views/profession/RecordDetail.vue @@ -313,6 +313,7 @@ watch( :visitors="spaceVisitors" :overview="space" :active-mouid="detail.moduleId ?? null" + :points-userid="detail.teacherId" @home="goSpaceHome" @module="goSpaceModule" @visitor="goVisitor" diff --git a/src/views/profession/RecordEdit.vue b/src/views/profession/RecordEdit.vue index 9b18de4..ebdb2da 100644 --- a/src/views/profession/RecordEdit.vue +++ b/src/views/profession/RecordEdit.vue @@ -337,6 +337,7 @@ watch( :modules="spaceModules" :visitors="spaceVisitors" :active-mouid="mouid || null" + :points-userid="spaceUid" @home="goSpaceHome" @module="goSpaceModule" @visitor="goVisitor" diff --git a/src/views/profession/Timeline.vue b/src/views/profession/Timeline.vue index d7a46f9..6a94462 100644 --- a/src/views/profession/Timeline.vue +++ b/src/views/profession/Timeline.vue @@ -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"