SAAS模式调整

This commit is contained in:
hebo 2026-02-23 17:29:36 +08:00
parent 8fa41cf9e2
commit 7f02fc7e9c
32 changed files with 4917 additions and 1887 deletions

View File

@ -61,6 +61,7 @@
"pinia-plugin-persist-uni": "1.2.0",
"pinyin-pro": "^3.27.0",
"qrcode": "^1.5.3",
"tinymce": "^5.10.7",
"uview-plus": "3.1.20",
"vconsole": "3.15.1",
"vue": "3.2.45",

View File

@ -1,4 +1,4 @@
import { get, post } from "@/utils/request";
import { get } from "@/utils/request";
// 学生档案相关API接口
@ -8,3 +8,15 @@ import { get, post } from "@/utils/request";
export function findStudentInfoByNjAndBjSimpleApi(njId: string, bjId: string) {
return get('/api/xs/findStudentInfoByNjAndBjSimple', { njId, bjId });
}
/**
*
*/
export function findStudentArchiveApi(params: {
njIds?: string;
bjIds?: string;
xsIds?: string;
xsxm?: string;
}) {
return get('/api/xs/findStudentArchive', params);
}

View File

@ -1,5 +1,13 @@
import { get, post } from "@/utils/request";
/**
*
* { jsId, jsName, topTypeId, topTypeName, approvedCount, calculatedScore, pendingCount, rejectedCount, draftCount }
*/
export function jfStatisticsByTeacherApi(params: any) {
return get('/api/jf/statisticsByTeacher', params);
}
/**
*
*/

14
src/api/base/jzApi.ts Normal file
View File

@ -0,0 +1,14 @@
import { get } from "@/utils/request";
/**
*
*/
export function findParentArchivePageApi(params: {
njId: string;
bjId: string;
jzxm?: string;
page?: number;
rows?: number;
}) {
return get('/api/jz/findParentArchivePage', params);
}

16
src/api/routine/jz.ts Normal file
View File

@ -0,0 +1,16 @@
import { get } from "@/utils/request";
/**
*
*
*/
export function findParentArchivePageApi(params: {
njIds?: string;
bjIds?: string;
xsIds?: string;
jzxm?: string;
page?: number;
rows?: number;
}) {
return get('/api/jz/findParentArchivePage', params);
}

View File

