SAAS模式调整
This commit is contained in:
parent
8fa41cf9e2
commit
7f02fc7e9c
@ -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",
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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
14
src/api/base/jzApi.ts
Normal 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
16
src/api/routine/jz.ts
Normal 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);
|
||||
}
|
||||
@ -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");
|
||||
|
||||
@ -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
17
src/api/system/menu.ts
Normal 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");
|
||||
178
src/components/Tinymce/TinymceEditorH5.vue
Normal file
178
src/components/Tinymce/TinymceEditorH5.vue
Normal 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;
|
||||
}
|
||||
|
||||
/* 初始隐藏 textarea,TinyMCE 会替换它 */
|
||||
.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>
|
||||
45
src/components/Tinymce/TinymceOrEditor.vue
Normal file
45
src/components/Tinymce/TinymceOrEditor.vue
Normal 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>
|
||||
@ -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": {
|
||||
|
||||
@ -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>
|
||||
1984
src/pages/base/service/index copy.vue
Normal file
1984
src/pages/base/service/index copy.vue
Normal file
File diff suppressed because it is too large
Load Diff
@ -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 [];
|
||||
}
|
||||
|
||||
// 过滤出有权限且show为true的子项
|
||||
return folderItems.filter(item => {
|
||||
return item.show && hasPermissionDirect(item.permissionKey);
|
||||
});
|
||||
return folderItems.filter(item => item.show);
|
||||
};
|
||||
|
||||
// 暴露生命周期钩子给uni-app
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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>
|
||||
@ -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(() => {
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
660
src/pages/view/routine/da/jsda/index.vue
Normal file
660
src/pages/view/routine/da/jsda/index.vue
Normal 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>
|
||||
|
||||
312
src/pages/view/routine/da/jzda/index.vue
Normal file
312
src/pages/view/routine/da/jzda/index.vue
Normal 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)
|
||||
|
||||
// 打开学生选择器:统一传递 njIds、bjIds、xsIds 供后端多条件查询
|
||||
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>
|
||||
450
src/pages/view/routine/da/xsda/studentArchive.vue
Normal file
450
src/pages/view/routine/da/xsda/studentArchive.vue
Normal 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
|
||||
})
|
||||
|
||||
// 打开学生选择器:统一传递 njIds、bjIds、xsIds 供后端多条件查询
|
||||
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>
|
||||
@ -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";
|
||||
|
||||
@ -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 || ''
|
||||
}
|
||||
};
|
||||
|
||||
BIN
src/static/base/home/grda.png
Normal file
BIN
src/static/base/home/grda.png
Normal file
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 |
BIN
src/static/base/home/jzda.png
Normal file
BIN
src/static/base/home/jzda.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.1 KiB |
31
src/static/tinymce/langs/zh_CN.js
Normal file
31
src/static/tinymce/langs/zh_CN.js
Normal 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
34
src/store/modules/menu.ts
Normal 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,
|
||||
},
|
||||
});
|
||||
@ -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({});
|
||||
|
||||
@ -2,88 +2,9 @@ import {ISROUTERINTERCEPT} from "@/config";
|
||||
import {getRouter} from "@/utils/uniapp";
|
||||
import {useUserStore} from "@/store/modules/user";
|
||||
|
||||
// 权限缓存相关常量
|
||||
// 权限缓存 key(getLogin 清除用)
|
||||
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();
|
||||
}
|
||||
};
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user