@ -1,5 +1,20 @@
import { get } from "@/utils/request";
/** 短时缓存,避免 launchPage → service 连续跳转时重复调用 */
let _changeTimeCache: { promise: Promise<any>; ts: number } | null = null;
const CACHE_MS = 10000; // 10 秒内复用
/** 获取权限变更时间戳,用于判断本地缓存的菜单是否需重新拉取 */
export const getPermissionChangeTimeApi = () => {
const now = Date.now();
if (_changeTimeCache && now - _changeTimeCache.ts < CACHE_MS) {
return _changeTimeCache.promise;
}
const promise = get("/api/comConfig/getPermissionChangeTime");
_changeTimeCache = { promise, ts: now };
return promise;
};
//配置参数接口
export const dmBeforeMinuteApi = async () => {
return await get("/api/comConfig/getDmBeforeMinute");

View File

@ -44,11 +44,6 @@ export const weChatLogin = async (param: any) => {
return await get("/userlogin/weloginByCode", param);
};
//获取用户按钮权限
export const authenticationApi = async (param: { userId: string }) => {
return await get("/api/authentication/find-by-user", param);
};
//获取公众号票据
export const wxConfigApi = async (param: any) => {
return await post("/userlogin/wxConfig", param);

17
src/api/system/menu.ts Normal file
View File

@ -0,0 +1,17 @@
import { get } from "@/utils/request";
/** 手机端菜单树节点 */
export interface MobileMenuTreeNode {
id: number;
parentId: number | null;
screenName: string;
normalCss?: string;
pagePath?: string;
sortNum: number;
authCode?: string;
children: MobileMenuTreeNode[];
}
/** 获取手机端菜单 */
export const getMobileMenuApi = () =>
get<{ result: MobileMenuTreeNode[] }>("/api/screen/find-mobile-menu");

View File

@ -0,0 +1,178 @@
<!-- H5 专用 TinyMCE 富文本编辑器 zhxy-vue 接龙描述一致 -->
<template>
<!-- #ifdef H5 -->
<view class="tinymce-editor-h5" :style="{ width: containerWidth }">
<textarea :id="tinymceId" ref="elRef" class="tinymce-textarea"></textarea>
</view>
<!-- #endif -->
</template>
<script lang="ts" setup>
// #ifdef H5
import { ref, computed, watch, onMounted, onBeforeUnmount, nextTick } from 'vue';
import tinymce from 'tinymce/tinymce';
import 'tinymce/themes/silver';
import 'tinymce/icons/default/icons';
import 'tinymce/plugins/advlist';
import 'tinymce/plugins/link';
import 'tinymce/plugins/lists';
import 'tinymce/plugins/paste';
import 'tinymce/plugins/code';
import 'tinymce/plugins/image';
import { config } from '@/utils/request';
import { useUserStore } from '@/store/modules/user';
import { AUTH_KEY } from '@/config';
import { imagUrl } from '@/utils';
const props = defineProps<{
modelValue?: string;
placeholder?: string;
height?: number | string;
width?: number | string;
}>();
const emit = defineEmits<{
(e: 'update:modelValue', value: string): void;
(e: 'change', value: string): void;
}>();
const userStore = useUserStore();
const tinymceId = ref(`tinymce-${Date.now()}`);
let initRetryCount = 0;
const elRef = ref<HTMLElement | null>(null);
const editorRef = ref<any>(null);
const containerWidth = computed(() => {
const w = props.width;
if (typeof w === 'number') return `${w}px`;
return w || '100%';
});
const uploadUrl = computed(() => `${config.baseUrl}/api/attachment/upload`);
const getInitOptions = (targetEl: HTMLElement) => ({
target: targetEl,
height: props.height || 320,
menubar: false,
branding: false,
// 404
// 使 unpkg CDN
skin_url: 'https://unpkg.com/tinymce@5.10.7/skins/ui/oxide',
content_css: 'https://unpkg.com/tinymce@5.10.7/skins/ui/oxide/content.min.css',
plugins: 'advlist link lists paste code image',
toolbar: 'undo redo | formatselect | bold italic underline | alignleft aligncenter alignright | bullist numlist | link image | removeformat',
placeholder: props.placeholder || '请输入内容,支持插入图片',
default_link_target: '_blank',
paste_data_images: true,
automatic_uploads: true,
images_upload_handler: async (blobInfo: any, success: (url: string) => void, failure: (err: string) => void) => {
try {
const formData = new FormData();
formData.append('files', blobInfo.blob(), blobInfo.filename());
const token = userStore.getToken;
const headers: Record<string, string> = {};
if (token) headers[AUTH_KEY] = token;
const resp = await fetch(uploadUrl.value, {
method: 'POST',
headers,
body: formData,
});
const res = await resp.json();
if (res && res.resultCode === 1 && res.result && res.result.length > 0) {
const filePath = res.result[0].filePath || res.result[0].url;
success(imagUrl(filePath));
} else {
failure(res?.message || '图片上传失败');
}
} catch (e: any) {
failure(e?.message || '图片上传失败');
}
},
setup: (editor: any) => {
editorRef.value = editor;
editor.on('init', () => {
editor.setContent(props.modelValue || '');
});
},
});
function initEditor() {
const el = elRef.value as HTMLElement | null;
if (el?.style) el.style.visibility = '';
const targetEl = document.getElementById(tinymceId.value);
if (!targetEl) {
initRetryCount++;
if (initRetryCount < 5) {
setTimeout(initEditor, 150);
} else {
console.error('[TinymceEditorH5] 目标元素未找到,已放弃');
}
return;
}
tinymce.init(getInitOptions(targetEl)).then((editors: any[]) => {
const editor = editors[0];
if (editor) {
editor.on('change keyup', () => {
const content = editor.getContent();
emit('update:modelValue', content);
emit('change', content);
});
}
}).catch((err) => {
console.error('[TinymceEditorH5] TinyMCE 初始化失败:', err);
});
}
function destroyEditor() {
try {
tinymce?.remove?.(`#${tinymceId.value}` as any);
} catch (_) {}
}
watch(
() => props.modelValue,
(val) => {
const editor = editorRef.value;
if (editor && val !== editor.getContent()) {
editor.setContent(val || '');
}
}
);
onMounted(() => {
nextTick(() => {
setTimeout(initEditor, 100);
});
});
onBeforeUnmount(() => {
destroyEditor();
});
// #endif
</script>
<style scoped>
.tinymce-editor-h5 {
min-height: 320px;
position: relative;
}
/* 初始隐藏 textareaTinyMCE 会替换它 */
.tinymce-textarea {
position: absolute;
left: -9999px;
width: 1px;
height: 1px;
opacity: 0;
}
.tinymce-editor-h5 :deep(.tox-tinymce) {
border-radius: 6px;
border: 1px solid #e0e0e0;
}
.tinymce-editor-h5 :deep(.tox .tox-edit-area__iframe) {
min-height: 280px;
}
</style>

View File

@ -0,0 +1,45 @@
<!-- 通知内容富文本H5 TinyMCE zhxy-vue 接龙描述一致小程序/App BasicEditor -->
<template>
<!-- #ifdef H5 -->
<TinymceEditorH5
:modelValue="modelValue"
@update:modelValue="$emit('update:modelValue', $event)"
@change="$emit('change', $event)"
:placeholder="placeholder"
:height="height"
:width="width"
/>
<!-- #endif -->
<!-- #ifndef H5 -->
<BasicEditor
:modelValue="modelValue"
@update:modelValue="$emit('update:modelValue', $event)"
:placeholder="placeholder"
v-bind="$attrs"
/>
<!-- #endif -->
</template>
<script lang="ts" setup>
import { computed } from 'vue';
// #ifdef H5
import TinymceEditorH5 from './TinymceEditorH5.vue';
// #endif
// #ifndef H5
import BasicEditor from '@/components/BasicForm/components/BasicEditor.vue';
// #endif
const props = defineProps<{
modelValue?: string;
placeholder?: string;
height?: number | string;
width?: number | string;
}>();
defineEmits<{
(e: 'update:modelValue', value: string): void;
(e: 'change', value: string): void;
}>();
const modelValue = computed(() => props.modelValue ?? '');
</script>

View File

@ -861,6 +861,69 @@
"enablePullDownRefresh": false
}
},
{
"path": "pages/view/routine/xszp/kc/kclx",
"style": {
"navigationBarTitleText": "课程任务类型",
"enablePullDownRefresh": false
}
},
{
"path": "pages/view/routine/xszp/tj/index",
"style": {
"navigationBarTitleText": "学业统计",
"enablePullDownRefresh": false
}
},
{
"path": "pages/view/routine/xszp/tj/kclx",
"style": {
"navigationBarTitleText": "课程任务类型",
"enablePullDownRefresh": false
}
},
{
"path": "pages/view/routine/xszp/tj/statistics",
"style": {
"navigationBarTitleText": "学业统计",
"enablePullDownRefresh": false
}
},
{
"path": "pages/view/routine/xszp/tj/personnelList",
"style": {
"navigationBarTitleText": "人员名单",
"enablePullDownRefresh": false
}
},
{
"path": "pages/view/routine/xszp/tj/groupDetail",
"style": {
"navigationBarTitleText": "分组情况明细",
"enablePullDownRefresh": false
}
},
{
"path": "pages/view/routine/xszp/kclx/index",
"style": {
"navigationBarTitleText": "作品课程类型",
"enablePullDownRefresh": false
}
},
{
"path": "pages/view/routine/xszp/kclx/add",
"style": {
"navigationBarTitleText": "新增课程类型",
"enablePullDownRefresh": false
}
},
{
"path": "pages/view/routine/xszp/kclx/edit",
"style": {
"navigationBarTitleText": "编辑课程类型",
"enablePullDownRefresh": false
}
},
{
"path": "pages/view/routine/xszp/pj/index",
"style": {
@ -1317,6 +1380,20 @@
"enablePullDownRefresh": false
}
},
{
"path": "pages/view/homeSchool/kspj/index",
"style": {
"navigationBarTitleText": "成绩评价",
"enablePullDownRefresh": false
}
},
{
"path": "pages/view/homeSchool/kspj/detail",
"style": {
"navigationBarTitleText": "评价详情",
"enablePullDownRefresh": false
}
},
{
"path": "pages/view/hr/jsQj/index",
"style": {
@ -1722,12 +1799,26 @@
}
},
{
"path": "pages/view/analysis/xs/studentArchive",
"path": "pages/view/routine/da/xsda/studentArchive",
"style": {
"navigationBarTitleText": "学生档案",
"enablePullDownRefresh": false
}
},
{
"path": "pages/view/routine/da/jzda/index",
"style": {
"navigationBarTitleText": "家长档案",
"enablePullDownRefresh": false
}
},
{
"path": "pages/view/routine/da/jsda/index",
"style": {
"navigationBarTitleText": "教师档案",
"enablePullDownRefresh": false
}
},
{
"path": "pages/view/analysis/xk/xkCourse",
"style": {

View File

@ -1,58 +1,62 @@
<template>
<view class="mine-page">
<!-- 1. 顶部 Header - 复制自service页面 -->
<!-- 1. 顶部 Header与自助服务一致 -->
<view class="header-section">
<view class="header-gradient"></view>
<!-- 退出按钮 -->
<view class="logout-btn" @click="handleLogout">
<text class="logout-text">退出</text>
</view>
<!-- 老师信息 -->
<view class="teacher-info">
<view class="teacher-avatar">
<image
class="avatar-image"
:src="teacherData.avatar || '/static/base/default-avatar.png'"
:src="jsTx || '/static/base/default-avatar.png'"
mode="aspectFill"
></image>
</view>
<view class="teacher-details">
<view class="teacher-name">{{ teacherData.name }}</view>
<view class="teacher-position">{{ teacherPosition }}</view>
<view class="teacher-class">{{ teacherData.className }}</view>
<view class="teacher-name">{{ js.jsxm }}</view>
<view class="teacher-position">{{ dzZwLabel }}</view>
<view class="teacher-position">{{ qtZwLabel }}</view>
<view class="teacher-class">{{ js.njz }}</view>
</view>
</view>
<!-- 统计信息 - 暂时隐藏 -->
<!-- <view class="stats-info">
<view class="stat-item">
<text class="stat-label">积分</text>
<text class="stat-value">{{ teacherData.score }}</text>
</view>
<view class="stat-divider">|</view>
<view class="stat-item">
<text class="stat-label">工作量</text>
<text class="stat-value">{{ teacherData.workload }}课时</text>
</view>
</view> -->
<!-- 介绍文字 -->
<view class="teacher-intro">
{{ teacherData.introduction }}
{{ js.introduction || "守正出新,求真务实\n让每一棵新苗拔节生长" }}
</view>
</view>
<!-- 2. 日程管理区域 -->
<!-- 2. 主要内容区域 -->
<view class="main-content">
<!-- 近期日程标题 -->
<view class="schedule-header">
<text class="schedule-title">近期日程</text>
<view class="schedule-link" @click="goToSchedule">
<text class="link-text">查看日程</text>
<text class="link-arrow">></text>
<!-- 快捷入口卡片 -->
<view class="quick-entry-card">
<view class="entry-item" @click="goToTeacherProfile">
<view class="entry-icon icon-profile"></view>
<text class="entry-title">个人档案</text>
<text class="entry-arrow"></text>
</view>
<view class="entry-divider"></view>
<view class="entry-item" @click="goToRengJiaoRengZhi">
<view class="entry-icon icon-position"></view>
<text class="entry-title">任职任教</text>
<text class="entry-arrow"></text>
</view>
<view class="entry-divider"></view>
<view class="entry-item" @click="goToSchedule">
<view class="entry-icon icon-schedule"></view>
<text class="entry-title">近期日程</text>
<text class="entry-arrow"></text>
</view>
</view>
<!-- 近期日程 -->
<view class="section-header">
<text class="section-title">近期日程</text>
<view class="section-link" @click="goToSchedule">
<text>查看全部</text>
<text class="link-arrow"></text>
</view>
</view>
@ -127,17 +131,6 @@ import { useCommonStore } from "@/store/modules/common";
import { imagUrl } from "@/utils";
import { reactive, ref, computed, onMounted } from "vue";
//
interface TeacherData {
name: string;
position: string;
className: string;
score: number;
workload: number;
introduction: string;
avatar?: string;
}
//
interface ScheduleItem {
startTime: string;
@ -158,48 +151,19 @@ interface DateItem {
date: string;
}
const { logout, getUser, getJs } = useUserStore();
const { getUser, getJs } = useUserStore();
const { getZwListByLx } = useCommonStore();
//
const jsTx = computed(() => imagUrl(getUser.profilePhoto));
//
const js = computed(() => getJs);
//
const dzZwLabel = ref<string>("");
const qtZwLabel = ref<string>("");
//
const teacherData = reactive<TeacherData>({
name: getJs.jsxm || getUser.loginName,
position: "", //
className: getJs.njz || "",
score: 88, //
workload: 40, //
introduction: getJs.introduction || "北冥有鱼,其名为鲲。鲲之大,不知其几千里也。",
avatar: imagUrl(getUser.profilePhoto),
});
//
const teacherPosition = computed(() => {
const positions = [];
if (dzZwLabel.value) positions.push(dzZwLabel.value);
if (qtZwLabel.value) positions.push(qtZwLabel.value);
return positions.join('、') || '暂无职务信息';
});
// 退
const handleLogout = () => {
uni.showModal({
title: "确认退出",
content: "确定要退出登录吗?",
success: (res) => {
if (res.confirm) {
logout();
uni.reLaunch({
url: "/pages/system/login/login",
});
}
},
});
};
//
const selectedDateIndex = ref(0); //
@ -325,6 +289,20 @@ const goToSchedule = () => {
});
};
//
const goToRengJiaoRengZhi = () => {
uni.navigateTo({
url: '/pages/view/routine/RengJiaoRengZhi/index'
});
};
//
const goToTeacherProfile = () => {
uni.navigateTo({
url: '/pages/view/hr/teacherProfile/index'
});
};
onMounted(async () => {
//
await initPositionInfo();
@ -347,7 +325,7 @@ onMounted(async () => {
position: relative;
}
// Header - service
// Header
.header-section {
position: relative;
background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 50%, #ec4899 100%);
@ -371,24 +349,6 @@ onMounted(async () => {
z-index: 1;
}
.logout-btn {
position: absolute;
top: 40rpx;
right: 30rpx;
z-index: 3;
background: rgba(255, 255, 255, 0.2);
border-radius: 20rpx;
padding: 10rpx 20rpx;
border: 1px solid rgba(255, 255, 255, 0.3);
backdrop-filter: blur(10px);
.logout-text {
color: #ffffff;
font-size: 14px;
font-weight: 500;
}
}
.teacher-info {
display: flex;
align-items: center;
@ -434,52 +394,24 @@ onMounted(async () => {
}
}
.stats-info {
display: flex;
align-items: center;
justify-content: center;
margin-top: 20rpx;
z-index: 2;
position: relative;
.stat-item {
display: flex;
align-items: center;
.stat-label {
font-size: 14px;
color: rgba(255, 255, 255, 0.9);
}
.stat-value {
font-size: 14px;
color: #ffffff;
font-weight: 600;
}
}
.stat-divider {
margin: 0 30rpx;
color: rgba(255, 255, 255, 0.6);
font-size: 14px;
}
}
.teacher-intro {
text-align: center;
font-size: 12px;
color: rgba(255, 255, 255, 0.8);
margin-top: 20rpx;
line-height: 1.4;
font-size: 15px;
color: rgba(255, 255, 255, 0.95);
margin-top: -10rpx;
line-height: 1.8;
z-index: 2;
position: relative;
white-space: pre-line;
font-weight: 500;
letter-spacing: 1px;
}
}
//
//
.main-content {
box-sizing: border-box;
padding: 20px 12px 0px 12px; // padding
padding: 20px 15px 40px 15px;
position: relative;
background: linear-gradient(
to bottom,
@ -490,59 +422,118 @@ onMounted(async () => {
margin-top: -20px;
z-index: 3;
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.1);
height: calc(100vh - 340rpx); //
min-height: calc(100vh - 340rpx);
display: flex;
flex-direction: column;
overflow: hidden; //
overflow: hidden;
}
//
.schedule-header {
//
.quick-entry-card {
background: #fff;
border-radius: 20rpx;
overflow: hidden;
margin-bottom: 28rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
.entry-item {
display: flex;
align-items: center;
padding: 28rpx 24rpx;
transition: background 0.2s;
&:active {
background: #f8fafc;
}
}
.entry-icon {
width: 56rpx;
height: 56rpx;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
font-weight: 600;
color: #fff;
margin-right: 24rpx;
}
.icon-profile {
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
}
.icon-position {
background: linear-gradient(135deg, #0ea5e9 0%, #06b6d4 100%);
}
.icon-schedule {
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
}
.entry-title {
flex: 1;
font-size: 30rpx;
font-weight: 500;
color: #1e293b;
}
.entry-arrow {
font-size: 36rpx;
color: #94a3b8;
font-weight: 300;
}
.entry-divider {
height: 1rpx;
background: #f1f5f9;
margin-left: 104rpx;
}
}
//
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
flex-shrink: 0; //
margin-bottom: 20rpx;
padding: 0 4rpx;
flex-shrink: 0;
.schedule-title {
font-size: 17px;
.section-title {
font-size: 32rpx;
font-weight: 600;
color: #1f2937;
color: #1e293b;
}
.schedule-link {
.section-link {
display: flex;
align-items: center;
cursor: pointer;
.link-text {
font-size: 13px;
color: #6b7280;
margin-right: 4px;
}
font-size: 26rpx;
color: #64748b;
.link-arrow {
font-size: 13px;
color: #6b7280;
margin-left: 4rpx;
font-size: 28rpx;
font-weight: 300;
}
}
}
//
.date-selector {
margin-bottom: 15px;
background: #ffffff;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
flex-shrink: 0; //
margin-left: 0;
margin-right: 0;
margin-bottom: 24rpx;
background: #fff;
border-radius: 20rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
flex-shrink: 0;
padding: 16rpx 8rpx;
.date-container {
display: flex;
padding: 8px 4px;
justify-content: space-around; // 使space-around
width: 100%; //
justify-content: space-between;
width: 100%;
box-sizing: border-box;
}
@ -550,54 +541,44 @@ onMounted(async () => {
display: flex;
flex-direction: column;
align-items: center;
padding: 6px 2px;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
padding: 16rpx 8rpx;
border-radius: 16rpx;
flex: 1;
max-width: calc((100% - 48rpx) / 7);
min-width: 0;
position: relative;
flex: 1; //
max-width: calc((100% - 32px) / 7); // padding
min-width: 0; //
.date-day {
font-size: 10px;
color: #6b7280;
margin-bottom: 2px;
white-space: nowrap; //
text-align: center;
font-size: 22rpx;
color: #64748b;
margin-bottom: 8rpx;
}
.date-number {
font-size: 14px;
font-size: 28rpx;
font-weight: 600;
color: #374151;
white-space: nowrap; //
text-align: center;
color: #334155;
}
.date-dot {
position: absolute;
bottom: 1px;
width: 3px;
height: 3px;
bottom: 4rpx;
width: 8rpx;
height: 8rpx;
background: #10b981;
border-radius: 50%;
}
&.active {
background: #10b981;
color: #ffffff;
transform: scale(1.02); //
z-index: 1; //
margin: 0 -1px; //
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
.date-day,
.date-number {
color: #ffffff;
color: #fff;
}
.date-dot {
background: #ffffff;
background: #fff;
}
}
}
@ -605,183 +586,130 @@ onMounted(async () => {
//
.schedule-list {
flex: 1; //
height: 0; // flex使
padding-bottom: 20px;
flex: 1;
height: 0;
background: #fff;
border-radius: 20rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
overflow: hidden;
.schedule-content {
padding-bottom: 35px; //
padding: 0 24rpx 40rpx;
}
.schedule-item {
display: flex;
padding: 14px 0;
border-bottom: 1px solid #f3f4f6;
display: flex;
padding: 28rpx 0;
border-bottom: 1rpx solid #f1f5f9;
&:last-child {
border-bottom: none;
&:last-child {
border-bottom: none;
}
.time-column {
width: 100rpx;
flex-shrink: 0;
margin-right: 24rpx;
.start-time {
display: block;
font-size: 28rpx;
font-weight: 600;
color: #334155;
line-height: 1.3;
}
.time-column {
width: 60px;
flex-shrink: 0;
margin-right: 15px;
.end-time {
display: block;
font-size: 24rpx;
color: #64748b;
margin-top: 4rpx;
line-height: 1.3;
}
}
.start-time {
display: block;
font-size: 15px;
font-weight: 600;
color: #374151;
line-height: 1.2;
.content-column {
flex: 1;
min-width: 0;
.event-header {
display: flex;
align-items: center;
margin-bottom: 12rpx;
.event-tag {
display: inline-flex;
align-items: center;
padding: 6rpx 16rpx;
border-radius: 12rpx;
margin-right: 12rpx;
flex-shrink: 0;
&.meeting { background: #10b981; }
&.class { background: #3b82f6; }
&.tutoring { background: #f59e0b; }
&.preparation { background: #8b5cf6; }
&.counseling { background: #ef4444; }
.tag-text {
font-size: 22rpx;
color: #fff;
font-weight: 500;
}
}
.end-time {
display: block;
font-size: 13px;
color: #6b7280;
margin-top: 2px;
line-height: 1.2;
.event-title {
font-size: 28rpx;
font-weight: 500;
color: #1e293b;
flex: 1;
line-height: 1.4;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.content-column {
flex: 1;
min-width: 0; //
.event-header {
.event-details {
.event-location,
.event-description {
display: flex;
align-items: center;
margin-bottom: 8px;
margin-bottom: 8rpx;
.event-tag {
display: inline-flex;
align-items: center;
padding: 3px 8px;
border-radius: 12px;
margin-right: 8px;
flex-shrink: 0; //
&.meeting {
background: #10b981;
}
&.class {
background: #3b82f6;
}
&.tutoring {
background: #f59e0b;
}
&.preparation {
background: #8b5cf6;
}
&.counseling {
background: #ef4444;
}
.tag-text {
font-size: 11px;
color: #ffffff;
font-weight: 500;
white-space: nowrap;
}
.location-icon,
.description-icon {
font-size: 24rpx;
margin-right: 8rpx;
}
.event-title {
font-size: 15px;
font-weight: 500;
color: #1f2937;
flex: 1;
line-height: 1.3;
.location-text,
.description-text {
font-size: 24rpx;
color: #64748b;
line-height: 1.4;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap; //
}
}
.event-details {
.event-location,
.event-description {
display: flex;
align-items: center;
margin-bottom: 4px;
.location-icon,
.description-icon {
font-size: 13px;
margin-right: 4px;
flex-shrink: 0;
}
.location-text,
.description-text {
font-size: 13px;
color: #6b7280;
line-height: 1.3;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap; //
}
white-space: nowrap;
}
}
}
}
.no-schedule {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
min-height: 200px;
.no-schedule-text {
font-size: 15px;
color: #9ca3af;
}
}
}
//
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
//
@media (max-width: 375px) {
.main-content {
padding: 20px 8px 0px 8px; // padding
}
.date-selector {
.date-container {
padding: 8px 2px; //
}
.date-item {
padding: 5px 1px; //
.date-day {
font-size: 9px; //
}
.date-number {
font-size: 13px; //
}
&.active {
transform: scale(1.01); //
margin: 0; //
}
.no-schedule {
display: flex;
align-items: center;
justify-content: center;
min-height: 280rpx;
padding: 60rpx 0;
.no-schedule-text {
font-size: 28rpx;
color: #94a3b8;
}
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@ -48,7 +48,6 @@
class="section-block"
v-for="(section, index) in sections"
:key="section.id"
v-show="hasSectionPermission(section)"
>
<view class="section-title">
<view
@ -61,7 +60,7 @@
<view class="section-grid-card">
<template v-for="(item, itemIdx) in section.items" :key="item.id">
<view
v-if="item.show && hasPermissionDirect(item.permissionKey)"
v-if="item.show"
class="grid-item"
@click="handleGridItemClick(item)"
>
@ -122,7 +121,7 @@
<view class="course-options">
<template v-for="option in courseManagementOptions" :key="option.id">
<view
v-if="option.show && hasPermissionDirect(option.permissionKey)"
v-if="option.show"
class="course-option"
@click="handleCourseOptionClick(option)"
>
@ -144,13 +143,14 @@
</template>
<script lang="ts" setup>
import {getMobileMenuApi, MobileMenuTreeNode} from "@/api/system/menu";
import {jsdfindJsByPhoneApi, clearUserOpenIdApi} from "@/api/base/server";
import {useCommonStore} from "@/store/modules/common";
import {useUserStore} from "@/store/modules/user";
import {useDataStore} from "@/store/modules/data";
import {useMenuStore} from "@/store/modules/menu";
import {imagUrl} from "@/utils";
import {hideLoading, showLoading} from "@/utils/uniapp";
import {hasPermission} from "@/utils/permission";
import {set} from "lodash";
import {reactive, ref, computed, watch, onMounted} from "vue";
import { onShow } from "@dcloudio/uni-app";
@ -158,6 +158,7 @@ import { onShow } from "@dcloudio/uni-app";
const {logout, getUser, getJs, setJs} = useUserStore();
const {getZwListByLx} = useCommonStore();
const dataStore = useDataStore();
const menuStore = useMenuStore();
const jsTx = computed(() => imagUrl(getUser.profilePhoto));
@ -379,6 +380,7 @@ const handleLogout = () => {
};
//
/*
const sections = reactive<Section[]>([
{
id: "gnyy",
@ -836,6 +838,40 @@ const sections = reactive<Section[]>([
},
],
},
{
id: "routine-da",
icon: "da",
text: "基础档案",
show: true,
permissionKey: "routine-da",
isFolder: true,
folderItems: [
{
id: "routine-xsda",
icon: "xsda",
text: "学生档案",
show: true,
permissionKey: "routine-xsda", //
path: "/pages/view/routine/da/xsda/studentArchive",
},
{
id: "routine-jzda",
icon: "jzda",
text: "家长档案",
show: true,
permissionKey: "routine-jzda",
path: "/pages/view/routine/da/jzda/index",
},
{
id: "routine-jsda",
icon: "jsda",
text: "教师档案",
show: true,
permissionKey: "routine-jsda",
path: "/pages/view/routine/da/jsda/index",
},
],
},
],
},
{
@ -860,14 +896,6 @@ const sections = reactive<Section[]>([
permissionKey: "routine-jszr", //
path: "/pages/view/routine/JiaoXueZiYuan/index",
},
{
id: "r4",
icon: "rzrj",
text: "任教任职",
show: true,
permissionKey: "routine-rzrj", //
path: "/pages/view/routine/RengJiaoRengZhi/index",
},
{
id: "r10",
icon: "qdfb",
@ -988,14 +1016,7 @@ const sections = reactive<Section[]>([
permissionKey: "routine-jltj", //
path: "/pages/view/analysis/jl/index",
},
{
id: "hs4",
icon: "xsda",
text: "学生档案",
show: true,
permissionKey: "home-xsda", //
path: "/pages/view/analysis/xs/studentArchive",
},
{
id: "hs5",
icon: "xkqd",
@ -1035,14 +1056,6 @@ const sections = reactive<Section[]>([
permissionKey: "personnel-qjsq", //
path: "/pages/view/hr/jsQj/index",
},
{
id: "hr2",
icon: "jsda",
text: "教师档案",
show: true,
permissionKey: "personnel-jsda", //
path: "/pages/view/hr/teacherProfile/index",
},
{
id: "r12",
icon: "gw",
@ -1071,6 +1084,10 @@ const sections = reactive<Section[]>([
},
]);
*/
// getMobileMenuApi onMounted
const sections = reactive<Section[]>([]);
//
const getSectionColor = (index: number) => {
@ -1154,17 +1171,83 @@ const onHide = () => {
//
};
//
// sections
// "" children sections
function transformMobileMenuToSections(nodes: MobileMenuTreeNode[]): Section[] {
let workNodes = nodes;
if (nodes.length === 1 && !nodes[0].pagePath && (nodes[0].children?.length ?? 0) > 0) {
workNodes = nodes[0].children!;
}
return workNodes.map((node, idx) => {
const sectionId = node.id ?? `section-${idx}`;
const items: GridItem[] = [];
(node.children || []).forEach((child) => {
if (child.pagePath) {
items.push({
id: child.id,
icon: (child.normalCss && /^[a-zA-Z0-9_-]+$/.test(child.normalCss)) ? child.normalCss : "app",
text: child.screenName,
show: true,
permissionKey: child.authCode || "",
path: child.pagePath,
});
} else if (child.children?.length) {
const folderItems: GridItem[] = (child.children || []).map((sub) => ({
id: sub.id,
icon: (sub.normalCss && /^[a-zA-Z0-9_-]+$/.test(sub.normalCss)) ? sub.normalCss : "app",
text: sub.screenName,
show: true,
permissionKey: sub.authCode || "",
path: sub.pagePath,
}));
items.push({
id: child.id,
icon: (child.normalCss && /^[a-zA-Z0-9_-]+$/.test(child.normalCss)) ? child.normalCss : "app",
text: child.screenName,
show: true,
permissionKey: child.authCode || "",
isFolder: true,
folderItems,
});
}
});
return {
id: sectionId,
title: node.screenName,
permissionKey: node.authCode || undefined,
items,
};
});
}
// launchPage changeTime service 使 menuStore
onMounted(async () => {
//
await initPositionInfo();
const cachedMenu = menuStore.getMobileMenu;
const needFetchMenu = !cachedMenu?.length;
if (cachedMenu?.length > 0 && !needFetchMenu) {
const apiSections = transformMobileMenuToSections(cachedMenu);
sections.splice(0, sections.length, ...apiSections);
}
const menuPromise = needFetchMenu
? getMobileMenuApi()
.then((res) => {
const menuList = res?.result;
if (menuList && Array.isArray(menuList) && menuList.length > 0) {
menuStore.setMobileMenu(menuList);
const apiSections = transformMobileMenuToSections(menuList);
sections.splice(0, sections.length, ...apiSections);
}
})
.catch(() => {})
: Promise.resolve();
await Promise.all([initPositionInfo(), menuPromise]);
//
isLoading.value = false;
//
const {clearPermissionCachePublic} = await import("@/utils/permission");
clearPermissionCachePublic();
});
//
@ -1197,35 +1280,12 @@ async function initPositionInfo() {
}
}
//
const hasSectionPermission = (section: Section) => {
if (!section.permissionKey) {
return true; //
}
return hasPermissionDirect(section.permissionKey);
};
//
const hasPermissionDirect = (permissionKey: string) => {
if (!permissionKey) return true;
const userStore = useUserStore();
const permissions = userStore.getAuth;
if (!permissions || permissions.length === 0) return false;
const uniquePermissions = [...new Set(permissions)];
return uniquePermissions.includes(permissionKey);
};
//
// getMobileMenuApi
const getVisibleFolderItems = (folderItems: GridItem[] | undefined) => {
if (!folderItems || !Array.isArray(folderItems)) {
return [];
}
// showtrue
return folderItems.filter(item => {
return item.show && hasPermissionDirect(item.permissionKey);
});
return folderItems.filter(item => item.show);
};
// uni-app

View File

@ -14,46 +14,19 @@
<script lang="ts" setup>
import {onLoad} from "@dcloudio/uni-app";
import {useDataStore} from "@/store/modules/data";
import {useMenuStore} from "@/store/modules/menu";
import {useUserStore} from "@/store/modules/user";
import {checkOpenId} from "@/api/system/login";
import {getMobileMenuApi} from "@/api/system/menu";
import {getPermissionChangeTimeApi} from "@/api/system/config";
const dataStore = useDataStore();
const { setGlobal, getGlobal, setFile, getFile, refreshMessageBadge } = dataStore;
const menuStore = useMenuStore();
const { setGlobal, getFile } = dataStore;
const userStore = useUserStore();
const { afterLoginAction } = userStore;
const isShow = ref(true);
/**
* 强制刷新权限
*/
async function forceRefreshPermission(changeTime?: string): Promise<void> {
try {
//
const userStore = useUserStore();
const currentUser = userStore.getUser;
if (currentUser && currentUser.id) {
//
const {authenticationApi} = await import('@/api/system/login');
const result = await authenticationApi({userId: currentUser.id});
if (result && result.result) {
// setAuth
userStore.auth = result.result;
//
const {refreshPermissionCache} = await import('@/utils/permission');
refreshPermissionCache(result.result, changeTime);
}
}
} catch (error) {
console.error('强制刷新权限失败:', error);
}
}
function goByqd(data: any) {
if (data && data.qdId) {
//
@ -81,9 +54,9 @@ function goByJs(js: any) {
url: "/pages/base/service/index",
});
} else {
setFile({
dataStore.setFile({
...js,
...getFile,
...dataStore.getFile,
});
setTimeout(() => {
uni.reLaunch({
@ -117,19 +90,35 @@ onLoad(async (data: any) => {
//
afterLoginAction(res.result);
//
await refreshMessageBadge(res.result?.js?.id);
// changeTime
if (data.changeTime) {
const {refreshPermissionCache} = await import('@/utils/permission');
const userStore = useUserStore();
const currentPermissions = userStore.getAuth;
if (currentPermissions && currentPermissions.length > 0) {
refreshPermissionCache(currentPermissions, data.changeTime);
}
//
let needFetchMenu = true;
try {
const timeRes = await getPermissionChangeTimeApi();
console.log('[launchPage] getPermissionChangeTime 原始返回:', JSON.stringify(timeRes));
const serverChangeTime = String((timeRes as any)?.result ?? '');
const localChangeTime = userStore.getChangeTime || '';
const localMenu = menuStore.getMobileMenu || [];
needFetchMenu = serverChangeTime !== localChangeTime || !localMenu?.length;
console.log('[launchPage] serverChangeTime:', serverChangeTime, 'localChangeTime:', localChangeTime, 'localMenuLength:', localMenu?.length ?? 0, 'needFetchMenu:', needFetchMenu);
if (serverChangeTime) userStore.setChangeTime(serverChangeTime);
} catch (e) {
console.warn('[launchPage] getPermissionChangeTime 失败:', e);
needFetchMenu = true;
}
if (needFetchMenu) {
console.log('[launchPage] 调用 find-mobile-menu');
await getMobileMenuApi()
.then((r) => {
if (r?.result && Array.isArray(r.result) && r.result.length > 0) {
menuStore.setMobileMenu(r.result);
}
})
.catch(() => {});
} else {
console.log('[launchPage] 跳过 find-mobile-menu使用本地缓存');
}
if (data && data.qdId) {
goByqd(data);
} else {

View File

@ -1,583 +0,0 @@
<template>
<view class="student-archive-container">
<!-- 班级选择器 -->
<view class="section">
<view class="section-title">选择班级</view>
<BasicNjBjPicker
v-model="classInfo"
placeholder="请选择年级班级"
@change="onClassChange"
icon-arrow="right"
:customStyle="{ backgroundColor: '#fff', borderRadius: '0', padding: '12px 15px' }"
/>
<!-- 班级选择提示 -->
<view v-if="!classInfo" class="class-tip">
<text class="tip-icon"></text>
<text class="tip-text">请先选择班级</text>
</view>
</view>
<!-- 学生档案统计 -->
<view class="section" v-if="classInfo">
<view class="section-title">
学生档案统计
</view>
<!-- 统计信息 -->
<view class="stats-container">
<view
class="stat-item"
:class="{ active: selectedStatType === 'all' }"
@click="onStatItemClick('all')"
>
<text class="stat-number">{{ studentList.length }}</text>
<text class="stat-label">总人数</text>
</view>
<view
class="stat-item"
:class="{ active: selectedStatType === 'followed' }"
@click="onStatItemClick('followed')"
>
<text class="stat-number followed">{{ followedCount }}</text>
<text class="stat-label">已关注</text>
</view>
<view
class="stat-item"
:class="{ active: selectedStatType === 'unfollowed' }"
@click="onStatItemClick('unfollowed')"
>
<text class="stat-number unfollowed">{{ unfollowedCount }}</text>
<text class="stat-label">未关注</text>
</view>
</view>
</view>
<!-- 学生档案列表 -->
<view class="section" v-if="classInfo && studentList.length > 0">
<view class="section-title">
{{ getListTitle() }} ({{ filteredStudentList.length }})
</view>
<!-- 学生列表 - 改为card形式 -->
<view class="student-list">
<view class="student-grid">
<view
v-for="student in filteredStudentList"
:key="student.xsId"
class="student-item bg-white r-md p-12"
@click="viewStudentDetail(student)"
>
<view class="flex-row items-center">
<view class="avatar-container mr-8">
<image
class="student-avatar"
:src="imagUrl(student.xstx || '') || '/static/images/default-avatar.png'"
mode="aspectFill"
></image>
</view>
<view class="flex-1 overflow-hidden">
<view class="student-name mb-8">
<text class="font-14 cor-333">{{ student.xsxm }}</text>
</view>
<view class="flex-row">
<!-- 关注状态标签 -->
<view
class="status-tag"
:class="getFollowStatusClass(student.jzxm)"
>
{{ student.jzxm ? '已关注' : '未关注' }}
</view>
</view>
</view>
<!-- 箭头图标 -->
<view class="arrow-icon-container" @click.stop="viewStudentDetail(student)">
<image
class="arrow-icon"
src="/static/base/view/more.png"
mode="aspectFit"
></image>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view class="empty-state" v-if="!loading && studentList.length === 0 && hasSearched">
<view class="empty-icon">📚</view>
<view class="empty-text">暂无学生档案数据</view>
<view class="empty-tip">请选择年级班级后查询</view>
</view>
<!-- 加载状态 -->
<view class="loading-state" v-if="loading">
<view class="loading-text">正在加载...</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { BasicNjBjPicker } from '@/components/BasicNjBjPicker'
import { onLoad } from '@dcloudio/uni-app'
import { findStudentInfoByNjAndBjSimpleApi } from '@/api/analysis/xs'
import { imagUrl } from "@/utils"
import { useDataStore } from '@/store/modules/data'
//
interface ClassInfo {
njId: string
bjId: string
nj: any
bj: any
}
interface StudentInfo {
xsId: string
xsxm: string
xstx?: string
njId: string
njmcId: string
njmc: string
bc: string
bjId: string
bjmc: string
jzIds?: string
jzxm?: string
xb?: string //
sfzh?: string //
cstime?: string //
}
interface ApiResponse {
code: number
data: StudentInfo[]
message?: string
}
//
const classInfo = ref<ClassInfo | null>(null)
const studentList = ref<StudentInfo[]>([])
const loading = ref(false)
const hasSearched = ref(false)
const selectedStatType = ref<string>('all') //
// 使store
const { setXs } = useDataStore()
//
const followedCount = computed(() => {
return studentList.value.filter(student => student.jzxm).length
})
const unfollowedCount = computed(() => {
return studentList.value.filter(student => !student.jzxm).length
})
//
const filteredStudentList = computed(() => {
if (selectedStatType.value === 'all') {
return studentList.value
} else if (selectedStatType.value === 'followed') {
return studentList.value.filter(student => student.jzxm)
} else if (selectedStatType.value === 'unfollowed') {
return studentList.value.filter(student => !student.jzxm)
}
return studentList.value
})
// -
const onClassChange = async (nj: any, bj: any) => {
console.log('年级班级选择变化:', nj, bj)
classInfo.value = {
njId: nj.key,
bjId: bj.key,
nj: nj,
bj: bj
}
//
await searchStudents()
}
//
const searchStudents = async () => {
if (!classInfo.value || !classInfo.value.njId || !classInfo.value.bjId) {
return
}
loading.value = true
hasSearched.value = true
try {
//
const response = await findStudentInfoByNjAndBjSimpleApi(classInfo.value!.njId, classInfo.value!.bjId)
console.log('API返回结果:', response)
if (response && response.resultCode === 1) {
studentList.value = response.result || []
uni.showToast({
title: `查询到${studentList.value.length}名学生`,
icon: 'success'
})
} else {
throw new Error(response?.message || '查询失败')
}
} catch (error) {
console.error('查询学生档案失败:', error)
uni.showToast({
title: '查询失败,请重试',
icon: 'none'
})
studentList.value = []
} finally {
loading.value = false
}
}
//
const refreshStudentList = () => {
searchStudents()
}
//
const onStatItemClick = (statType: string) => {
selectedStatType.value = statType
console.log('选中统计类型:', statType)
}
//
const getListTitle = () => {
switch (selectedStatType.value) {
case 'followed':
return '已关注学生'
case 'unfollowed':
return '未关注学生'
default:
return '学生档案列表'
}
}
//
const getFollowStatusClass = (jzxm?: string) => {
return jzxm ? 'status-followed' : 'status-unfollowed'
}
//
const viewStudentDetail = (student: StudentInfo) => {
console.log('查看学生详情:', student)
// store
setXs({
xsId: student.xsId,
id: student.xsId, //
xsxm: student.xsxm,
xm: student.xsxm, //
xstx: student.xstx,
avatar: student.xstx, //
njId: student.njId,
njmc: student.njmc,
njmcName: student.njmc, //
bjId: student.bjId,
bjmc: student.bjmc,
jzIds: student.jzIds,
jzxm: student.jzxm,
xb: student.xb, //
sfzh: student.sfzh, //
cstime: student.cstime //
})
//
uni.navigateTo({
url: '/pages/view/homeSchool/parentAddressBook/detail'
})
}
//
onLoad(() => {
console.log('学生档案页面加载')
})
</script>
<style lang="scss" scoped>
.student-archive-container {
min-height: 100vh;
background-color: #f5f5f5;
padding: 20rpx;
}
.section {
background-color: #fff;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 20rpx;
display: flex;
justify-content: space-between;
align-items: center;
}
.refresh-btn {
font-size: 24rpx;
color: #007aff;
font-weight: normal;
}
.stats-container {
display: flex;
justify-content: space-around;
margin-bottom: 30rpx;
padding: 20rpx;
background-color: #f8f9fa;
border-radius: 12rpx;
flex-wrap: wrap;
gap: 60rpx;
}
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
min-width: 120rpx;
padding: 20rpx 10rpx;
border-radius: 12rpx;
transition: all 0.2s ease;
cursor: pointer;
&.active {
background-color: #e6f7ff;
border: 2rpx solid #007aff;
transform: scale(1.05);
}
&:hover {
background-color: #f0f9ff;
transform: scale(1.02);
}
}
.stat-number {
font-size: 36rpx;
font-weight: bold;
color: #333;
margin-bottom: 8rpx;
&.followed {
color: #52c41a;
}
&.unfollowed {
color: #ff4d4f;
}
}
.stat-label {
font-size: 24rpx;
color: #666;
}
.student-list {
margin-bottom: 30rpx;
}
.student-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20rpx;
}
.student-item {
position: relative;
border-radius: 16rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
transition: all 0.2s;
min-height: 120rpx; //
}
.student-item:hover {
transform: translateY(-2rpx);
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.15);
}
.avatar-container {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
padding: 4rpx;
background-color: #fff;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.05);
flex-shrink: 0; //
}
.student-avatar {
width: 72rpx;
height: 72rpx;
border-radius: 50%;
background-color: #f5f5f5;
}
.status-tag {
font-size: 20rpx;
padding: 6rpx 16rpx;
border-radius: 8rpx;
display: flex;
align-items: center;
gap: 4rpx;
}
.status-followed {
background-color: #e6f7ff;
color: #52c41a;
}
.status-unfollowed {
background-color: #fff2f0;
color: #ff4d4f;
}
.flex-row {
display: flex;
flex-direction: row;
}
.flex-1 {
flex: 1;
}
.items-center {
align-items: center;
}
.overflow-hidden {
overflow: hidden;
}
.mr-8 {
margin-right: 12rpx;
}
.mb-8 {
margin-bottom: 16rpx;
}
.font-14 {
font-size: 28rpx;
}
.cor-333 {
color: #333;
}
.bg-white {
background-color: #fff;
}
.r-md {
border-radius: 16rpx;
}
.p-12 {
padding: 24rpx;
}
.arrow-icon-container {
width: 40rpx;
height: 40rpx;
display: flex;
align-items: center;
justify-content: center;
margin-left: 8rpx;
border-radius: 50%;
transition: all 0.2s ease;
flex-shrink: 0; //
&:hover {
background-color: #f0f0f0;
}
&:active {
background-color: #e0e0e0;
transform: scale(0.95);
}
}
.arrow-icon {
width: 35rpx;
height: 35rpx;
opacity: 0.6;
transition: opacity 0.2s ease;
}
.arrow-icon-container:hover .arrow-icon {
opacity: 1;
}
.class-tip {
display: flex;
align-items: center;
margin-top: 20rpx;
padding: 15rpx 20rpx;
background-color: #fffbe6;
border: 1rpx solid #ffe58f;
border-radius: 12rpx;
color: #faad14;
font-size: 28rpx;
font-weight: bold;
.tip-icon {
margin-right: 10rpx;
font-size: 32rpx;
}
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 40rpx;
.empty-icon {
font-size: 120rpx;
margin-bottom: 20rpx;
}
.empty-text {
font-size: 32rpx;
color: #666;
margin-bottom: 10rpx;
}
.empty-tip {
font-size: 28rpx;
color: #999;
}
}
.loading-state {
display: flex;
justify-content: center;
align-items: center;
padding: 100rpx;
.loading-text {
font-size: 32rpx;
color: #666;
}
}
</style>

View File

@ -79,7 +79,7 @@ import { ref } from "vue";
import { onLoad, onShow } from "@dcloudio/uni-app";
import dayjs from "dayjs";
import BasicSearch from "@/components/BasicSearch/Search.vue";
import { jfFindPageApi } from "@/api/base/jfApi";
import { jfFindPageApi, jfTypeStructureApi } from "@/api/base/jfApi";
import { navigateTo } from "@/utils/uniapp";
import { useUserStore } from "@/store/modules/user";
@ -90,6 +90,11 @@ const userStore = useUserStore();
const targetJsId = ref<string | null>(null); // ID
const targetNjId = ref<string | null>(null); // ID
const targetNjmc = ref<string>(""); //
const targetTopTypeId = ref<string | null>(null); // ID
const targetColumnType = ref<string>("approvedCount"); //
const targetStartTime = ref<string | null>(null); //
const targetEndTime = ref<string | null>(null); //
const allJfTypeIds = ref<string>(""); // ID
const handleSearch = (keyword: string) => {
searchKeyword.value = keyword;
@ -134,6 +139,19 @@ const queryData = async (pageNo: number, pageSize: number) => {
} else if (jsId) {
params.jsId = jsId; //
}
// ID
if (allJfTypeIds.value) {
params.jfTypeIds = allJfTypeIds.value;
}
//
if (targetStartTime.value) {
params.startTime = targetStartTime.value;
}
if (targetEndTime.value) {
params.endTime = targetEndTime.value;
}
console.log('请求参数:', params);
const res: any = await jfFindPageApi(params);
@ -153,18 +171,36 @@ const queryData = async (pageNo: number, pageSize: number) => {
console.log('result.total:', result?.total);
console.log('result.records:', result?.records);
// spJd === "Z" || spResult === "B"spResult === "C"
const completedRows = rows.filter((it) => {
// columnType
const filteredByType = rows.filter((it) => {
const spJd = it.spJd;
const spResult = it.spResult;
// spJd === "Z" || spResult === "B"
const columnType = targetColumnType.value;
if (columnType === 'approvedCount') {
// sp_jd = 'Z' AND sp_result = 'B'
return spJd === 'Z' && spResult === 'B';
} else if (columnType === 'pendingCount') {
// sp_jd = 'A' OR sp_result = 'A'
return spJd === 'A' || spResult === 'A';
} else if (columnType === 'rejectedCount') {
// sp_result = 'C'
return spResult === 'C';
} else if (columnType === 'draftCount') {
// sp_jd sp_result
const isSpJdEmpty = spJd === null || spJd === undefined || spJd === '';
const isSpResultEmpty = spResult === null || spResult === undefined || spResult === '';
return isSpJdEmpty && isSpResultEmpty;
}
//
return (spJd === "Z" || spResult === "B") && spResult !== "C";
});
console.log('已完结数据数量:', completedRows.length);
console.log('按类型过滤后数量:', filteredByType.length);
//
const filtered = completedRows.filter((it) => matchKeyword(it));
const filtered = filteredByType.filter((it) => matchKeyword(it));
console.log('过滤后数量:', filtered.length);
console.log('是否还有更多数据(根据数据量判断):', filtered.length === pageSize);
@ -229,8 +265,31 @@ const goEdit = (item: any) => {
navigateTo(`/pages/view/routine/JiFenPingJia/jfsp/JfFlow?id=${item.id}`);
};
// ID
const loadJfTypeIds = async (topTypeId: string) => {
try {
const res: any = await jfTypeStructureApi({ topTypeId });
const structureList = res?.result || res || [];
// ID
const ids: string[] = [topTypeId];
structureList.forEach((item: any) => {
if (item.jfTypeId && !ids.includes(item.jfTypeId)) {
ids.push(item.jfTypeId);
}
});
allJfTypeIds.value = ids.join(',');
console.log('大类及子类ID:', allJfTypeIds.value);
} catch (error) {
console.error('获取大类结构失败:', error);
// ID
allJfTypeIds.value = topTypeId;
}
};
//
onLoad((options: any) => {
onLoad(async (options: any) => {
if (options?.jsId) {
targetJsId.value = options.jsId;
console.log('接收到的教师ID:', targetJsId.value);
@ -240,6 +299,24 @@ onLoad((options: any) => {
targetNjmc.value = options?.njmc || "";
console.log('接收到的年级ID:', targetNjId.value, '年级名称:', targetNjmc.value);
}
if (options?.topTypeId) {
targetTopTypeId.value = options.topTypeId;
console.log('接收到的大类ID:', targetTopTypeId.value);
// ID
await loadJfTypeIds(options.topTypeId);
}
if (options?.columnType) {
targetColumnType.value = options.columnType;
console.log('接收到的列类型:', targetColumnType.value);
}
if (options?.startTime) {
targetStartTime.value = decodeURIComponent(options.startTime);
console.log('接收到的开始时间:', targetStartTime.value);
}
if (options?.endTime) {
targetEndTime.value = decodeURIComponent(options.endTime);
console.log('接收到的结束时间:', targetEndTime.value);
}
});
onShow(() => {

View File

@ -11,7 +11,7 @@
</view>
</view>
<view class="total-score">
<text class="score-number">{{ totalScore }}</text>
<text class="score-number">{{ totalApprovedScore }}</text>
<text class="score-label">总积分</text>
</view>
</view>
@ -46,30 +46,30 @@
<text class="picker-value">{{ endTime || '请选择' }}</text>
</view>
</picker>
<button class="query-btn-inline" @click="loadPerformanceRanking">查询</button>
<button class="query-btn-inline" @click="loadStatistics">查询</button>
</view>
</view>
</view>
<!-- 统计摘要暂时隐藏 -->
<!-- <view class="summary-section">
<!-- 统计摘要 -->
<view class="summary-section">
<view class="summary-item">
<text class="summary-number">{{ teacherCount }}</text>
<text class="summary-label">教师数</text>
</view>
<view class="summary-item">
<text class="summary-number">{{ totalRecords }}</text>
<text class="summary-label">记录总数</text>
<text class="summary-number">{{ totalApproved }}</text>
<text class="summary-label">已审批</text>
</view>
<view class="summary-item">
<text class="summary-number">{{ completedCount }}</text>
<text class="summary-label">已完结</text>
<text class="summary-number">{{ totalPending }}</text>
<text class="summary-label">待审批</text>
</view>
<view class="summary-item">
<text class="summary-number">{{ pendingCount }}</text>
<text class="summary-label">待审核</text>
<text class="summary-number">{{ totalRejected }}</text>
<text class="summary-label">已驳回</text>
</view>
</view> -->
</view>
<!-- 教师排名列表 -->
<view class="teacher-list">
@ -80,29 +80,56 @@
lower-threshold="100"
>
<!-- 加载状态 -->
<view v-if="isLoading && gradeList.length === 0" class="loading-indicator">
<view v-if="isLoading && teacherList.length === 0" class="loading-indicator">
<text class="loading-icon"></text>
<text class="loading-text">加载中...</text>
</view>
<!-- 排名列表 -->
<template v-else-if="gradeList.length > 0">
<template v-else-if="teacherList.length > 0">
<view
v-for="(grade, index) in gradeList"
:key="grade.njId || index"
v-for="(teacher, index) in teacherList"
:key="teacher.jsId || index"
class="teacher-card"
@click="goToDetail(teacher)"
>
<view class="card-main">
<view class="teacher-name">{{ grade.njmc || '未知年级' }}</view>
<!-- 排名标识 -->
<view class="rank-badge" :class="getRankClass(index)">
<text class="rank-number">{{ index + 1 }}</text>
</view>
<!-- 教师姓名 -->
<text class="teacher-name">{{ teacher.jsName || '未知' }}</text>
<!-- 积分徽章 -->
<view class="score-badge">
<text class="score-value">{{ grade.totalScore || 0 }}</text>
<text class="score-value">{{ teacher.approvedScore || 0 }}</text>
<text class="score-unit"></text>
</view>
</view>
<view class="teacher-info">
<view class="record-info" @click="goToDetail(grade.njId, grade.njmc)">
<text class="record-text">教师 {{ grade.teacherCount || 0 }} 记录 {{ grade.recordCount || 0 }} </text>
<view class="stats-row">
<view class="stat-item">
<text class="stat-label">已审批</text>
<text class="stat-value approved">{{ teacher.approvedScore || 0 }}</text>
</view>
<view class="stat-item">
<text class="stat-label">待审批</text>
<text class="stat-value pending">{{ teacher.pendingCount || 0 }}</text>
</view>
<view class="stat-item">
<text class="stat-label">已驳回</text>
<text class="stat-value rejected">{{ teacher.rejectedCount || 0 }}</text>
</view>
<view class="stat-item">
<text class="stat-label">暂存</text>
<text class="stat-value draft">{{ teacher.draftCount || 0 }}</text>
</view>
</view>
<view class="record-info">
<text class="record-text">查看大类明细</text>
<text class="record-arrow">></text>
</view>
</view>
@ -117,7 +144,7 @@
</view>
<!-- 加载更多 -->
<view v-if="isLoading && gradeList.length > 0" class="loading-more">
<view v-if="isLoading && teacherList.length > 0" class="loading-more">
<text class="loading-more-icon"></text>
<text class="loading-more-text">加载中...</text>
</view>
@ -129,20 +156,28 @@
<script lang="ts" setup>
import { ref, computed, onMounted } from "vue";
import { onShow } from "@dcloudio/uni-app";
import { getPerformanceRankingByGradeApi } from "@/api/base/jfApi";
import { jfStatisticsByTeacherApi } from "@/api/base/jfApi";
import { navigateTo } from "@/utils/uniapp";
import dayjs from "dayjs";
//
interface GradeRanking {
njId: string; // ID
njmc: string; //
totalScore: number; //
recordCount: number; //
teacherCount: number; //
interface TeacherStatistics {
jsId: string;
jsName: string;
approvedScore: number;
calculatedScore: number;
pendingCount: number;
rejectedCount: number;
draftCount: number;
categories: Array<{
topTypeId: string;
topTypeName: string;
approvedCount: number;
calculatedScore: number;
pendingCount: number;
rejectedCount: number;
draftCount: number;
}>;
}
//
const getCurrentYearRange = () => {
const year = new Date().getFullYear();
return {
@ -151,36 +186,41 @@ const getCurrentYearRange = () => {
};
};
//
const yearRange = getCurrentYearRange();
const startTime = ref(yearRange.start);
const endTime = ref(yearRange.end);
const gradeList = ref<GradeRanking[]>([]);
const teacherList = ref<TeacherStatistics[]>([]);
const isLoading = ref(false);
// -
const totalScore = computed(() => {
return gradeList.value.reduce((sum, g) => sum + (Number(g.totalScore) || 0), 0);
const teacherCount = computed(() => teacherList.value.length);
const totalApprovedScore = computed(() => {
return teacherList.value.reduce((sum, t) => sum + (Number(t.approvedScore) || 0), 0);
});
const totalApproved = computed(() => {
return teacherList.value.reduce((sum, t) => sum + (Number(t.approvedScore) || 0), 0);
});
const totalPending = computed(() => {
return teacherList.value.reduce((sum, t) => sum + (Number(t.pendingCount) || 0), 0);
});
const totalRejected = computed(() => {
return teacherList.value.reduce((sum, t) => sum + (Number(t.rejectedCount) || 0), 0);
});
//
const handleStartTimeChange = (e: any) => {
startTime.value = e.detail.value;
console.log('选择的开始时间:', startTime.value);
};
//
const handleEndTimeChange = (e: any) => {
endTime.value = e.detail.value;
console.log('选择的结束时间:', endTime.value);
};
//
const loadPerformanceRanking = async () => {
const loadStatistics = async () => {
if (isLoading.value) return;
//
if (startTime.value && endTime.value && startTime.value > endTime.value) {
uni.showToast({
title: '开始时间不能大于结束时间',
@ -191,8 +231,6 @@ const loadPerformanceRanking = async () => {
isLoading.value = true;
try {
console.log('加载业绩排名,时间范围:', startTime.value, '至', endTime.value);
const params: any = {};
if (startTime.value) {
params.startTime = startTime.value;
@ -201,39 +239,82 @@ const loadPerformanceRanking = async () => {
params.endTime = endTime.value;
}
const response = await getPerformanceRankingByGradeApi(params);
console.log('年级排名API响应:', response);
const response = await jfStatisticsByTeacherApi(params);
console.log('统计API响应:', response);
if (response && response.resultCode === 1) {
gradeList.value = response.result || [];
console.log('年级排名数据:', gradeList.value);
const rawData = response.result || [];
teacherList.value = processStatisticsData(rawData);
if (gradeList.value.length === 0) {
if (teacherList.value.length === 0) {
uni.showToast({
title: '暂无数据',
icon: 'none'
});
}
} else {
gradeList.value = [];
teacherList.value = [];
uni.showToast({
title: response?.message || '查询失败',
icon: 'none'
});
}
} catch (error) {
console.error('加载业绩排名失败:', error);
console.error('加载统计数据失败:', error);
uni.showToast({
title: '加载失败',
icon: 'error'
});
gradeList.value = [];
teacherList.value = [];
} finally {
isLoading.value = false;
}
};
//
const processStatisticsData = (rawData: any[]): TeacherStatistics[] => {
const teacherMap = new Map<string, TeacherStatistics>();
rawData.forEach((item: any) => {
const jsId = item.jsId;
if (!jsId) return;
if (!teacherMap.has(jsId)) {
teacherMap.set(jsId, {
jsId: jsId,
jsName: item.jsName || '未知',
approvedScore: 0,
calculatedScore: 0,
pendingCount: 0,
rejectedCount: 0,
draftCount: 0,
categories: []
});
}
const teacher = teacherMap.get(jsId)!;
teacher.approvedScore += Number(item.approvedCount || 0);
teacher.calculatedScore += Number(item.calculatedScore || 0);
teacher.pendingCount += Number(item.pendingCount || 0);
teacher.rejectedCount += Number(item.rejectedCount || 0);
teacher.draftCount += Number(item.draftCount || 0);
teacher.categories.push({
topTypeId: item.topTypeId,
topTypeName: item.topTypeName || '未分类',
approvedCount: Number(item.approvedCount || 0),
calculatedScore: Number(item.calculatedScore || 0),
pendingCount: Number(item.pendingCount || 0),
rejectedCount: Number(item.rejectedCount || 0),
draftCount: Number(item.draftCount || 0)
});
});
const result = Array.from(teacherMap.values());
result.sort((a, b) => b.approvedScore - a.approvedScore);
return result;
};
const getRankClass = (index: number) => {
if (index === 0) return 'rank-gold';
if (index === 1) return 'rank-silver';
@ -241,38 +322,34 @@ const getRankClass = (index: number) => {
return 'rank-normal';
};
//
const loadMore = () => {
//
};
// yjpm.vue
const goToDetail = (njId?: string, njmc?: string) => {
if (!njId) {
const goToDetail = (teacher: TeacherStatistics) => {
if (!teacher.jsId) {
uni.showToast({
title: '缺少年级ID',
title: '缺少教师ID',
icon: 'none'
});
return;
}
const start = startTime.value ? encodeURIComponent(startTime.value) : '';
const end = endTime.value ? encodeURIComponent(endTime.value) : '';
const name = njmc ? encodeURIComponent(njmc) : '';
const name = teacher.jsName ? encodeURIComponent(teacher.jsName) : '';
const categories = encodeURIComponent(JSON.stringify(teacher.categories));
navigateTo(
`/pages/view/routine/JiFenPingJia/jftj/yjpm?njId=${njId}&njmc=${name}&startTime=${start}&endTime=${end}`
`/pages/view/routine/JiFenPingJia/jftj/yjpm?jsId=${teacher.jsId}&jsName=${name}&startTime=${start}&endTime=${end}&categories=${categories}`
);
};
//
onMounted(() => {
console.log('业绩排名页面加载');
//
loadPerformanceRanking();
loadStatistics();
});
//
onShow(() => {
console.log('页面显示');
//
});
</script>
@ -284,7 +361,6 @@ onShow(() => {
background: linear-gradient(180deg, #f0f5ff 0%, #f5f7fa 100%);
}
//
.page-banner {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 20px 16px;
@ -352,7 +428,6 @@ onShow(() => {
}
}
//
.filter-bar {
display: flex;
flex-direction: column;
@ -382,52 +457,45 @@ onShow(() => {
.filter-right {
width: 100%;
.date-picker-wrapper {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
.date-picker-wrapper {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
.date-picker {
flex: 1;
min-width: 0;
.date-picker {
flex: 1;
min-width: 0;
.picker-view {
display: flex;
flex-direction: column;
padding: 10px 12px;
background-color: #f5f7ff;
border-radius: 8px;
border: 1px solid #d4dbff;
.picker-view {
display: flex;
flex-direction: column;
padding: 10px 12px;
background-color: #f5f7ff;
border-radius: 8px;
border: 1px solid #d4dbff;
.picker-label {
font-size: 11px;
color: #adb5bd;
margin-bottom: 4px;
}
.picker-value {
font-size: 14px;
color: #212529;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.picker-value {
font-size: 14px;
color: #212529;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
.date-separator {
font-size: 14px;
color: #666;
padding: 0 4px;
flex-shrink: 0;
}
}
.date-separator {
font-size: 14px;
color: #666;
padding: 0 4px;
flex-shrink: 0;
}
}
}
}
//
.query-btn-inline {
flex-shrink: 0;
height: 36px;
@ -448,7 +516,6 @@ onShow(() => {
}
}
//
.summary-section {
display: flex;
justify-content: space-around;
@ -476,7 +543,6 @@ onShow(() => {
}
}
//
.teacher-list {
flex: 1;
overflow: hidden;
@ -491,7 +557,6 @@ onShow(() => {
}
}
//
.teacher-card {
background: linear-gradient(135deg, #ffffff 0%, #f5f7ff 100%);
border-radius: 16px;
@ -510,7 +575,6 @@ onShow(() => {
0 0 0 1px rgba(102, 126, 234, 0.15);
}
//
&::before {
content: '';
position: absolute;
@ -597,6 +661,43 @@ onShow(() => {
.teacher-info {
padding: 12px 16px 16px 16px;
.stats-row {
display: flex;
justify-content: space-between;
margin-bottom: 12px;
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
.stat-label {
font-size: 11px;
color: #999;
margin-bottom: 4px;
}
.stat-value {
font-size: 16px;
font-weight: 600;
&.approved {
color: #52c41a;
}
&.pending {
color: #faad14;
}
&.rejected {
color: #ff4d4f;
}
&.draft {
color: #999;
}
}
}
}
.record-info {
display: flex;
align-items: center;
@ -624,7 +725,6 @@ onShow(() => {
}
}
//
.loading-indicator {
display: flex;
flex-direction: column;
@ -644,7 +744,6 @@ onShow(() => {
}
}
//
.empty-state {
display: flex;
flex-direction: column;
@ -671,7 +770,6 @@ onShow(() => {
}
}
//
.loading-more {
display: flex;
align-items: center;
@ -691,7 +789,6 @@ onShow(() => {
}
}
//
@keyframes pulse {
0%, 100% {
opacity: 1;
@ -703,4 +800,3 @@ onShow(() => {
}
}
</style>

View File

@ -1,17 +1,17 @@
<template>
<view class="performance-ranking-page">
<view class="performance-detail-page">
<!-- 页面标题横幅 -->
<view class="page-banner">
<view class="banner-content">
<view class="banner-title-wrapper">
<text class="banner-icon">🏆</text>
<text class="banner-icon">📊</text>
<view class="banner-text">
<text class="banner-title">业绩排名</text>
<text class="banner-subtitle">{{ njmc || '教师积分排名统计' }}</text>
<text class="banner-title">{{ jsName || '教师' }}</text>
<text class="banner-subtitle">积分大类明细</text>
</view>
</view>
<view class="total-score">
<text class="score-number">{{ totalScore }}</text>
<text class="score-number">{{ totalApprovedScore }}</text>
<text class="score-label">总积分</text>
</view>
</view>
@ -46,72 +46,80 @@
<text class="picker-value">{{ endTime || '请选择' }}</text>
</view>
</picker>
<button class="query-btn-inline" @click="loadPerformanceRanking">查询</button>
<button class="query-btn-inline" @click="loadStatistics">查询</button>
</view>
</view>
</view>
<!-- 统计摘要暂时隐藏 -->
<!-- <view class="summary-section">
<!-- 汇总统计 -->
<view class="summary-section">
<view class="summary-item">
<text class="summary-number">{{ teacherCount }}</text>
<text class="summary-label">教师数</text>
<text class="summary-number approved">{{ totalApprovedScore }}</text>
<text class="summary-label">已审批积分</text>
</view>
<view class="summary-item">
<text class="summary-number">{{ totalRecords }}</text>
<text class="summary-label">记录总数</text>
<text class="summary-number calculated">{{ totalCalculatedScore }}</text>
<text class="summary-label">公式计算</text>
</view>
<view class="summary-item">
<text class="summary-number">{{ completedCount }}</text>
<text class="summary-label">已完结</text>
<text class="summary-number pending">{{ totalPending }}</text>
<text class="summary-label">待审批</text>
</view>
<view class="summary-item">
<text class="summary-number">{{ pendingCount }}</text>
<text class="summary-label">待审核</text>
<text class="summary-number rejected">{{ totalRejected }}</text>
<text class="summary-label">已驳回</text>
</view>
</view> -->
</view>
<!-- 教师排名列表 -->
<view class="teacher-list">
<!-- 大类明细列表 -->
<view class="category-list">
<scroll-view
scroll-y
class="list-scroll-view"
@scrolltolower="loadMore"
lower-threshold="100"
>
<!-- 加载状态 -->
<view v-if="isLoading && rankingList.length === 0" class="loading-indicator">
<view v-if="isLoading && categoryList.length === 0" class="loading-indicator">
<text class="loading-icon"></text>
<text class="loading-text">加载中...</text>
</view>
<!-- 排名列表 -->
<template v-else-if="rankingList.length > 0">
<!-- 大类列表 -->
<template v-else-if="categoryList.length > 0">
<view
v-for="(teacher, index) in rankingList"
:key="teacher.jsId"
class="teacher-card"
v-for="(category, index) in categoryList"
:key="category.topTypeId || index"
class="category-card"
>
<view class="card-main">
<!-- 排名标识 -->
<view class="rank-badge" :class="getRankClass(index)">
<text class="rank-number">{{ index + 1 }}</text>
</view>
<!-- 教师姓名 -->
<text class="teacher-name">{{ teacher.jsName || '未知' }}</text>
<!-- 积分徽章 -->
<view class="score-badge">
<text class="score-value">{{ teacher.totalScore || 0 }}</text>
<view class="card-header">
<view class="category-name">{{ category.topTypeName || '未分类' }}</view>
<view class="category-score">
<text class="score-value">{{ category.approvedCount || 0 }}</text>
<text class="score-unit"></text>
</view>
</view>
<view class="teacher-info">
<view class="record-info" @click="goToDetail(teacher.jsId)">
<text class="record-text">记录 {{ teacher.recordCount || 0 }}</text>
<text class="record-arrow">></text>
<view class="card-body">
<view class="stats-grid">
<view class="stat-cell clickable" @click="goToDetail(category, 'approvedCount')">
<text class="stat-label">已审批积分</text>
<text class="stat-value approved">{{ category.approvedCount || 0 }}</text>
</view>
<view class="stat-cell">
<text class="stat-label">公式计算</text>
<text class="stat-value calculated">{{ category.calculatedScore || 0 }}</text>
</view>
<view class="stat-cell clickable" @click="goToDetail(category, 'pendingCount')">
<text class="stat-label">待审批</text>
<text class="stat-value pending">{{ category.pendingCount || 0 }}</text>
</view>
<view class="stat-cell clickable" @click="goToDetail(category, 'rejectedCount')">
<text class="stat-label">已驳回</text>
<text class="stat-value rejected">{{ category.rejectedCount || 0 }}</text>
</view>
<view class="stat-cell clickable" @click="goToDetail(category, 'draftCount')">
<text class="stat-label">暂存</text>
<text class="stat-value draft">{{ category.draftCount || 0 }}</text>
</view>
</view>
</view>
</view>
@ -120,14 +128,8 @@
<!-- 空状态 -->
<view v-else class="empty-state">
<text class="empty-icon">📊</text>
<text class="empty-text">暂无排名数据</text>
<text class="empty-hint">请选择时间范围进行查询</text>
</view>
<!-- 加载更多 -->
<view v-if="isLoading && rankingList.length > 0" class="loading-more">
<text class="loading-more-icon"></text>
<text class="loading-more-text">加载中...</text>
<text class="empty-text">暂无数据</text>
<text class="empty-hint">该教师在此时间范围内没有积分记录</text>
</view>
</scroll-view>
</view>
@ -136,23 +138,20 @@
<script lang="ts" setup>
import { ref, computed, onMounted } from "vue";
import { onShow, onLoad } from "@dcloudio/uni-app";
import { getPerformanceRankingApi } from "@/api/base/jfApi";
import { onLoad } from "@dcloudio/uni-app";
import { jfStatisticsByTeacherApi } from "@/api/base/jfApi";
import { navigateTo } from "@/utils/uniapp";
import dayjs from "dayjs";
//
interface TeacherRanking {
jsId: string; // ID
jsName: string; //
totalScore: number; //
recordCount: number; //
completedCount: number; //
pendingCount: number; //
rejectedCount: number; //
interface CategoryStatistics {
topTypeId: string;
topTypeName: string;
approvedCount: number;
calculatedScore: number;
pendingCount: number;
rejectedCount: number;
draftCount: number;
}
//
const getCurrentYearRange = () => {
const year = new Date().getFullYear();
return {
@ -161,52 +160,45 @@ const getCurrentYearRange = () => {
};
};
//
const yearRange = getCurrentYearRange();
const startTime = ref(yearRange.start);
const endTime = ref(yearRange.end);
const njId = ref<string>("");
const njmc = ref<string>("");
const rankingList = ref<TeacherRanking[]>([]);
const jsId = ref<string>("");
const jsName = ref<string>("");
const categoryList = ref<CategoryStatistics[]>([]);
const isLoading = ref(false);
// -
const teacherCount = computed(() => rankingList.value.length);
const totalScore = computed(() => {
return rankingList.value.reduce((sum, teacher) => sum + (Number(teacher.totalScore) || 0), 0);
const totalApprovedScore = computed(() => {
return categoryList.value.reduce((sum, c) => sum + (Number(c.approvedCount) || 0), 0);
});
const totalRecords = computed(() => {
return rankingList.value.reduce((sum, teacher) => sum + (Number(teacher.recordCount) || 0), 0);
const totalCalculatedScore = computed(() => {
return categoryList.value.reduce((sum, c) => sum + (Number(c.calculatedScore) || 0), 0);
});
const completedCount = computed(() => {
return rankingList.value.reduce((sum, teacher) => sum + (Number(teacher.completedCount) || 0), 0);
const totalPending = computed(() => {
return categoryList.value.reduce((sum, c) => sum + (Number(c.pendingCount) || 0), 0);
});
const pendingCount = computed(() => {
return rankingList.value.reduce((sum, teacher) => sum + (Number(teacher.pendingCount) || 0), 0);
const totalRejected = computed(() => {
return categoryList.value.reduce((sum, c) => sum + (Number(c.rejectedCount) || 0), 0);
});
const totalDraft = computed(() => {
return categoryList.value.reduce((sum, c) => sum + (Number(c.draftCount) || 0), 0);
});
//
const handleStartTimeChange = (e: any) => {
startTime.value = e.detail.value;
console.log('选择的开始时间:', startTime.value);
};
//
const handleEndTimeChange = (e: any) => {
endTime.value = e.detail.value;
console.log('选择的结束时间:', endTime.value);
};
//
const loadPerformanceRanking = async () => {
if (isLoading.value) return;
const loadStatistics = async () => {
if (isLoading.value || !jsId.value) return;
//
if (startTime.value && endTime.value && startTime.value > endTime.value) {
uni.showToast({
title: '开始时间不能大于结束时间',
@ -217,113 +209,116 @@ const loadPerformanceRanking = async () => {
isLoading.value = true;
try {
console.log('加载业绩排名,时间范围:', startTime.value, '至', endTime.value);
const params: any = {};
const params: any = {
jsId: jsId.value
};
if (startTime.value) {
params.startTime = startTime.value;
}
if (endTime.value) {
params.endTime = endTime.value;
}
if (njId.value) {
params.njId = njId.value;
}
const response = await getPerformanceRankingApi(params);
console.log('业绩排名API响应:', response);
const response = await jfStatisticsByTeacherApi(params);
console.log('教师大类统计API响应:', response);
if (response && response.resultCode === 1) {
rankingList.value = response.result || [];
console.log('业绩排名数据:', rankingList.value);
const rawData = response.result || [];
categoryList.value = rawData.map((item: any) => ({
topTypeId: item.topTypeId,
topTypeName: item.topTypeName || '未分类',
approvedCount: Number(item.approvedCount || 0),
calculatedScore: Number(item.calculatedScore || 0),
pendingCount: Number(item.pendingCount || 0),
rejectedCount: Number(item.rejectedCount || 0),
draftCount: Number(item.draftCount || 0)
}));
if (rankingList.value.length === 0) {
if (categoryList.value.length === 0) {
uni.showToast({
title: '暂无数据',
icon: 'none'
});
}
} else {
rankingList.value = [];
categoryList.value = [];
uni.showToast({
title: response?.message || '查询失败',
icon: 'none'
});
}
} catch (error) {
console.error('加载业绩排名失败:', error);
console.error('加载教师大类统计失败:', error);
uni.showToast({
title: '加载失败',
icon: 'error'
});
rankingList.value = [];
categoryList.value = [];
} finally {
isLoading.value = false;
}
};
//
const getRankClass = (index: number) => {
if (index === 0) return 'rank-gold';
if (index === 1) return 'rank-silver';
if (index === 2) return 'rank-bronze';
return 'rank-normal';
};
//
const loadMore = () => {
//
};
//
const goToDetail = (jsId: string) => {
if (!jsId) {
uni.showToast({
title: '缺少教师ID',
icon: 'none'
});
return;
}
navigateTo(`/pages/view/routine/JiFenPingJia/jftj/detail?jsId=${jsId}`);
};
//
onLoad((options) => {
if (options?.njId) {
njId.value = options.njId;
if (options?.jsId) {
jsId.value = options.jsId;
}
if (options?.njmc) {
njmc.value = decodeURIComponent(options.njmc);
if (options?.jsName) {
jsName.value = decodeURIComponent(options.jsName);
}
if (options?.startTime) {
startTime.value = options.startTime;
startTime.value = decodeURIComponent(options.startTime);
}
if (options?.endTime) {
endTime.value = options.endTime;
endTime.value = decodeURIComponent(options.endTime);
}
// categories 使
if (options?.categories) {
try {
const categories = JSON.parse(decodeURIComponent(options.categories));
if (Array.isArray(categories) && categories.length > 0) {
categoryList.value = categories;
return;
}
} catch (e) {
console.error('解析 categories 失败:', e);
}
}
});
onMounted(() => {
console.log('业绩排名页面加载');
//
loadPerformanceRanking();
//
if (categoryList.value.length === 0 && jsId.value) {
loadStatistics();
}
});
//
onShow(() => {
console.log('页面显示');
});
const goToDetail = (category: CategoryStatistics, columnType: string) => {
const params = new URLSearchParams();
params.append('jsId', jsId.value);
params.append('topTypeId', category.topTypeId || '');
params.append('topTypeName', encodeURIComponent(category.topTypeName || ''));
params.append('columnType', columnType);
if (startTime.value) {
params.append('startTime', startTime.value);
}
if (endTime.value) {
params.append('endTime', endTime.value);
}
navigateTo(`/pages/view/routine/JiFenPingJia/jftj/detail?${params.toString()}`);
};
</script>
<style lang="scss" scoped>
.performance-ranking-page {
.performance-detail-page {
display: flex;
flex-direction: column;
height: 100vh;
background: linear-gradient(180deg, #f0f5ff 0%, #f5f7fa 100%);
}
//
.page-banner {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 20px 16px;
@ -391,7 +386,6 @@ onShow(() => {
}
}
//
.filter-bar {
display: flex;
flex-direction: column;
@ -421,52 +415,45 @@ onShow(() => {
.filter-right {
width: 100%;
.date-picker-wrapper {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
.date-picker-wrapper {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
.date-picker {
flex: 1;
min-width: 0;
.date-picker {
flex: 1;
min-width: 0;
.picker-view {
display: flex;
flex-direction: column;
padding: 10px 12px;
background-color: #f5f7ff;
border-radius: 8px;
border: 1px solid #d4dbff;
.picker-view {
display: flex;
flex-direction: column;
padding: 10px 12px;
background-color: #f5f7ff;
border-radius: 8px;
border: 1px solid #d4dbff;
.picker-label {
font-size: 11px;
color: #adb5bd;
margin-bottom: 4px;
}
.picker-value {
font-size: 14px;
color: #212529;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.picker-value {
font-size: 14px;
color: #212529;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
.date-separator {
font-size: 14px;
color: #666;
padding: 0 4px;
flex-shrink: 0;
}
}
.date-separator {
font-size: 14px;
color: #666;
padding: 0 4px;
flex-shrink: 0;
}
}
}
}
//
.query-btn-inline {
flex-shrink: 0;
height: 36px;
@ -487,7 +474,6 @@ onShow(() => {
}
}
//
.summary-section {
display: flex;
justify-content: space-around;
@ -504,19 +490,30 @@ onShow(() => {
gap: 6px;
.summary-number {
font-size: 22px;
font-size: 20px;
font-weight: bold;
color: #667eea;
&.approved {
color: #52c41a;
}
&.calculated {
color: #1890ff;
}
&.pending {
color: #faad14;
}
&.rejected {
color: #ff4d4f;
}
}
.summary-label {
font-size: 12px;
font-size: 11px;
color: #666;
}
}
//
.teacher-list {
.category-list {
flex: 1;
overflow: hidden;
display: flex;
@ -530,140 +527,145 @@ onShow(() => {
}
}
//
.teacher-card {
background: linear-gradient(135deg, #ffffff 0%, #f5f7ff 100%);
border-radius: 16px;
margin-bottom: 16px;
box-shadow:
0 2px 16px rgba(0, 0, 0, 0.08),
0 0 0 1px rgba(102, 126, 234, 0.1);
position: relative;
.category-card {
background: #ffffff;
border-radius: 12px;
margin-bottom: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
overflow: hidden;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
&:active {
transform: translateY(2px) scale(0.98);
box-shadow:
0 1px 8px rgba(0, 0, 0, 0.12),
0 0 0 1px rgba(102, 126, 234, 0.15);
}
//
&::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 5px;
background: linear-gradient(180deg, #667eea 0%, #764ba2 100%);
border-radius: 16px 0 0 16px;
}
.card-main {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
padding-left: 20px;
border-bottom: 1px solid #f0f0f0;
}
.rank-badge {
width: 36px;
height: 36px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
flex-shrink: 0;
border: 2px solid #ffffff;
.rank-number {
font-size: 14px;
font-weight: bold;
color: #ffffff;
}
&.rank-gold {
background: linear-gradient(135deg, #ffd700 0%, #ffb300 100%);
}
&.rank-silver {
background: linear-gradient(135deg, #c0c0c0 0%, #a8a8a8 100%);
}
&.rank-bronze {
background: linear-gradient(135deg, #cd7f32 0%, #b87333 100%);
}
&.rank-normal {
background: linear-gradient(135deg, #78909c 0%, #607d8b 100%);
}
}
.teacher-name {
font-size: 18px;
font-weight: 700;
color: #1a202c;
flex: 1;
}
.score-badge {
display: flex;
align-items: baseline;
gap: 2px;
&.total-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 8px 14px;
border-radius: 20px;
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
flex-shrink: 0;
.score-value {
font-size: 20px;
font-weight: bold;
color: #ffffff;
.card-header {
border-bottom-color: rgba(255, 255, 255, 0.2);
.category-name {
color: #ffffff;
&.total {
font-size: 18px;
}
}
.category-score {
background: rgba(255, 255, 255, 0.2);
.score-value, .score-unit {
color: #ffffff;
}
}
}
.score-unit {
font-size: 12px;
color: rgba(255, 255, 255, 0.9);
.card-body {
.stat-label {
color: rgba(255, 255, 255, 0.8) !important;
}
.stat-value {
color: #ffffff !important;
}
}
}
.teacher-info {
padding: 12px 16px 16px 16px;
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 14px 16px;
border-bottom: 1px solid #f0f0f0;
.record-info {
.category-name {
font-size: 16px;
font-weight: 600;
color: #1a202c;
flex: 1;
}
.category-score {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 4px;
cursor: pointer;
user-select: none;
align-items: baseline;
gap: 2px;
background: linear-gradient(135deg, #52c41a 0%, #73d13d 100%);
padding: 6px 12px;
border-radius: 16px;
&:active {
opacity: 0.7;
}
.record-text {
font-size: 14px;
color: #667eea;
font-weight: 500;
}
.record-arrow {
.score-value {
font-size: 16px;
color: #667eea;
font-weight: 600;
font-weight: bold;
color: #ffffff;
}
.score-unit {
font-size: 11px;
color: rgba(255, 255, 255, 0.9);
}
}
}
.card-body {
padding: 12px 16px;
.stats-grid {
display: flex;
flex-wrap: wrap;
gap: 8px;
.stat-cell {
flex: 1;
min-width: 60px;
display: flex;
flex-direction: column;
align-items: center;
padding: 8px 4px;
background: #f8f9fa;
border-radius: 8px;
&.clickable {
cursor: pointer;
&:active {
background: #e9ecef;
transform: scale(0.96);
}
.stat-value {
text-decoration: underline;
}
}
.stat-label {
font-size: 10px;
color: #999;
margin-bottom: 4px;
white-space: nowrap;
}
.stat-value {
font-size: 14px;
font-weight: 600;
&.approved {
color: #52c41a;
}
&.calculated {
color: #1890ff;
}
&.pending {
color: #faad14;
}
&.rejected {
color: #ff4d4f;
}
&.draft {
color: #999;
}
}
}
}
}
}
//
.loading-indicator {
display: flex;
flex-direction: column;
@ -683,7 +685,6 @@ onShow(() => {
}
}
//
.empty-state {
display: flex;
flex-direction: column;
@ -710,27 +711,6 @@ onShow(() => {
}
}
//
.loading-more {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 20px;
.loading-more-icon {
font-size: 18px;
animation: pulse 1.5s ease-in-out infinite;
}
.loading-more-text {
color: #667eea;
font-size: 14px;
font-weight: 500;
}
}
//
@keyframes pulse {
0%, 100% {
opacity: 1;
@ -742,4 +722,3 @@ onShow(() => {
}
}
</style>

View File

@ -200,9 +200,8 @@ const setTeachInfo = (index: number, value: string) => {
//
.info-list {
background-color: #ffffff;
border-radius: 16rpx;
overflow: hidden;
margin: 0 30rpx;
margin: 0;
}
.info-item {

View File

@ -0,0 +1,660 @@
<template>
<view class="teacher-detail-page">
<!-- 顶部查询教师类型 + 教师姓名 -->
<view class="top-query">
<view class="query-row">
<view class="query-type" @click="openJsTypePicker">
<text :class="{ placeholder: !queryJsType }">{{ queryJsType || '教师类型' }}</text>
<uni-icons type="right" size="14" color="#666"></uni-icons>
</view>
<view class="query-name">
<input
v-model="queryName"
class="name-input"
placeholder="教师姓名"
@confirm="onSearch"
/>
<u-button text="搜索" type="primary" size="small" class="search-btn" @click="onSearch" />
</view>
</view>
<uni-popup ref="jsTypePopup" type="bottom" @change="onJsTypePopupChange">
<view class="js-type-picker">
<view
v-for="opt in jsTypeOptions"
:key="opt.value"
:class="['type-item', { active: queryJsType === opt.value }]"
@click="selectJsType(opt.value)"
>
{{ opt.label }}
</view>
</view>
</uni-popup>
</view>
<view class="page-header">
<view class="header-content">
<text class="page-title">{{ pageTitle }}</text>
<text class="total-count"> {{ totalCount }} </text>
</view>
</view>
<scroll-view scroll-y class="list-scroll">
<view v-if="loading" class="loading-container">
<text class="loading-text">加载中...</text>
</view>
<view v-else-if="!filteredTeachers.length" class="empty-container">
<text class="empty-text">暂无数据</text>
</view>
<view v-else class="teacher-list">
<view
class="teacher-item"
v-for="(teacher, index) in filteredTeachers"
:key="teacher.id"
@click="handleTeacherClick(teacher)"
>
<view class="item-left">
<view class="teacher-avatar">
<text class="avatar-text">{{ teacher.jsxm?.charAt(0) || '?' }}</text>
</view>
<view class="teacher-info">
<text class="teacher-name">{{ teacher.jsxm }}</text>
<text class="teacher-meta" v-if="teacher.jsdah">档案号{{ teacher.jsdah }}</text>
<!-- 请假信息展示 -->
<view v-if="filterType === 'zdqk' && filterValue === '请假' && teacher.qjlx" class="leave-info">
<text class="leave-item">请假类型{{ teacher.qjlx }}</text>
<text class="leave-item" v-if="teacher.qjkstime">开始时间{{ formatDate(teacher.qjkstime) }}</text>
<text class="leave-item" v-if="teacher.qjjstime">结束时间{{ formatDate(teacher.qjjstime) }}</text>
<text class="leave-item" v-if="teacher.qjsy">请假事由{{ teacher.qjsy }}</text>
</view>
<!-- 调出原因展示 -->
<view v-if="filterType === 'zdqk' && filterValue === '调出' && teacher.dcyy" class="leave-info">
<text class="leave-item">调出原因{{ teacher.dcyy }}</text>
</view>
</view>
</view>
<view class="item-right">
<text class="detail-arrow"></text>
</view>
</view>
</view>
</scroll-view>
<!-- 固定在底部的返回按钮 -->
<view v-if="filteredTeachers.length > 0" class="fixed-bottom">
<button class="back-btn" @click="handleBack">
返回
</button>
</view>
</view>
</template>
<script lang="ts" setup>
import { ref, computed } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import { useCommonStore } from "@/store/modules/common";
import { get } from "@/utils/request";
import { dicFindByPidApi } from "@/api/base/server";
interface Teacher {
id: string;
jsxm: string;
jsdah: string;
jsxbId?: string;
age?: number;
zhxlId?: string;
latestZcdjId?: string;
latestGwjbId?: string;
lxdh?: string;
jsType?: string;
zzmmId?: string;
jsgl?: number;
qjlx?: string; //
qjkstime?: string; //
qjjstime?: string; //
qjsy?: string; //
dcyy?: string; //
}
const commonStore = useCommonStore();
const filterType = ref(""); // type/age/education/title/position/political/workingYears
const filterValue = ref(""); //
const source = ref("");
const xkIdParam = ref("");
const teacherTypeParam = ref("all");
//
const queryJsType = ref(""); // dictionaryValue
const queryName = ref("");
const jsTypePopup = ref<any>(null);
const jsTypeOptions = ref<{ label: string; value: string }[]>([]);
const JS_TYPE_DICT_PID = "1472384467"; // pid zhxy-vue jsAccount
const pageTitle = computed(() => {
if (source.value === "club") {
if (teacherTypeParam.value === "inside") return "校内教师";
if (teacherTypeParam.value === "outside") return "校外教师";
return "课程教师";
}
let title = filterValue.value || "教师档案";
if (effectiveJsTypes.value.length > 0 && effectiveJsTypes.value.length < 2) {
title = `${effectiveJsTypes.value[0]} - ${title}`;
}
return title;
});
const totalCount = computed(() => filteredTeachers.value.length);
const allTeachers = ref<Teacher[]>([]);
const loading = ref(false);
//
const filterByAge = (teacher: Teacher, ageRange: string) => {
const age = teacher.age;
if (!age) return false;
switch (ageRange) {
case '25岁以下': return age < 25;
case '25-30岁': return age >= 25 && age <= 30;
case '31-35岁': return age >= 31 && age <= 35;
case '36-40岁': return age >= 36 && age <= 40;
case '41-45岁': return age >= 41 && age <= 45;
case '46-50岁': return age >= 46 && age <= 50;
case '51-54岁': return age >= 51 && age <= 54;
case '55岁以上': return age >= 55;
default: return false;
}
};
//
const filterByWorkingYears = (teacher: Teacher, yearsRange: string) => {
const years = teacher.jsgl;
if (!years) return false;
switch (yearsRange) {
case '5年以下': return years < 5;
case '6-10年': return years >= 6 && years <= 10;
case '11-15年': return years >= 11 && years <= 15;
case '16-20年': return years >= 16 && years <= 20;
case '21-25年': return years >= 21 && years <= 25;
case '26-30年': return years >= 26 && years <= 30;
case '31-35年': return years >= 31 && years <= 35;
case '36-40年': return years >= 36 && years <= 40;
case '41年以上': return years >= 41;
default: return false;
}
};
const effectiveJsTypes = computed(() => {
return queryJsType.value ? [queryJsType.value] : [];
});
const filteredTeachers = computed(() => {
let result = allTeachers.value;
if (source.value !== 'club' && effectiveJsTypes.value.length > 0) {
result = result.filter((teacher) => {
return effectiveJsTypes.value.includes(teacher.jsType || '');
});
}
if (queryName.value.trim()) {
const kw = queryName.value.trim().toLowerCase();
result = result.filter((t) => (t.jsxm || '').toLowerCase().includes(kw));
}
if (source.value === 'club' || !filterType.value || !filterValue.value) {
return result;
}
return result.filter((teacher) => {
switch (filterType.value) {
case 'type':
return teacher.jsType === filterValue.value;
case 'age':
return filterByAge(teacher, filterValue.value);
case 'education':
return teacher.zhxlId === filterValue.value;
case 'title':
return teacher.latestZcdjId === filterValue.value;
case 'position':
return teacher.latestGwjbId === filterValue.value;
case 'political':
return teacher.zzmmId === filterValue.value;
case 'workingYears':
return filterByWorkingYears(teacher, filterValue.value);
case 'zdqk':
// fetchList
return true;
default:
return true;
}
});
});
const fetchList = async () => {
loading.value = true;
try {
if (source.value === "club" && xkIdParam.value) {
const res = await get("/api/xkkc/statistics/clubTeacherDetail", {
xkId: xkIdParam.value,
teacherType: teacherTypeParam.value,
});
const list = res?.result || [];
allTeachers.value = list.map((item: any) => ({
id: item.id,
jsxm: item.name,
jsdah: item.archiveCode,
jsType: item.teacherType,
lxdh: item.phone,
deptName: item.deptName,
}));
return;
}
//
if (filterType.value === 'zdqk' && filterValue.value) {
const params: any = {
zdqkValue: filterValue.value,
};
if (queryJsType.value) {
params.jsTypes = queryJsType.value;
}
const res = await get("/api/js/statistics/zdqk/list", params);
const list = res?.result || res || [];
allTeachers.value = list.map((item: any) => ({
id: item.id,
jsxm: item.jsxm,
jsdah: item.jsdah,
jsType: item.jsType,
lxdh: item.lxdh,
jsxbId: item.jsxbId,
age: item.age,
zhxlId: item.zhxlId,
latestZcdjId: item.latestZcdjId,
latestGwjbId: item.latestGwjbId,
zzmmId: item.zzmmId,
jsgl: item.jsgl,
qjlx: item.qjlx,
qjkstime: item.qjkstime,
qjjstime: item.qjjstime,
qjsy: item.qjsy,
dcyy: item.dcyy,
}));
return;
}
const res = await commonStore.getAllJs();
if (res?.resultCode === 1 && Array.isArray(res.result)) {
allTeachers.value = res.result;
} else if (Array.isArray(res)) {
allTeachers.value = res;
} else {
allTeachers.value = [];
}
} catch (error) {
console.error("获取教师列表失败", error);
allTeachers.value = [];
uni.showToast({
title: "加载失败",
icon: "none",
});
} finally {
loading.value = false;
}
};
const openJsTypePicker = () => {
jsTypePopup.value?.open();
};
const selectJsType = (value: string) => {
queryJsType.value = value;
jsTypePopup.value?.close();
};
const onJsTypePopupChange = () => {};
//
const loadJsTypeDict = async () => {
try {
const res: any = await dicFindByPidApi({ pid: JS_TYPE_DICT_PID });
const data = Array.isArray(res) ? res : (res?.result || res?.data || res?.rows || []);
const options: { label: string; value: string }[] = [{ label: "全部", value: "" }];
data.forEach((d: any) => {
const val = d.dictionaryValue ?? d.dictionaryCode ?? d.dictValue ?? d.value ?? "";
const label = d.dictionaryValue ?? d.dictLabel ?? d.label ?? d.name ?? val;
if (val) options.push({ label: String(label), value: String(val) });
});
jsTypeOptions.value = options;
if (options.length > 1 && !queryJsType.value) {
queryJsType.value = options[1].value;
}
} catch (e) {
console.error("加载教师类型字典失败:", e);
jsTypeOptions.value = [{ label: "全部", value: "" }];
}
};
const onSearch = () => {
// computed
uni.hideKeyboard();
};
//
const handleBack = () => {
uni.navigateBack();
};
//
const handleTeacherClick = (teacher: Teacher) => {
uni.navigateTo({
url: `/pages/statistics/teacher/jsdtal?id=${teacher.id}`,
});
};
//
const parseGender = (jsxbId?: string) => {
if (!jsxbId) return '-';
if (jsxbId === '1' || jsxbId === '男') return '男';
if (jsxbId === '2' || jsxbId === '女') return '女';
return jsxbId;
};
//
const formatDate = (dateStr?: string) => {
if (!dateStr) return '-';
try {
const date = new Date(dateStr);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
} catch (e) {
return dateStr;
}
};
onLoad(async (options: any) => {
if (options.filterType) {
filterType.value = decodeURIComponent(options.filterType);
}
if (options.filterValue) {
filterValue.value = decodeURIComponent(options.filterValue);
}
if (options.source) {
source.value = options.source;
}
if (options.xkId) {
xkIdParam.value = options.xkId;
}
if (options.teacherType) {
teacherTypeParam.value = options.teacherType;
}
if (options.type && !options.filterType) {
filterType.value = "type";
filterValue.value = decodeURIComponent(options.type);
}
await loadJsTypeDict();
fetchList();
});
</script>
<style scoped lang="scss">
.teacher-detail-page {
min-height: 100vh;
background: #f4f6fb;
display: flex;
flex-direction: column;
}
.page-header {
flex-shrink: 0;
background: #ffffff;
padding: 24rpx 30rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.page-title {
font-size: 36rpx;
font-weight: 600;
color: #1f2933;
}
.total-count {
font-size: 26rpx;
color: #6b7280;
}
.top-query {
margin-top: 0;
padding: 24rpx;
background: #fff;
border-bottom: 1rpx solid #eee;
}
.query-row {
display: flex;
gap: 20rpx;
align-items: center;
}
.query-type {
flex-shrink: 0;
width: 200rpx;
padding: 20rpx 24rpx;
background: #f5f5f5;
border-radius: 12rpx;
font-size: 28rpx;
color: #333;
display: flex;
justify-content: space-between;
align-items: center;
&.placeholder { color: #999; }
}
.query-name {
flex: 1;
min-width: 0;
display: flex;
gap: 16rpx;
align-items: center;
.name-input {
flex: 1;
height: 72rpx;
padding: 0 24rpx;
font-size: 28rpx;
background: #f5f5f5;
border-radius: 12rpx;
}
.search-btn { flex-shrink: 0; width: 140rpx !important; }
}
.js-type-picker {
padding: 32rpx;
background: #fff;
border-radius: 24rpx 24rpx 0 0;
}
.type-item {
padding: 28rpx;
font-size: 30rpx;
color: #333;
border-bottom: 1rpx solid #eee;
&:last-child { border-bottom: none; }
&.active { color: #3b82f6; font-weight: 600; }
}
.list-scroll {
flex: 1;
margin-top: 0;
padding: 24rpx 0 160rpx 0;
background-color: #f4f6fb;
}
.loading-container,
.empty-container {
min-height: 400rpx;
display: flex;
align-items: center;
justify-content: center;
margin: 0 30rpx;
}
.loading-text,
.empty-text {
font-size: 28rpx;
color: #9ca3af;
}
.teacher-list {
display: flex;
flex-direction: column;
gap: 0; // gap使margin
padding: 0 30rpx; // padding
}
.teacher-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx;
margin-bottom: 24rpx;
background: linear-gradient(135deg, #ffffff 0%, #f7f9fc 100%);
border-radius: 20rpx;
border: 1px solid rgba(59, 130, 246, 0.15);
box-shadow: 0 2rpx 8rpx rgba(59, 130, 246, 0.08);
transition: all 0.3s ease;
cursor: pointer;
&:last-child {
margin-bottom: 0;
}
&:hover {
transform: translateY(-2rpx);
box-shadow: 0 4rpx 16rpx rgba(59, 130, 246, 0.15);
border-color: rgba(59, 130, 246, 0.3);
}
&:active {
transform: translateY(0);
box-shadow: 0 2rpx 12rpx rgba(59, 130, 246, 0.12);
}
}
.item-left {
display: flex;
align-items: center;
gap: 24rpx;
flex: 1;
min-width: 0;
}
.teacher-avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
box-shadow: 0 4rpx 12rpx rgba(102, 126, 234, 0.3);
}
.avatar-text {
color: #ffffff;
font-size: 32rpx;
font-weight: 600;
}
.teacher-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 8rpx;
min-width: 0;
}
.teacher-name {
font-size: 32rpx;
font-weight: 600;
color: #1f2933;
line-height: 1.3;
}
.teacher-meta {
font-size: 24rpx;
color: #6b7280;
line-height: 1.2;
}
.leave-info {
margin-top: 12rpx;
padding: 16rpx;
background: #fef3c7;
border-radius: 8rpx;
border-left: 4rpx solid #f59e0b;
display: flex;
flex-direction: column;
gap: 8rpx;
}
.leave-item {
font-size: 24rpx;
color: #92400e;
line-height: 1.4;
}
.item-right {
flex-shrink: 0;
padding-left: 16rpx;
}
.detail-arrow {
font-size: 48rpx;
color: #9ca3af;
font-weight: 300;
line-height: 1;
}
//
.fixed-bottom {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #ffffff;
padding: 24rpx;
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.08);
z-index: 100;
}
.back-btn {
width: 100%;
height: 88rpx;
line-height: 88rpx;
background: #3b82f6;
color: #ffffff;
border: none;
border-radius: 16rpx;
font-size: 32rpx;
font-weight: 600;
box-shadow: 0 4rpx 16rpx rgba(59, 130, 246, 0.3);
transition: all 0.3s;
&:hover {
background: #2563eb;
box-shadow: 0 6rpx 20rpx rgba(59, 130, 246, 0.4);
}
&:active {
transform: scale(0.98);
background: #1d4ed8;
}
}
</style>

View File

@ -0,0 +1,312 @@
<template>
<view class="parent-archive-page">
<!-- 顶部区域可选范围 + 按家长姓名搜索 -->
<view class="top-section">
<view class="selector-picker" @click="openStudentPicker">
<text :class="{ placeholder: !pickerDisplayText }">{{ pickerDisplayText || '可选年级、班级或学生筛选' }}</text>
<view class="selector-actions">
<view v-if="queryScope" class="clear-btn" @click.stop="clearQueryScope">×</view>
<uni-icons type="right" size="16" color="#666"></uni-icons>
</view>
</view>
<view class="search-section">
<view class="search-box">
<BasicSearch
placeholder="按家长姓名搜索"
v-model="searchKeyword"
@search="onSearch"
/>
<u-button text="搜索" type="primary" size="small" class="search-btn" @click="onSearch" />
</view>
</view>
</view>
<!-- 家长档案列表z-paging 滚动加载 -->
<view class="list-section">
<z-paging
ref="pagingRef"
v-model="parentList"
@query="queryData"
:auto="true"
:refresher-enabled="true"
:loading-more-enabled="true"
:default-page-size="20"
:show-loading-more-no-more-view="true"
:fixed="false"
class="paging-container"
>
<template #top>
<view class="list-header">家长档案 ( {{ totalCount }} )</view>
</template>
<view
v-for="parent in parentList"
:key="parent.jzId"
class="parent-item"
@click="viewParentDetail(parent)"
>
<view class="parent-row">
<text class="label">家长姓名</text>
<text class="value">{{ parent.jzxm || '-' }}</text>
</view>
<view class="parent-row">
<text class="label">联系方式</text>
<text class="value link" @click.stop="contactParent(parent)">{{ parent.jzsj || '-' }}</text>
</view>
<view class="parent-row">
<text class="label">单位</text>
<text class="value">{{ parent.gzdw || '-' }}</text>
</view>
<view class="parent-row">
<text class="label">与学生关系</text>
<text class="value">{{ parent.jzxsgxId || '-' }}</text>
</view>
<view class="parent-row">
<text class="label">学生姓名</text>
<text class="value">{{ parent.xsxm || '-' }}</text>
</view>
<view class="parent-row">
<text class="label">所在年级班级</text>
<text class="value">{{ (parent.njmc || '') + (parent.bjmc || '') || '-' }}</text>
</view>
</view>
</z-paging>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import BasicSearch from '@/components/BasicSearch/Search.vue'
import { onLoad } from '@dcloudio/uni-app'
import { findParentArchivePageApi } from '@/api/routine/jz'
interface QueryScope {
njIds?: string
bjIds?: string
xsIds?: string
displayText: string
}
interface ParentInfo {
jzId: string
jzxm: string
jzsj?: string
gzdw?: string
jzxsgxId?: string
njmc?: string
bjmc?: string
xsIds?: string
xsxm?: string
}
const queryScope = ref<QueryScope | null>(null)
const pickerDisplayText = computed(() => queryScope.value?.displayText || '')
const parentList = ref<ParentInfo[]>([])
const searchKeyword = ref('')
const pagingRef = ref<any>(null)
const totalCount = ref(0)
// njIdsbjIdsxsIds
const openStudentPicker = () => {
uni.$once('studentPickerConfirm', (students: any[]) => {
if (students && students.length > 0) {
const njIdSet = [...new Set(students.map((s: any) => s.njId).filter(Boolean))]
const bjIdSet = [...new Set(students.map((s: any) => s.bjId).filter(Boolean))]
const xsIdList = students.map((s: any) => s.xsId || s.id).filter(Boolean)
let displayText: string
if (bjIdSet.length === 1 && njIdSet.length === 1) {
displayText = students[0]?.bc || [students[0]?.njmc, students[0]?.bjmc].filter(Boolean).join('') || '已选班级'
} else if (njIdSet.length === 1) {
displayText = bjIdSet.length > 1 ? `已选 ${bjIdSet.length} 个班级` : (students[0]?.njmc || '已选年级')
} else if (students.length <= 5) {
displayText = students.map((s: any) => s.xsxm || s.xm).join('、')
} else {
displayText = `已选 ${students.length}`
}
queryScope.value = {
njIds: njIdSet.length > 0 ? njIdSet.join(',') : undefined,
bjIds: bjIdSet.length > 0 ? bjIdSet.join(',') : undefined,
xsIds: xsIdList.length > 0 ? xsIdList.join(',') : undefined,
displayText
}
searchKeyword.value = ''
pagingRef.value?.reload()
}
})
uni.navigateTo({
url: '/pages/components/StudentPicker/index?showGrade=true&showClass=true&showStudent=true&multiple=true'
})
}
const clearQueryScope = () => {
queryScope.value = null
pagingRef.value?.reload()
}
const onSearch = (_keyword?: string) => {
pagingRef.value?.reload()
}
const queryData = async (pageNo: number, pageSize: number) => {
const scope = queryScope.value
const njIds = scope?.njIds
const bjIds = scope?.bjIds
const xsIds = scope?.xsIds
const jzxm = searchKeyword.value?.trim() || undefined
if (!njIds && !bjIds && !xsIds && !jzxm) {
pagingRef.value?.complete([])
return
}
try {
const res = await findParentArchivePageApi({
njIds,
bjIds,
xsIds,
jzxm,
page: pageNo,
rows: pageSize
})
const result = (res as any)?.data || res
let rows: ParentInfo[] = []
if (result?.rows && Array.isArray(result.rows)) {
rows = result.rows
totalCount.value = result.records ?? 0
}
pagingRef.value?.complete(rows)
} catch (error) {
console.error('查询家长档案失败:', error)
uni.showToast({ title: '查询失败', icon: 'none' })
pagingRef.value?.complete([])
}
}
const viewParentDetail = (parent: ParentInfo) => {
const info = [
parent.jzxm ? `家长:${parent.jzxm}` : '',
parent.jzsj ? `电话:${parent.jzsj}` : '',
parent.gzdw ? `单位:${parent.gzdw}` : '',
parent.jzxsgxId ? `关系:${parent.jzxsgxId}` : '',
parent.xsxm ? `子女:${parent.xsxm}` : '',
(parent.njmc || parent.bjmc) ? `班级:${parent.njmc || ''}${parent.bjmc || ''}` : ''
]
.filter(Boolean)
.join('\n')
if (info) {
uni.showModal({
title: '家长档案',
content: info,
showCancel: true,
cancelText: '关闭',
confirmText: '拨打电话',
success: (res) => {
if (res.confirm && parent.jzsj) uni.makePhoneCall({ phoneNumber: parent.jzsj })
}
})
}
}
const contactParent = (parent: ParentInfo) => {
if (parent.jzsj) {
uni.makePhoneCall({ phoneNumber: parent.jzsj })
} else {
uni.showToast({ title: '暂无联系电话', icon: 'none' })
}
}
onLoad(() => {})
</script>
<style lang="scss" scoped>
.parent-archive-page {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f5f5;
}
.top-section {
flex-shrink: 0;
background-color: #fff;
padding: 24rpx;
border-bottom: 1rpx solid #eee;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
}
.search-section {
margin-top: 20rpx;
}
.search-section .search-box {
display: flex;
gap: 20rpx;
align-items: center;
& > *:first-child { flex: 1; min-width: 0; }
.search-btn { flex-shrink: 0; width: 140rpx !important; }
}
.selector-picker {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx 20rpx;
background-color: #fff;
border: 1rpx solid #eee;
border-radius: 12rpx;
font-size: 28rpx;
color: #333;
&.placeholder { color: #999; }
.selector-actions {
display: flex;
align-items: center;
gap: 16rpx;
}
.clear-btn {
font-size: 36rpx;
color: #999;
width: 48rpx;
height: 48rpx;
display: flex;
align-items: center;
justify-content: center;
background: #f5f5f5;
border-radius: 50%;
}
}
/* 列表区域必须明确高度z-paging 的 scroll-view 才能正常滚动 */
.list-section {
flex: 1;
min-height: 0;
overflow: hidden;
background-color: #f5f5f5;
}
.list-header {
font-size: 30rpx;
font-weight: bold;
color: #333;
padding: 20rpx 0;
}
.paging-container {
height: 100%;
padding: 20rpx;
box-sizing: border-box;
}
.parent-item {
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08);
}
.parent-row {
font-size: 28rpx;
line-height: 48rpx;
color: #333;
.label { color: #999; margin-right: 8rpx; }
.value.link { color: #007aff; }
}
</style>

View File

@ -0,0 +1,450 @@
<template>
<view class="student-archive-page">
<!-- 顶部区域可选年级班级学生 + 按学生姓名搜索 jzda 一致 -->
<view class="top-section">
<view class="selector-picker" @click="openStudentPicker">
<text :class="{ placeholder: !pickerDisplayText }">{{ pickerDisplayText || '可选年级、班级或学生筛选' }}</text>
<view class="selector-actions">
<view v-if="queryScope" class="clear-btn" @click.stop="clearQueryScope">×</view>
<uni-icons type="right" size="16" color="#666"></uni-icons>
</view>
</view>
<view class="search-section">
<view class="search-box">
<BasicSearch
placeholder="按学生姓名搜索"
v-model="searchKeyword"
@search="onSearch"
/>
<u-button text="搜索" type="primary" size="small" class="search-btn" @click="onSearch" />
</view>
</view>
</view>
<!-- 学生档案统计 -->
<view class="section" v-if="queryScope || searchKeyword.trim()">
<view class="section-title">学生档案统计</view>
<view class="stats-container">
<view
class="stat-item"
:class="{ active: selectedStatType === 'all' }"
@click="onStatItemClick('all')"
>
<text class="stat-number">{{ studentList.length }}</text>
<text class="stat-label">总人数</text>
</view>
<view
class="stat-item"
:class="{ active: selectedStatType === 'followed' }"
@click="onStatItemClick('followed')"
>
<text class="stat-number followed">{{ followedCount }}</text>
<text class="stat-label">已关注</text>
</view>
<view
class="stat-item"
:class="{ active: selectedStatType === 'unfollowed' }"
@click="onStatItemClick('unfollowed')"
>
<text class="stat-number unfollowed">{{ unfollowedCount }}</text>
<text class="stat-label">未关注</text>
</view>
</view>
</view>
<!-- 学生档案列表 -->
<view class="section" v-if="queryScope || searchKeyword.trim()">
<view class="section-title">{{ getListTitle() }} ({{ filteredStudentList.length }})</view>
<view v-if="loading" class="loading-text">加载中...</view>
<view v-else-if="!filteredStudentList.length" class="empty-text">暂无学生档案数据</view>
<view v-else class="student-list">
<view class="student-grid">
<view
v-for="student in filteredStudentList"
:key="student.xsId"
class="student-item bg-white r-md p-12"
@click="viewStudentDetail(student)"
>
<view class="flex-row items-center">
<view class="avatar-container mr-8">
<image
class="student-avatar"
:src="imagUrl(student.xstx || '') || '/static/images/default-avatar.png'"
mode="aspectFill"
/>
</view>
<view class="flex-1 overflow-hidden">
<view class="student-name mb-8">
<text class="font-14 cor-333">{{ student.xsxm }}</text>
</view>
<view class="flex-row">
<view
class="status-tag"
:class="getFollowStatusClass(student.jzxm)"
>
{{ student.jzxm ? '已关注' : '未关注' }}
</view>
</view>
</view>
<view class="arrow-icon-container" @click.stop="viewStudentDetail(student)">
<image class="arrow-icon" src="/static/base/view/more.png" mode="aspectFit" />
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 空状态未选择未搜索 -->
<view class="empty-state" v-if="!queryScope && !searchKeyword.trim()">
<view class="empty-icon">📚</view>
<view class="empty-text">请输入学生姓名搜索或选择年级班级学生</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import BasicSearch from '@/components/BasicSearch/Search.vue'
import { onLoad } from '@dcloudio/uni-app'
import { findStudentArchiveApi } from '@/api/analysis/xs'
import { imagUrl } from '@/utils'
import { useDataStore } from '@/store/modules/data'
interface QueryScope {
njIds?: string
bjIds?: string
xsIds?: string
displayText: string
}
interface StudentInfo {
xsId: string
xsxm: string
xstx?: string
njId: string
njmcId: string
njmc: string
bc: string
bjId: string
bjmc: string
jzIds?: string
jzxm?: string
xb?: string
sfzh?: string
cstime?: string
}
const queryScope = ref<QueryScope | null>(null)
const pickerDisplayText = computed(() => queryScope.value?.displayText || '')
const studentList = ref<StudentInfo[]>([])
const searchKeyword = ref('')
const loading = ref(false)
const selectedStatType = ref<string>('all')
const { setXs } = useDataStore()
const followedCount = computed(() => studentList.value.filter((s) => s.jzxm).length)
const unfollowedCount = computed(() => studentList.value.filter((s) => !s.jzxm).length)
const filteredStudentList = computed(() => {
let list = studentList.value
if (selectedStatType.value === 'followed') list = list.filter((s) => s.jzxm)
else if (selectedStatType.value === 'unfollowed') list = list.filter((s) => !s.jzxm)
return list
})
// njIdsbjIdsxsIds
const openStudentPicker = () => {
uni.$once('studentPickerConfirm', (students: any[]) => {
if (students && students.length > 0) {
const njIdSet = [...new Set(students.map((s: any) => s.njId).filter(Boolean))]
const bjIdSet = [...new Set(students.map((s: any) => s.bjId).filter(Boolean))]
const xsIdList = students.map((s: any) => s.xsId || s.id).filter(Boolean)
let displayText: string
if (bjIdSet.length === 1 && njIdSet.length === 1) {
displayText = students[0]?.bc || [students[0]?.njmc, students[0]?.bjmc].filter(Boolean).join('') || '已选班级'
} else if (njIdSet.length === 1) {
displayText = bjIdSet.length > 1 ? `已选 ${bjIdSet.length} 个班级` : (students[0]?.njmc || '已选年级')
} else if (students.length <= 5) {
displayText = students.map((s: any) => s.xsxm || s.xm).join('、')
} else {
displayText = `已选 ${students.length}`
}
queryScope.value = {
njIds: njIdSet.length > 0 ? njIdSet.join(',') : undefined,
bjIds: bjIdSet.length > 0 ? bjIdSet.join(',') : undefined,
xsIds: xsIdList.length > 0 ? xsIdList.join(',') : undefined,
displayText
}
searchKeyword.value = ''
loadStudents()
}
})
uni.navigateTo({
url: '/pages/components/StudentPicker/index?showGrade=true&showClass=true&showStudent=true&multiple=true'
})
}
const clearQueryScope = () => {
queryScope.value = null
studentList.value = []
}
const onSearch = () => {
loadStudents()
}
const loadStudents = async () => {
const scope = queryScope.value
const njIds = scope?.njIds
const bjIds = scope?.bjIds
const xsIds = scope?.xsIds
const xsxm = searchKeyword.value?.trim() || undefined
if (!njIds && !bjIds && !xsIds && !xsxm) {
studentList.value = []
return
}
loading.value = true
try {
const res: any = await findStudentArchiveApi({ njIds, bjIds, xsIds, xsxm })
const list = res?.result || res?.data || []
studentList.value = Array.isArray(list) ? list : []
} catch (error) {
console.error('查询学生档案失败:', error)
uni.showToast({ title: '查询失败', icon: 'none' })
studentList.value = []
} finally {
loading.value = false
}
}
const onStatItemClick = (statType: string) => {
selectedStatType.value = statType
}
const getListTitle = () => {
switch (selectedStatType.value) {
case 'followed': return '已关注学生'
case 'unfollowed': return '未关注学生'
default: return '学生档案列表'
}
}
const getFollowStatusClass = (jzxm?: string) => (jzxm ? 'status-followed' : 'status-unfollowed')
const viewStudentDetail = (student: StudentInfo) => {
setXs({
xsId: student.xsId,
id: student.xsId,
xsxm: student.xsxm,
xm: student.xsxm,
xstx: student.xstx,
avatar: student.xstx,
njId: student.njId,
njmc: student.njmc,
njmcName: student.njmc,
bjId: student.bjId,
bjmc: student.bjmc,
jzIds: student.jzIds,
jzxm: student.jzxm,
xb: student.xb,
sfzh: student.sfzh,
cstime: student.cstime
})
uni.navigateTo({ url: '/pages/view/homeSchool/parentAddressBook/detail' })
}
onLoad(() => {})
</script>
<style lang="scss" scoped>
.student-archive-page {
min-height: 100vh;
background-color: #f5f5f5;
padding: 20rpx;
}
.top-section {
background-color: #fff;
padding: 24rpx;
border-radius: 16rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
}
.search-section {
margin-top: 20rpx;
}
.search-section .search-box {
display: flex;
gap: 20rpx;
align-items: center;
& > *:first-child { flex: 1; min-width: 0; }
.search-btn { flex-shrink: 0; width: 140rpx !important; }
}
.selector-picker {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx 20rpx;
background-color: #f5f5f5;
border-radius: 12rpx;
font-size: 28rpx;
color: #333;
&.placeholder { color: #999; }
.selector-actions {
display: flex;
align-items: center;
gap: 16rpx;
}
.clear-btn {
font-size: 36rpx;
color: #999;
width: 48rpx;
height: 48rpx;
display: flex;
align-items: center;
justify-content: center;
background: #eee;
border-radius: 50%;
}
}
.section {
background-color: #fff;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 20rpx;
}
.stats-container {
display: flex;
justify-content: space-around;
margin-bottom: 20rpx;
padding: 20rpx;
background-color: #f8f9fa;
border-radius: 12rpx;
gap: 60rpx;
}
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
min-width: 120rpx;
padding: 20rpx 10rpx;
border-radius: 12rpx;
&.active {
background-color: #e6f7ff;
border: 2rpx solid #007aff;
}
}
.stat-number {
font-size: 36rpx;
font-weight: bold;
color: #333;
margin-bottom: 8rpx;
&.followed { color: #52c41a; }
&.unfollowed { color: #ff4d4f; }
}
.stat-label {
font-size: 24rpx;
color: #666;
}
.student-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20rpx;
}
.student-item {
border-radius: 16rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.avatar-container {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
flex-shrink: 0;
}
.student-avatar {
width: 72rpx;
height: 72rpx;
border-radius: 50%;
}
.status-tag {
font-size: 20rpx;
padding: 6rpx 16rpx;
border-radius: 8rpx;
}
.status-followed {
background-color: #e6f7ff;
color: #52c41a;
}
.status-unfollowed {
background-color: #fff2f0;
color: #ff4d4f;
}
.flex-row { display: flex; flex-direction: row; }
.flex-1 { flex: 1; }
.items-center { align-items: center; }
.overflow-hidden { overflow: hidden; }
.mr-8 { margin-right: 12rpx; }
.mb-8 { margin-bottom: 16rpx; }
.font-14 { font-size: 28rpx; }
.cor-333 { color: #333; }
.bg-white { background-color: #fff; }
.r-md { border-radius: 16rpx; }
.p-12 { padding: 24rpx; }
.arrow-icon-container {
width: 40rpx;
height: 40rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.arrow-icon {
width: 35rpx;
height: 35rpx;
opacity: 0.6;
}
.loading-text, .empty-text {
font-size: 28rpx;
color: #999;
text-align: center;
padding: 40rpx;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
padding: 100rpx 40rpx;
.empty-icon { font-size: 120rpx; margin-bottom: 20rpx; }
.empty-text { font-size: 32rpx; color: #666; }
}
</style>

View File

@ -19,7 +19,7 @@
<view class="form-item">
<text class="form-label content-label">通知内容 <text class="required">*</text></text>
<BasicEditor
<TinymceOrEditor
v-model="formData.content"
placeholder="请输入通知内容,支持插入图片"
/>
@ -177,7 +177,7 @@
import { ref, reactive, computed } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import BasicTeacherSelect from "@/components/BasicTeacherSelect/index.vue";
import BasicEditor from "@/components/BasicForm/components/BasicEditor.vue";
import TinymceOrEditor from "@/components/Tinymce/TinymceOrEditor.vue";
import { attachmentUpload } from "@/api/system/upload";
import { imagUrl } from "@/utils";
import { tzSaveApi, tzFindByIdApi } from "@/api/base/tzApi";

View File

@ -68,7 +68,7 @@
<view class="teacher-input-row">
<textarea
v-model="xkkc.kcjsms"
placeholder="请输入教师信息(必填)"
placeholder="请输入教师信息,包含姓名、职称(教练级别)等(必填)"
class="form-textarea"
/>
</view>
@ -198,41 +198,30 @@ const [register, { getValue, setValue }] = useForm({
],
});
// 1 ~ 20
const kecOptions = Array.from({ length: 20 }, (_, i) => ({ name: `${i + 1}次课` }));
const schema = reactive<FormsSchema[]>([
{
field: "jhjd",
label: "阶段",
component: "BasicInput",
componentProps: {},
},
{
field: "jhsj",
label: "计划时间",
component: "BasicInput",
label: "课次",
component: "BasicPicker",
componentProps: {
type: "date",
placeholder: "请选择计划日期",
//
style: "position: relative; z-index: 1000;",
range: kecOptions,
rangeKey: "name",
savaKey: "name",
placeholder: "请选择课次",
},
},
{
field: "jhdd",
label: "地址",
component: "BasicInput",
componentProps: {},
},
{
field: "jhnr",
label: "计划内容",
component: "BasicInput",
component: "BasicEditor",
itemProps: {
labelPosition: "top",
},
componentProps: {
type: "textarea",
maxlength: -1, //
showCount: false, //
placeholder: "请输入计划内容,支持插入图片",
},
},
])
@ -246,8 +235,6 @@ const education = reactive<any>(
function addEducation() {
education.xl.push({ value: {
jhjd: '',
jhsj: '',
jhdd: '',
jhnr: ''
} })
}
@ -356,10 +343,8 @@ const submit = async () => {
//
for (let i = 0; i < education.xl.length; i++) {
const item = education.xl[i];
if (!item.value.jhjd || item.value.jhjd.trim() === '' ||
!item.value.jhsj || item.value.jhsj.trim() === '' ||
!item.value.jhdd || item.value.jhdd.trim() === '' ||
!item.value.jhnr || item.value.jhnr.trim() === '') {
const jhnrStr = String(item.value.jhnr || '').replace(/<[^>]*>/g, '').trim();
if (!item.value.jhjd || item.value.jhjd.trim() === '' || !item.value.jhnr || jhnrStr === '') {
uni.showToast({
title: `教学计划第${i + 1}项信息不完整`,
icon: 'none',
@ -418,8 +403,6 @@ onMounted(() => {
return {
value: {
jhjd: item.value.jhjd || '',
jhsj: item.value.jhsj || '',
jhdd: item.value.jhdd || '',
jhnr: item.value.jhnr || ''
}
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

@ -0,0 +1,31 @@
tinymce.addI18n('zh_CN',{
"Redo": "重做",
"Undo": "撤销",
"Cut": "剪切",
"Copy": "复制",
"Paste": "粘贴",
"Select all": "全选",
"New document": "新文件",
"Ok": "确定",
"Cancel": "取消",
"Visual aids": "网格线",
"Bold": "粗体",
"Italic": "斜体",
"Underline": "下划线",
"Strikethrough": "删除线",
"Superscript": "上标",
"Subscript": "下标",
"Clear formatting": "清除格式",
"Align left": "左边对齐",
"Align center": "中间对齐",
"Align right": "右边对齐",
"Justify": "两端对齐",
"Bullet list": "项目符号",
"Numbered list": "编号列表",
"Decrease indent": "减少缩进",
"Increase indent": "增加缩进",
"Close": "关闭",
"Formats": "格式",
"Insert link": "插入链接",
"Insert image": "插入图片"
});

34
src/store/modules/menu.ts Normal file
View File

@ -0,0 +1,34 @@
import { defineStore } from "pinia";
import type { MobileMenuTreeNode } from "@/api/system/menu";
export const useMenuStore = defineStore({
id: "app-Menu",
state: () => ({
/** 树形菜单数据,持久化到 localStorage key: app-Menu */
mobileMenu: [] as MobileMenuTreeNode[],
}),
getters: {
getMobileMenu(): MobileMenuTreeNode[] {
return this.mobileMenu || [];
},
},
actions: {
/** 清空菜单 */
clearMenu() {
this.mobileMenu = [];
},
/**
*
* @param menu
*/
setMobileMenu(menu: MobileMenuTreeNode[]) {
this.clearMenu();
this.mobileMenu = menu && Array.isArray(menu) ? [...menu] : [];
},
},
persist: {
enabled: true,
detached: true,
H5Storage: localStorage,
},
});

View File

@ -1,6 +1,5 @@
import { defineStore } from "pinia";
import {
authenticationApi,
loginCode,
loginPass,
weChatLogin,
@ -9,10 +8,6 @@ import {
import { AUTH_KEY } from "@/config";
import { useDicStore } from "@/store/modules/dic";
import { useCommonStore } from "@/store/modules/common";
import {
refreshPermissionCache,
clearPermissionCachePublic,
} from "@/utils/permission";
import { useDataStore } from "./data";
interface UserState {
@ -85,12 +80,6 @@ export const useUserStore = defineStore({
console.log("设置新的权限数据");
this.auth = data;
// 权限数据更新时,自动刷新缓存(可选)
if (autoRefreshCache && data && data.length > 0) {
console.log("自动刷新权限缓存...");
refreshPermissionCache(data);
}
console.log("=== setAuth 完成 ===");
},
/**
@ -192,35 +181,15 @@ export const useUserStore = defineStore({
useDicStore().setData({});
useCommonStore().setData({});
// 检查用户数据中是否已经包含权限信息
// 权限由后端 getMobileMenuApi 等接口按角色过滤,不再单独拉取 find-by-user
if (value.auth && Array.isArray(value.auth) && value.auth.length > 0) {
console.log("✅ 用户数据中包含权限信息:", value.auth);
console.log("权限数量:", value.auth.length);
this.setAuth(value.auth, false);
} else {
console.log("用户数据中无权限信息,调用 authenticationApi 获取权限...");
authenticationApi({ userId: value.id })
.then(({ result }) => {
if (result) {
console.log("✅ 获取到权限数据:", result);
console.log("权限数量:", result.length);
this.setAuth(result, false);
} else {
console.log("❌ authenticationApi 返回空结果");
}
})
.catch((error) => {
console.error("❌ authenticationApi 调用失败:", error);
});
}
},
/**
* @description:
*/
logout() {
// 清除权限缓存
clearPermissionCachePublic();
this.setToken("");
this.setUser({});
this.setJs({});

View File

@ -2,88 +2,9 @@ import {ISROUTERINTERCEPT} from "@/config";
import {getRouter} from "@/utils/uniapp";
import {useUserStore} from "@/store/modules/user";
// 权限缓存相关常量
// 权限缓存 keygetLogin 清除用)
const PERMISSION_CACHE_KEY = 'user_permissions_cache';
// 权限缓存接口
interface PermissionCache {
permissions: string[];
timestamp: number;
userId: string;
changeTime: string;
}
// 存储工具函数
function setStorage(key: string, value: any): void {
try {
if (typeof localStorage !== 'undefined') {
const jsonValue = JSON.stringify(value);
localStorage.setItem(key, jsonValue);
} else {
uni.setStorageSync(key, value);
}
} catch (error) {
// 静默处理错误
}
}
function getStorage(key: string): any {
try {
if (typeof localStorage !== 'undefined') {
const value = localStorage.getItem(key);
return value ? JSON.parse(value) : null;
} else {
const value = uni.getStorageSync(key);
return value;
}
} catch (error) {
return null;
}
}
// 从app-user中获取权限变更时间
function getChangeTimeFromAppUser(): string | null {
try {
const userStore = useUserStore();
if (userStore.getChangeTime && userStore.getChangeTime.trim() !== '') {
return userStore.getChangeTime;
}
if (typeof localStorage !== 'undefined') {
const appUser = localStorage.getItem('app-user');
if (appUser) {
const userData = JSON.parse(appUser);
if (userData.changeTime) {
return userData.changeTime;
}
}
}
return null;
} catch (error) {
return null;
}
}
// 将权限变更时间存储到app-user中
function setChangeTimeToAppUser(changeTime: string): void {
try {
const userStore = useUserStore();
userStore.setChangeTime(changeTime);
if (typeof localStorage !== 'undefined') {
const appUser = localStorage.getItem('app-user');
if (appUser) {
const userData = JSON.parse(appUser);
userData.changeTime = changeTime;
localStorage.setItem('app-user', JSON.stringify(userData));
}
}
} catch (error) {
// 静默处理错误
}
}
// 路由拦截器 - 默认导出
export default function (whitelist: WhiteList) {
const WHITELIST = ['/', ...whitelist, {pattern: /^\/pages\/system\/.*/}];
@ -122,55 +43,7 @@ export default function (whitelist: WhiteList) {
}
/**
*
*/
function getPermissionCache(): PermissionCache | null {
try {
const cacheData = getStorage(PERMISSION_CACHE_KEY);
const changeTime = getChangeTimeFromAppUser();
if (!cacheData || !changeTime) {
return null;
}
return {
permissions: cacheData.permissions,
timestamp: cacheData.timestamp,
userId: cacheData.userId,
changeTime: cacheData.changeTime
};
} catch (error) {
return null;
}
}
/**
*
* @param permissions
* @param userId ID
* @param changeTime
*/
function setPermissionCache(permissions: string[], userId: string, changeTime?: string): void {
try {
const defaultChangeTime = '2024-01-01 00:00:00';
const finalChangeTime = changeTime || defaultChangeTime;
const cacheData: PermissionCache = {
permissions,
timestamp: Date.now(),
userId,
changeTime: finalChangeTime
};
setStorage(PERMISSION_CACHE_KEY, cacheData);
setChangeTimeToAppUser(finalChangeTime);
} catch (error) {
// 静默处理错误
}
}
/**
*
* getLogin
*/
function clearPermissionCache(): void {
try {
@ -184,129 +57,6 @@ function clearPermissionCache(): void {
}
}
/**
*
*/
function isCacheValid(cache: PermissionCache, currentUserId: string): boolean {
return cache.userId === currentUserId;
}
/**
*
* @param currentChangeTime
* @returns
*/
function getUserPermissionsWithCache(currentChangeTime?: string): string[] {
const userStore = useUserStore();
const currentUser = userStore.getUser;
const currentUserId = currentUser?.id || currentUser?.userdata?.id;
if (!currentUserId) {
return userStore.getAuth;
}
const cache = getPermissionCache();
if (cache && isCacheValid(cache, currentUserId)) {
if (currentChangeTime) {
const serverTime = new Date(currentChangeTime).getTime();
const cacheTime = new Date(cache.changeTime).getTime();
if (serverTime > cacheTime) {
const permissions = userStore.getAuth;
if (permissions && permissions.length > 0) {
setPermissionCache(permissions, currentUserId, currentChangeTime);
}
return permissions;
} else {
return cache.permissions;
}
} else {
return cache.permissions;
}
}
const permissions = userStore.getAuth;
if (permissions && permissions.length > 0) {
setPermissionCache(permissions, currentUserId, currentChangeTime);
}
return permissions;
}
/**
*
* @param permissions
* @param changeTime
*/
export function refreshPermissionCache(permissions?: string[], changeTime?: string): void {
const userStore = useUserStore();
const currentUser = userStore.getUser;
const currentUserId = currentUser?.id || currentUser?.userdata?.id;
if (!currentUserId) {
return;
}
const permissionList = permissions || userStore.getAuth;
const currentCache = getPermissionCache();
if (currentCache && currentCache.permissions && permissionList) {
const isSame = JSON.stringify(currentCache.permissions.sort()) === JSON.stringify(permissionList.sort());
if (isSame && currentCache.changeTime === changeTime) {
return;
}
}
setPermissionCache(permissionList, currentUserId, changeTime);
if (changeTime) {
userStore.setChangeTime(changeTime);
}
}
/**
*
*/
export function clearPermissionCachePublic(): void {
clearPermissionCache();
}
// 权限检查函数
export function _auth(autd: string, changeTime?: string) {
const permissions = getUserPermissionsWithCache(changeTime);
return permissions.includes(autd);
}
export function hasPermission(permissionKey: string, changeTime?: string): boolean {
if (!permissionKey) return true;
const permissions = getUserPermissionsWithCache(changeTime);
// 去重处理,避免重复权限影响判断
const uniquePermissions = [...new Set(permissions)];
return uniquePermissions.includes(permissionKey);
}
export function hasAnyPermission(permissionKeys: string[], changeTime?: string): boolean {
if (!permissionKeys || permissionKeys.length === 0) return true;
const permissions = getUserPermissionsWithCache(changeTime);
// 去重处理,避免重复权限影响判断
const uniquePermissions = [...new Set(permissions)];
return permissionKeys.some(key => uniquePermissions.includes(key));
}
export function hasAllPermissions(permissionKeys: string[], changeTime?: string): boolean {
if (!permissionKeys || permissionKeys.length === 0) return true;
const permissions = getUserPermissionsWithCache(changeTime);
// 去重处理,避免重复权限影响判断
const uniquePermissions = [...new Set(permissions)];
return permissionKeys.every(key => uniquePermissions.includes(key));
}
export function getUserPermissions(changeTime?: string): string[] {
const permissions = getUserPermissionsWithCache(changeTime);
// 返回去重后的权限列表
return permissions ? [...new Set(permissions)] : [];
}
export function getLogin(): void {
clearPermissionCache();
uni.reLaunch({
@ -314,88 +64,8 @@ export function getLogin(): void {
});
}
export function isLogin(): boolean {
if (ISROUTERINTERCEPT) {
const store = useUserStore();
if (!store.getToken) {
let curRoute: string | undefined = getRouter()
if (curRoute) {
if (!/^\//.test(curRoute)) {
curRoute = '/' + curRoute
}
}
loginPage(curRoute ? curRoute : '')
return false
}
return true
}
return false
}
function loginPage(url: string) {
uni.redirectTo({
url: "/pages/system/login/login?redirect=" + url,
});
}
export const ROUTINE_PERMISSIONS = {
JIAO_XUE_ZI_YUAN: 'JiaoXueZiYuan',
JI_FEN_PING_JIA: 'JiFenPingJia',
GONG_ZUO_LIANG: 'GongZuoLiang',
RENG_JIAO_RENG_ZHI: 'RengJiaoRengZhi',
SHI_TANG_XUN_CHA: 'ShiTangXunCha',
KE_FU_XUN_CHA: 'kefuxuncha',
GROUP_TEACHING: 'groupTeaching',
NOTICE: 'notice',
ROUTINE: 'routine',
} as const;
export function isTeacherUser(): boolean {
const userStore = useUserStore();
const user = userStore.getUser;
return user && user.userType === 'teacher';
}
export function isAdminUser(): boolean {
const userStore = useUserStore();
const user = userStore.getUser;
return user && user.userType === 'admin';
}
export const PermissionCacheManager = {
getCacheInfo() {
const cache = getPermissionCache();
const changeTime = getChangeTimeFromAppUser();
return {
hasCache: !!cache,
changeTime: changeTime ? new Date(changeTime).toLocaleString() : null,
isExpired: cache ? Date.now() > cache.timestamp : true,
cacheSize: cache ? cache.permissions.length : 0
};
},
/**
*
*/
debugCache() {
// 移除所有调试信息
},
forceRefresh() {
const userStore = useUserStore();
const permissions = userStore.getAuth;
const currentUser = userStore.getUser;
const currentUserId = currentUser?.id || currentUser?.userdata?.id;
if (currentUserId && permissions) {
setPermissionCache(permissions, currentUserId);
return true;
}
return false;
},
clear() {
clearPermissionCache();
}
};
}