统计调整

This commit is contained in:
hebo 2025-11-18 15:49:03 +08:00
parent 6839157a49
commit 6161a2fa8d
20 changed files with 2736 additions and 415 deletions

View File

@ -34,3 +34,6 @@ export const findAllZw = () => {

View File

@ -0,0 +1,101 @@
import { get } from "@/utils/request";
export interface StudentCountStatistics {
totalCount: number;
followedCount: number;
unfollowedCount: number;
}
export interface ParentIndustryStatisticsItem {
industry: string;
count: number;
}
export interface StudentSourceStatisticsItem {
sourceName: string;
sourceCode: string;
count: number;
}
export interface StudentStatusStatisticsItem {
statusName: string;
statusCode: string;
count: number;
}
/**
*
*/
export const getStudentCountStatisticsApi = () => {
return get<StudentCountStatistics>("/api/xs/statistics/studentCount");
};
/**
*
*/
export const getParentIndustryStatisticsApi = () => {
return get<ParentIndustryStatisticsItem[]>("/api/xs/statistics/parentIndustry");
};
/**
*
*/
export const getStudentSourceStatisticsApi = () => {
return get<StudentSourceStatisticsItem[]>("/api/xs/statistics/studentSource");
};
/**
*
*/
export const getStudentStatusStatisticsApi = () => {
return get<StudentStatusStatisticsItem[]>("/api/xs/statistics/studentStatus");
};
/**
*
*/
export interface StudentScaleDashboard {
studentCount: StudentCountStatistics;
parentIndustry: ParentIndustryStatisticsItem[];
studentSource: StudentSourceStatisticsItem[];
studentStatus: StudentStatusStatisticsItem[];
}
export const getStudentScaleDashboardApi = () => {
return get<StudentScaleDashboard>("/api/xs/statistics/dashboard");
};
/**
*
*/
export const getGradeStatisticsApi = (params: { type: string; code: string }) => {
return get("/api/xs/gradeStatistics", params);
};
/**
*
*/
export const getClassStatisticsApi = (params: { type: string; code: string; njId: string }) => {
return get("/api/xs/classStatistics", params);
};
/**
*
*/
export const getStudentListApi = (params: {
type: string;
code: string;
subType?: string;
njId?: string;
bjId?: string;
}) => {
return get("/api/xs/studentList", params);
};
/**
*
*/
export const getParentListApi = (params: { industry: string }) => {
return get("/api/xs/parentList", params);
};

View File

@ -48,6 +48,7 @@ export interface TeacherAnalyticsDashboardResponse {
position?: TeacherDistributionItem[];
political?: TeacherDistributionItem[];
workingYears?: TeacherDistributionItem[];
zdqk?: TeacherDistributionItem[];
}
export interface TeacherAnalyticsListRequest extends TeacherAnalyticsFilter {

View File

@ -297,6 +297,41 @@
"enablePullDownRefresh": false
}
},
{
"path": "pages/statistics/xs/index",
"style": {
"navigationBarTitleText": "学生规模",
"enablePullDownRefresh": false
}
},
{
"path": "pages/statistics/xs/gradeDetail",
"style": {
"navigationBarTitleText": "年级统计",
"enablePullDownRefresh": false
}
},
{
"path": "pages/statistics/xs/classDetail",
"style": {
"navigationBarTitleText": "班级统计",
"enablePullDownRefresh": false
}
},
{
"path": "pages/statistics/xs/studentList",
"style": {
"navigationBarTitleText": "学生明细",
"enablePullDownRefresh": false
}
},
{
"path": "pages/statistics/xs/parentDetail",
"style": {
"navigationBarTitleText": "家长明细",
"enablePullDownRefresh": false
}
},
{
"path": "pages/statistics/zb/pdfPreview",
"style": {

View File

@ -527,6 +527,14 @@ const sections = reactive<Section[]>([
},
{
id: "analysis2",
icon: "xsgm",
text: "学生规模",
show: true,
permissionKey: "analysis-xsgm",
path: "/pages/statistics/xs/index",
},
{
id: "analysis3",
icon: "jctj",
text: "就餐统计",
show: true,
@ -534,7 +542,7 @@ const sections = reactive<Section[]>([
path: "/pages/statistics/jc/index",
},
{
id: "analysis3",
id: "analysis4",
icon: "jlb",
text: "俱乐部统计",
show: true,
@ -542,7 +550,7 @@ const sections = reactive<Section[]>([
path: "/pages/statistics/jlb/index",
},
{
id: "analysis4",
id: "analysis5",
icon: "xqk",
text: "兴趣课统计",
show: true,
@ -550,7 +558,7 @@ const sections = reactive<Section[]>([
path: "/pages/statistics/xqk/index",
},
{
id: "analysis5",
id: "analysis6",
icon: "ky",
text: "课业统计",
show: true,
@ -558,15 +566,15 @@ const sections = reactive<Section[]>([
path: "/pages/statistics/ky/index",
},
{
id: "analysis6",
id: "analysis7",
icon: "zbtj",
text: "值统计",
text: "值统计",
show: true,
permissionKey: "analysis-zbtj",
path: "/pages/statistics/zb/index",
},
{
id: "analysis7",
id: "analysis8",
icon: "qdtj",
text: "签到统计",
show: true,

View File

@ -30,6 +30,17 @@
<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">
@ -67,6 +78,11 @@ interface Teacher {
jsType?: string;
zzmmId?: string;
jsgl?: number;
qjlx?: string; //
qjkstime?: string; //
qjjstime?: string; //
qjsy?: string; //
dcyy?: string; //
}
const commonStore = useCommonStore();
@ -165,6 +181,9 @@ const filteredTeachers = computed(() => {
return teacher.zzmmId === filterValue.value;
case 'workingYears':
return filterByWorkingYears(teacher, filterValue.value);
case 'zdqk':
// fetchList
return true;
default:
return true;
}
@ -190,6 +209,39 @@ const fetchList = async () => {
}));
return;
}
//
if (filterType.value === 'zdqk' && filterValue.value) {
const params: any = {
zdqkValue: filterValue.value,
};
if (jsTypes.value.length > 0) {
params.jsTypes = jsTypes.value.join(',');
}
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;
@ -230,6 +282,20 @@ const parseGender = (jsxbId?: string) => {
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((options: any) => {
if (options.filterType) {
filterType.value = decodeURIComponent(options.filterType);
@ -409,6 +475,23 @@ onLoad((options: any) => {
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;

View File

@ -66,6 +66,47 @@
</view>
</view>
<view class="section">
<view class="section-header">
<text class="section-title">在岗情况</text>
</view>
<view class="personnel-content">
<view class="composition-chart-wrapper">
<QiunDataCharts
class="composition-chart"
canvas-id="zdqk-ring"
type="ring"
:chart-data="zdqkCompositionChart"
:opts="zdqkCompositionOpts"
:animation="false"
:in-scroll-view="true"
/>
</view>
<view class="composition-side">
<view v-if="compositionLoading" class="composition-placeholder">加载中...</view>
<view v-else-if="!zdqkCompositionLegend.length" class="composition-placeholder">暂无数据</view>
<view v-else class="composition-legend">
<view class="legend-item" v-for="item in zdqkCompositionLegend" :key="item.key" @click="handleZdqkCompositionClick(item)">
<view class="legend-info">
<view class="legend-left">
<view class="legend-dot" :style="{ backgroundColor: item.color }" />
<text class="legend-name-large">{{ item.label }}</text>
</view>
<view class="legend-right">
<view class="legend-value-group">
<text class="legend-count">{{ item.count }}</text>
<text class="legend-divider">|</text>
<text class="legend-percent">{{ item.percent }}</text>
</view>
<text class="legend-arrow">></text>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
<template v-for="(section, index) in chartSections" :key="section.key">
<view class="section">
<view class="section-header">
@ -180,7 +221,7 @@ interface DistributionItem {
averageAge?: number; //
}
type DistributionKey = "age" | "education" | "title" | "position" | "political" | "workingYears";
type DistributionKey = "age" | "education" | "title" | "position" | "political" | "workingYears" | "zdqk";
type DistributionMap = Record<DistributionKey, DistributionItem[]>;
@ -231,6 +272,14 @@ const workingYearsColors = [
"#EA580C",
];
const zdqkColors = [
"#22C55E", // - 绿
"#F59E0B", // -
"#EF4444", // -
"#8B5CF6", // 退 -
"#6B7280", // -
];
const analyticsStore = useTeacherAnalyticsStore();
const { composition, distributions } = storeToRefs(analyticsStore);
@ -415,6 +464,75 @@ const staffCompositionLegend = computed(() =>
}))
);
const zdqkCompositionLegend = computed(() =>
zdqkDetails.value.map((item) => ({
key: item.key,
label: item.label,
count: item.count,
percent: zdqkTotal.value > 0
? `${((item.count / zdqkTotal.value) * 100).toFixed(1)}%`
: "-",
color: item.color,
}))
);
const zdqkCompositionChart = computed(() => {
const total = zdqkTotal.value;
return {
series: [
{
name: "在岗情况",
data: zdqkDetails.value.map((item) => {
const percent = total > 0 ? ((item.count / total) * 100).toFixed(1) : "0.0";
return {
name: item.label,
value: item.count,
color: item.color,
labelText: `${percent}%`,
};
}),
},
],
};
});
const zdqkCompositionOpts = computed(() => ({
height: 420,
rotate: false,
rotateLock: false,
color: zdqkColors,
padding: [15, 20, 15, 15],
dataLabel: true,
enableScroll: false,
legend: {
show: false,
},
title: {
name: String(zdqkTotal.value || 0),
fontSize: 30,
color: "#3b82f6",
},
subtitle: {
name: "总人数",
fontSize: 12,
color: "#666666",
},
dataPointShape: false,
extra: {
ring: {
ringWidth: 15,
activeOpacity: 0.5,
activeRadius: 10,
offsetAngle: 0,
labelWidth: 22,
border: true,
borderWidth: 3,
borderColor: "#FFFFFF",
customLabel: true,
},
},
}));
const educationDetails = computed(() => {
const eduList = distributions.value?.education || [];
return eduList.map((item, index) => ({
@ -507,6 +625,72 @@ const workingYearsTotal = computed(() =>
workingYearsDetails.value.reduce((sum, item) => sum + (item.count ?? 0), 0)
);
const zdqkDetails = computed(() => {
const zdqkList = distributions.value?.zdqk || [];
return zdqkList.map((item, index) => ({
key: item.name || `zdqk-${index}`,
label: item.name || "其他",
count: item.count ?? 0,
color: zdqkColors[index % zdqkColors.length],
}));
});
const zdqkTotal = computed(() =>
zdqkDetails.value.reduce((sum, item) => sum + (item.count ?? 0), 0)
);
const zdqkChart = computed(() => {
const total = zdqkTotal.value;
return {
series: [
{
name: "在岗情况",
data: zdqkDetails.value.map((item) => {
const percent = total > 0 ? Math.round((item.count / total) * 100) : 0;
return {
name: `${item.label}${item.count}`,
value: item.count,
color: item.color,
labelText: `${item.label}${percent}%`,
};
}),
},
],
};
});
const zdqkChartOpts = {
height: 500,
color: zdqkColors,
padding: [20, 20, 0, 15],
enableScroll: false,
legend: {
show: true,
position: "bottom",
lineHeight: 25,
},
dataLabel: true,
title: {
name: "",
},
subtitle: {
name: "",
},
extra: {
pie: {
activeOpacity: 0.5,
activeRadius: 10,
offsetAngle: 0,
labelWidth: 25,
border: true,
borderWidth: 2,
borderColor: "#FFFFFF",
linearType: "none",
radius: 140,
},
},
};
const workingYearsChart = computed(() => {
return {
series: [
@ -528,6 +712,7 @@ const emptyDistributions: DistributionMap = {
position: [],
political: [],
workingYears: [],
zdqk: [],
};
const CHART_HEIGHT = 320;
@ -955,7 +1140,7 @@ const chartSections = computed<ChartSectionConfig[]>(() => {
},
{
key: "position",
title: "岗位情况",
title: "专业技术岗位",
type: "bar",
chartData: {
categories: (dist.position || []).map((item) => item.name),
@ -995,6 +1180,14 @@ const handleCompositionClick = (item: any) => {
});
};
const handleZdqkCompositionClick = (item: any) => {
//
const jsTypes = selectedJsTypes.value.join(',');
uni.navigateTo({
url: `/pages/statistics/teacher/detail?filterType=zdqk&filterValue=${encodeURIComponent(item.label)}&jsTypes=${encodeURIComponent(jsTypes)}`,
});
};
//
const handleChartClick = (filterType: string, filterValue: string) => {
//
@ -1048,6 +1241,28 @@ const handleWorkingYearsClick = (e: any) => {
}
};
//
const handleZdqkClick = (e: any) => {
console.log('在岗情况图表点击事件:', e);
if (!e || !zdqkDetails.value.length) return;
let index = -1;
if (e.currentIndex !== undefined) {
if (typeof e.currentIndex === 'object' && e.currentIndex.index !== undefined) {
index = e.currentIndex.index;
} else if (typeof e.currentIndex === 'number') {
index = e.currentIndex;
}
}
if (index >= 0 && index < zdqkDetails.value.length) {
const item = zdqkDetails.value[index];
console.log('点击在岗情况项:', item);
handleChartClick('zdqk', item.label);
}
};
//
const handleSectionChartClick = (sectionKey: string, e: any) => {
console.log('图表点击事件:', sectionKey, e);

View File

@ -0,0 +1,280 @@
<!-- src/pages/statistics/xs/classDetail.vue -->
<template>
<view class="class-detail-page">
<!-- 顶部标题 -->
<view class="page-header">
<text class="page-title">{{ njmc }} - {{ pageTitle }}</text>
<text class="refresh-btn" @click="refreshData">刷新</text>
</view>
<!-- 班级统计列表 -->
<view class="class-list">
<view
class="class-item"
v-for="classItem in classStats"
:key="classItem.bj_id"
>
<view class="class-header">
<text class="class-name">{{ classItem.bjmc }}</text>
</view>
<view class="class-stats">
<view class="stat-item">
<text class="stat-label">总人数</text>
<text class="stat-value">{{ classItem.totalCount }}</text>
</view>
<view
class="stat-item clickable"
v-for="(item, index) in getClassDetailItems(classItem)"
:key="index"
@click="goToStudentList(classItem, item.type, item.label)"
>
<text class="stat-label">{{ item.label }}</text>
<text class="stat-value" :class="item.valueClass">{{ item.value }}</text>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view class="empty-state" v-if="classStats.length === 0 && !loading">
<text class="empty-text">暂无班级数据</text>
</view>
<!-- 加载状态 -->
<view class="loading-state" v-if="loading">
<text class="loading-text">加载中...</text>
</view>
</view>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { getClassStatisticsApi } from '@/api/statistics/studentScaleApi'
interface ClassStats {
nj_id: string;
njmc: string;
bj_id: string;
bjmc: string;
totalCount: number;
[key: string]: any;
}
const pageTitle = ref('')
const statType = ref('')
const statCode = ref('')
const njId = ref('')
const njmc = ref('')
const classStats = ref<ClassStats[]>([])
const loading = ref(false)
//
const getClassDetailItems = (classItem: ClassStats) => {
const items: any[] = []
if (statType.value === 'count') {
if (statCode.value === 'all') {
items.push(
{ label: '已关注', value: classItem.followedCount || 0, type: 'followed', valueClass: 'success' },
{ label: '未关注', value: classItem.unfollowedCount || 0, type: 'unfollowed', valueClass: 'danger' }
)
} else if (statCode.value === 'followed') {
items.push({ label: '已关注', value: classItem.followedCount || 0, type: 'followed', valueClass: 'success' })
} else if (statCode.value === 'unfollowed') {
items.push({ label: '未关注', value: classItem.unfollowedCount || 0, type: 'unfollowed', valueClass: 'danger' })
}
} else if (statType.value === 'source') {
const sourceMap: Record<string, string> = {
'A': '正常入学',
'B': '转入',
'C': '复学'
}
const label = sourceMap[statCode.value] || '未知'
items.push({ label, value: classItem.totalCount || 0, type: statCode.value, valueClass: 'info' })
} else if (statType.value === 'status') {
const statusMap: Record<string, string> = {
'A': '正常',
'B': '转出',
'C': '休学',
'D': '删除',
'E': '辍学',
'G': '死亡'
}
const label = statusMap[statCode.value] || '未知'
items.push({ label, value: classItem.totalCount || 0, type: statCode.value, valueClass: 'info' })
}
return items
}
//
const refreshData = async () => {
await loadClassDetailData()
}
//
const goToStudentList = (classItem: ClassStats, type: string, label: string) => {
uni.navigateTo({
url: `/pages/statistics/xs/studentList?type=${statType.value}&code=${statCode.value}&subType=${type}&njId=${njId.value}&bjId=${classItem.bj_id}&njmc=${encodeURIComponent(njmc.value)}&bjmc=${encodeURIComponent(classItem.bjmc)}&title=${encodeURIComponent(label)}`
})
}
//
const loadClassDetailData = async () => {
try {
loading.value = true
const result = await getClassStatisticsApi({
type: statType.value,
code: statCode.value,
njId: njId.value
})
if (result.resultCode === 1 && result.result) {
classStats.value = result.result
}
} catch (error) {
console.error('加载班级详情数据失败:', error)
uni.showToast({
title: '加载失败',
icon: 'error'
})
} finally {
loading.value = false
}
}
onLoad((options) => {
if (options.type && options.code && options.njId && options.njmc && options.title) {
statType.value = options.type
statCode.value = options.code
njId.value = options.njId
njmc.value = decodeURIComponent(options.njmc)
pageTitle.value = decodeURIComponent(options.title)
loadClassDetailData()
}
})
</script>
<style scoped lang="scss">
.class-detail-page {
min-height: 100vh;
background-color: #f5f7fa;
padding: 12px;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
background-color: #fff;
border-radius: 12px;
padding: 16px;
margin-bottom: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.page-title {
font-size: 16px;
font-weight: 600;
color: #333;
}
.refresh-btn {
font-size: 14px;
color: #1890ff;
padding: 4px 8px;
border: 1px solid #1890ff;
border-radius: 4px;
}
.class-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.class-item {
background-color: #fff;
border-radius: 12px;
padding: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.class-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
padding-bottom: 12px;
border-bottom: 1px solid #f0f0f0;
}
.class-name {
font-size: 16px;
font-weight: 600;
color: #333;
}
.class-stats {
display: flex;
justify-content: space-around;
}
.stat-item {
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
&.clickable {
&:active {
transform: scale(0.95);
}
}
}
.stat-label {
display: block;
font-size: 12px;
color: #666;
margin-bottom: 4px;
}
.stat-value {
font-size: 16px;
font-weight: bold;
color: #333;
&.success {
color: #52c41a;
}
&.danger {
color: #ff4d4f;
}
&.info {
color: #1890ff;
}
}
.empty-state,
.loading-state {
display: flex;
justify-content: center;
align-items: center;
height: 200px;
background-color: #fff;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.empty-text,
.loading-text {
font-size: 14px;
color: #999;
}
</style>

View File

@ -0,0 +1,311 @@
<!-- src/pages/statistics/xs/gradeDetail.vue -->
<template>
<view class="grade-detail-page">
<!-- 顶部标题 -->
<view class="page-header">
<text class="page-title">{{ pageTitle }}</text>
<text class="refresh-btn" @click="refreshData">刷新</text>
</view>
<!-- 各年级统计 -->
<view class="grade-stats" v-for="grade in gradeStats" :key="grade.nj_id">
<view class="grade-header">
<text class="grade-name">{{ grade.njmc }}</text>
<view class="grade-right">
<text class="detail-arrow" @click="goToClassDetail(grade.nj_id, grade.njmc)">></text>
</view>
</view>
<view class="grade-details">
<view class="detail-item">
<text class="detail-label">总人数</text>
<text class="detail-value">{{ grade.totalCount }}</text>
</view>
<view
class="detail-item clickable"
v-for="(item, index) in getGradeDetailItems(grade)"
:key="index"
@click="goToStudentList(grade.nj_id, grade.njmc, item.type, item.label)"
>
<text class="detail-label">{{ item.label }}</text>
<text class="detail-value" :class="item.valueClass">{{ item.value }}</text>
</view>
</view>
</view>
<!-- 空状态 -->
<view class="empty-state" v-if="gradeStats.length === 0 && !loading">
<text class="empty-text">暂无年级数据</text>
</view>
<!-- 加载状态 -->
<view class="loading-state" v-if="loading">
<text class="loading-text">加载中...</text>
</view>
</view>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { getGradeStatisticsApi } from '@/api/statistics/studentScaleApi'
interface GradeStats {
nj_id: string;
njmc: string;
totalCount: number;
[key: string]: any;
}
const pageTitle = ref('')
const statType = ref('') // count, source, status
const statCode = ref('') // all, followed, unfollowed, A, B, C, etc.
const gradeStats = ref<GradeStats[]>([])
const loading = ref(false)
//
const getGradeDetailItems = (grade: GradeStats) => {
const items: any[] = []
if (statType.value === 'count') {
if (statCode.value === 'all') {
//
items.push(
{ label: '已关注', value: grade.followedCount || 0, type: 'followed', valueClass: 'success' },
{ label: '未关注', value: grade.unfollowedCount || 0, type: 'unfollowed', valueClass: 'danger' }
)
} else if (statCode.value === 'followed') {
//
items.push({ label: '已关注', value: grade.followedCount || 0, type: 'followed', valueClass: 'success' })
} else if (statCode.value === 'unfollowed') {
//
items.push({ label: '未关注', value: grade.unfollowedCount || 0, type: 'unfollowed', valueClass: 'danger' })
}
} else if (statType.value === 'source') {
// - totalCount
const sourceMap: Record<string, string> = {
'A': '正常入学',
'B': '转入',
'C': '复学'
}
const label = sourceMap[statCode.value] || '未知'
items.push({ label, value: grade.totalCount || 0, type: statCode.value, valueClass: 'info' })
} else if (statType.value === 'status') {
// - totalCount
const statusMap: Record<string, string> = {
'A': '正常',
'B': '转出',
'C': '休学',
'D': '删除',
'E': '辍学',
'G': '死亡'
}
const label = statusMap[statCode.value] || '未知'
items.push({ label, value: grade.totalCount || 0, type: statCode.value, valueClass: 'info' })
}
return items
}
//
const refreshData = async () => {
await loadGradeStats()
}
//
const goToClassDetail = (njId: string, njmc: string) => {
uni.navigateTo({
url: `/pages/statistics/xs/classDetail?type=${statType.value}&code=${statCode.value}&njId=${njId}&njmc=${encodeURIComponent(njmc)}&title=${encodeURIComponent(pageTitle.value)}`
})
}
//
const goToStudentList = (njId: string, njmc: string, type: string, label: string) => {
uni.navigateTo({
url: `/pages/statistics/xs/studentList?type=${statType.value}&code=${statCode.value}&subType=${type}&njId=${njId}&njmc=${encodeURIComponent(njmc)}&title=${encodeURIComponent(label)}`
})
}
//
const loadGradeStats = async () => {
try {
loading.value = true
const result = await getGradeStatisticsApi({
type: statType.value,
code: statCode.value
})
if (result.resultCode === 1 && result.result) {
gradeStats.value = result.result
}
} catch (error) {
console.error('加载年级统计数据失败:', error)
uni.showToast({
title: '加载失败',
icon: 'error'
})
} finally {
loading.value = false
}
}
onLoad((options) => {
if (options.type && options.code && options.title) {
statType.value = options.type
statCode.value = options.code
pageTitle.value = decodeURIComponent(options.title)
loadGradeStats()
}
})
</script>
<style scoped lang="scss">
.grade-detail-page {
min-height: 100vh;
background-color: #f5f7fa;
padding: 12px;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
background-color: #fff;
border-radius: 12px;
padding: 16px;
margin-bottom: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.page-title {
font-size: 16px;
font-weight: 600;
color: #333;
}
.refresh-btn {
font-size: 14px;
color: #1890ff;
padding: 4px 8px;
border: 1px solid #1890ff;
border-radius: 4px;
}
.grade-stats {
margin-bottom: 12px;
border: 1px solid #f0f0f0;
border-radius: 8px;
background-color: #fff;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.grade-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background-color: #fafafa;
border-bottom: 1px solid #f0f0f0;
}
.grade-right {
display: flex;
align-items: center;
gap: 8px;
}
.grade-name {
font-size: 14px;
font-weight: 600;
color: #333;
}
.detail-arrow {
font-size: 16px;
color: #999;
font-weight: bold;
cursor: pointer;
transition: color 0.3s ease;
&:active {
color: #1890ff;
}
}
.grade-details {
display: flex;
justify-content: space-between;
padding: 12px 16px;
align-items: center;
flex-wrap: wrap;
gap: 8px 0;
}
.detail-item {
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
min-height: 50px;
justify-content: center;
flex: 1;
min-width: 0;
&.clickable {
cursor: pointer;
transition: all 0.3s ease;
border-radius: 8px;
padding: 8px;
&:active {
transform: scale(0.95);
background-color: #f0f8ff;
}
}
}
.detail-label {
display: block;
font-size: 12px;
color: #666;
margin-bottom: 4px;
}
.detail-value {
font-size: 16px;
font-weight: bold;
color: #333;
&.success {
color: #52c41a;
}
&.danger {
color: #ff4d4f;
}
&.info {
color: #1890ff;
}
}
.empty-state,
.loading-state {
display: flex;
justify-content: center;
align-items: center;
height: 200px;
background-color: #fff;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.empty-text,
.loading-text {
font-size: 14px;
color: #999;
}
</style>

View File

@ -0,0 +1,909 @@
<template>
<view class="student-scale-page">
<!-- 加载遮罩层 -->
<view v-if="loading" class="loading-mask">
<view class="loading-content">
<view class="loading-spinner"></view>
<text class="loading-text">数据加载中...</text>
</view>
</view>
<scroll-view
scroll-y
class="statistics-scroll"
:enable-back-to-top="true"
>
<!-- 1. 学生人数 -->
<view class="section">
<view class="section-header">
<text class="section-title">学生人数</text>
</view>
<view class="student-count-stats">
<view
class="stat-card stat-card-full clickable"
:style="{ backgroundColor: studentCountStats[0].bgColor }"
@click="goToStudentDetail('count', 'all', '总人数')"
>
<view class="card-icon" :style="{ backgroundColor: studentCountStats[0].iconBg }">
<text class="card-icon-text" :style="{ color: studentCountStats[0].color }">{{ studentCountStats[0].icon }}</text>
</view>
<view class="card-content">
<text class="card-label">{{ studentCountStats[0].label }}</text>
<text class="card-value" :style="{ color: studentCountStats[0].color }">{{ studentCountStats[0].value }}</text>
</view>
</view>
<view class="student-count-cards">
<view
class="stat-card clickable"
v-for="(item, index) in studentCountStats.slice(1)"
:key="index"
:style="{ backgroundColor: item.bgColor }"
@click="goToStudentDetail('count', item.type || 'all', item.label)"
>
<view class="card-icon" :style="{ backgroundColor: item.iconBg }">
<text class="card-icon-text" :style="{ color: item.color }">{{ item.icon }}</text>
</view>
<view class="card-content">
<text class="card-label">{{ item.label }}</text>
<text class="card-value" :style="{ color: item.color }">{{ item.value }}</text>
</view>
</view>
</view>
</view>
</view>
<!-- 2. 家长情况 -->
<view class="section">
<view class="section-header">
<text class="section-title">家长情况</text>
</view>
<view class="parent-content">
<view class="parent-chart-wrapper">
<QiunDataCharts
class="parent-chart"
canvas-id="parent-industry-ring"
type="ring"
:chart-data="parentIndustryChart"
:opts="parentIndustryChartOpts"
:animation="false"
:in-scroll-view="true"
/>
</view>
<view class="parent-side">
<view v-if="loading" class="parent-placeholder">加载中...</view>
<view v-else-if="!parentIndustryLegend.length" class="parent-placeholder">暂无数据</view>
<view v-else class="parent-legend">
<view
class="legend-item clickable"
v-for="(item, index) in parentIndustryLegend"
:key="index"
@click="goToParentDetail(item.industry)"
>
<view class="legend-info">
<view class="legend-left">
<view class="legend-dot" :style="{ backgroundColor: item.color }" />
<text class="legend-name-large">{{ item.label }}</text>
</view>
<view class="legend-right">
<view class="legend-value-group">
<text class="legend-count">{{ item.value }}</text>
<text class="legend-divider">|</text>
<text class="legend-percent">{{ item.percent }} ></text>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 3. 学生来源 -->
<view class="section">
<view class="section-header">
<text class="section-title">学生来源</text>
</view>
<view class="source-stats">
<view
class="stat-card clickable"
v-for="(item, index) in studentSourceStats"
:key="index"
:style="{ backgroundColor: item.bgColor }"
@click="goToStudentDetail('source', item.sourceCode, item.label)"
>
<view class="card-content">
<text class="card-label">{{ item.label }}</text>
<text class="card-value" :style="{ color: item.color }">{{ item.value }}</text>
</view>
</view>
</view>
</view>
<!-- 4. 学生状态 -->
<view class="section">
<view class="section-header">
<text class="section-title">学生状态</text>
</view>
<view class="status-stats">
<view
class="stat-card clickable"
v-for="(item, index) in studentStatusStats"
:key="index"
:style="{ backgroundColor: item.bgColor }"
@click="goToStudentDetail('status', item.statusCode, item.label)"
>
<view class="card-content">
<text class="card-label">{{ item.label }}</text>
<text class="card-value" :style="{ color: item.color }">{{ item.value }}</text>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
</template>
<script lang="ts" setup>
import { ref, computed, onMounted } from 'vue';
import QiunDataCharts from '@/components/charts/qiun-data-charts/components/qiun-data-charts/qiun-data-charts.vue';
import {
getStudentScaleDashboardApi,
} from '@/api/statistics/studentScaleApi';
//
const loading = ref(false);
// 1.
interface StudentCountStat {
label: string;
value: number;
type?: string;
icon: string;
color: string;
bgColor: string;
iconBg: string;
}
const studentCountStats = ref<StudentCountStat[]>([
{
label: '总人数',
value: 0,
icon: '👥',
color: '#4F46E5',
bgColor: '#EEF2FF',
iconBg: '#E0E7FF',
},
{
label: '已关注',
value: 0,
type: 'followed',
icon: '✓',
color: '#22C55E',
bgColor: '#F0FDF4',
iconBg: '#DCFCE7',
},
{
label: '未关注',
value: 0,
type: 'unfollowed',
icon: '✕',
color: '#EF4444',
bgColor: '#FEF2F2',
iconBg: '#FEE2E2',
}
]);
// 2.
const parentIndustryData = ref<Array<{industry: string; count: number}>>([]);
const parentTotalCount = computed(() => {
return parentIndustryData.value.reduce((sum, item) => sum + (item.count || 0), 0);
});
const parentIndustryChart = computed(() => {
const total = parentTotalCount.value;
const colors = [
'#0EA5E9', '#22C55E', '#F59E0B', '#EF4444', '#8B5CF6',
'#14B8A6', '#F97316', '#EC4899', '#6366F1', '#84CC16'
];
return {
series: [
{
name: '家长行业',
data: parentIndustryData.value.map((item, index) => {
const percent = total > 0 ? ((item.count / total) * 100).toFixed(1) : '0.0';
return {
name: item.industry,
value: item.count,
color: colors[index % colors.length],
labelText: `${percent}%`,
};
}),
},
],
};
});
interface ParentIndustryLegendItem {
label: string;
value: number;
percent: string;
industry: string;
color: string;
}
const parentIndustryLegend = computed<ParentIndustryLegendItem[]>(() => {
const colors = [
'#0EA5E9', '#22C55E', '#F59E0B', '#EF4444', '#8B5CF6',
'#14B8A6', '#F97316', '#EC4899', '#6366F1', '#84CC16'
];
const total = parentTotalCount.value;
return parentIndustryData.value.map((item, index) => {
const percent = total > 0 ? ((item.count / total) * 100).toFixed(1) : '0.0';
return {
label: item.industry,
value: item.count,
percent: `${percent}%`,
industry: item.industry,
color: colors[index % colors.length],
};
});
});
const parentIndustryChartOpts = computed(() => ({
height: 420,
rotate: false,
rotateLock: false,
color: ['#0EA5E9', '#22C55E', '#F59E0B', '#EF4444', '#8B5CF6',
'#14B8A6', '#F97316', '#EC4899', '#6366F1', '#84CC16'],
padding: [15, 20, 15, 15],
dataLabel: true,
enableScroll: false,
legend: {
show: false,
},
title: {
name: String(parentTotalCount.value || 0),
fontSize: 30,
color: '#3b82f6',
},
subtitle: {
name: '总人数',
fontSize: 12,
color: '#666666',
},
dataPointShape: false,
extra: {
ring: {
ringWidth: 15,
activeOpacity: 0.5,
activeRadius: 10,
offsetAngle: 0,
labelWidth: 22,
border: true,
borderWidth: 3,
borderColor: '#FFFFFF',
customLabel: true,
},
},
}));
// 3.
interface StudentSourceStat {
label: string;
value: number;
sourceCode: string;
icon: string;
color: string;
bgColor: string;
iconBg: string;
}
const studentSourceStats = ref<StudentSourceStat[]>([
{
label: '正常入学',
value: 0,
sourceCode: 'A',
icon: '📚',
color: '#22C55E',
bgColor: '#F0FDF4',
iconBg: '#DCFCE7',
},
{
label: '转入',
value: 0,
sourceCode: 'B',
icon: '🔄',
color: '#0EA5E9',
bgColor: '#F0F9FF',
iconBg: '#E0F2FE',
},
{
label: '复学',
value: 0,
sourceCode: 'C',
icon: '📖',
color: '#F59E0B',
bgColor: '#FFFBEB',
iconBg: '#FEF3C7',
}
]);
// 4.
interface StudentStatusStat {
label: string;
value: number;
statusCode: string;
icon: string;
color: string;
bgColor: string;
iconBg: string;
}
const studentStatusStats = ref<StudentStatusStat[]>([
{
label: '正常',
value: 0,
statusCode: 'A',
icon: '✓',
color: '#22C55E',
bgColor: '#F0FDF4',
iconBg: '#DCFCE7',
},
{
label: '转出',
value: 0,
statusCode: 'B',
icon: '➡️',
color: '#0EA5E9',
bgColor: '#F0F9FF',
iconBg: '#E0F2FE',
},
{
label: '休学',
value: 0,
statusCode: 'C',
icon: '⏸️',
color: '#F59E0B',
bgColor: '#FFFBEB',
iconBg: '#FEF3C7',
},
{
label: '辍学',
value: 0,
statusCode: 'E',
icon: '⚠️',
color: '#EF4444',
bgColor: '#FEF2F2',
iconBg: '#FEE2E2',
},
{
label: '死亡',
value: 0,
statusCode: 'G',
icon: '💔',
color: '#6B7280',
bgColor: '#F9FAFB',
iconBg: '#F3F4F6',
}
]);
//
const loadStatistics = async () => {
loading.value = true;
try {
//
const dashboardRes = await getStudentScaleDashboardApi();
if (dashboardRes?.resultCode === 1 && dashboardRes.result) {
const dashboard = dashboardRes.result;
// 1.
if (dashboard.studentCount) {
const data = dashboard.studentCount;
studentCountStats.value = [
{
label: '总人数',
value: data.totalCount || 0,
icon: '👥',
color: '#4F46E5',
bgColor: '#EEF2FF',
iconBg: '#E0E7FF',
},
{
label: '已关注',
value: data.followedCount || 0,
type: 'followed',
icon: '✓',
color: '#22C55E',
bgColor: '#F0FDF4',
iconBg: '#DCFCE7',
},
{
label: '未关注',
value: data.unfollowedCount || 0,
type: 'unfollowed',
icon: '✕',
color: '#EF4444',
bgColor: '#FEF2F2',
iconBg: '#FEE2E2',
}
];
}
// 2.
if (Array.isArray(dashboard.parentIndustry)) {
parentIndustryData.value = dashboard.parentIndustry.map((item: any) => ({
industry: item.industry || '未填写',
count: item.count || 0,
}));
}
// 3.
if (Array.isArray(dashboard.studentSource)) {
const sourceMap = new Map();
dashboard.studentSource.forEach((item: any) => {
sourceMap.set(item.sourceCode, item.count || 0);
});
studentSourceStats.value = [
{
label: '正常入学',
value: sourceMap.get('A') || 0,
sourceCode: 'A',
icon: '📚',
color: '#22C55E',
bgColor: '#F0FDF4',
iconBg: '#DCFCE7',
},
{
label: '转入',
value: sourceMap.get('B') || 0,
sourceCode: 'B',
icon: '🔄',
color: '#0EA5E9',
bgColor: '#F0F9FF',
iconBg: '#E0F2FE',
},
{
label: '复学',
value: sourceMap.get('C') || 0,
sourceCode: 'C',
icon: '📖',
color: '#F59E0B',
bgColor: '#FFFBEB',
iconBg: '#FEF3C7',
}
];
}
// 4.
if (Array.isArray(dashboard.studentStatus)) {
const statusMap = new Map();
dashboard.studentStatus.forEach((item: any) => {
statusMap.set(item.statusCode, item.count || 0);
});
studentStatusStats.value = [
{
label: '正常',
value: statusMap.get('A') || 0,
statusCode: 'A',
icon: '✓',
color: '#22C55E',
bgColor: '#F0FDF4',
iconBg: '#DCFCE7',
},
{
label: '转出',
value: statusMap.get('B') || 0,
statusCode: 'B',
icon: '➡️',
color: '#0EA5E9',
bgColor: '#F0F9FF',
iconBg: '#E0F2FE',
},
{
label: '休学',
value: statusMap.get('C') || 0,
statusCode: 'C',
icon: '⏸️',
color: '#F59E0B',
bgColor: '#FFFBEB',
iconBg: '#FEF3C7',
},
{
label: '辍学',
value: statusMap.get('E') || 0,
statusCode: 'E',
icon: '⚠️',
color: '#EF4444',
bgColor: '#FEF2F2',
iconBg: '#FEE2E2',
},
{
label: '死亡',
value: statusMap.get('G') || 0,
statusCode: 'G',
icon: '💔',
color: '#6B7280',
bgColor: '#F9FAFB',
iconBg: '#F3F4F6',
}
];
}
}
} catch (error) {
console.error('加载统计数据异常:', error);
uni.showToast({
title: '网络异常,请稍后重试',
icon: 'none',
duration: 2000
});
} finally {
loading.value = false;
}
};
//
const goToStudentDetail = (type: string, code: string, title: string) => {
uni.navigateTo({
url: `/pages/statistics/xs/gradeDetail?type=${type}&code=${code}&title=${encodeURIComponent(title)}`
});
};
//
const goToParentDetail = (industry: string) => {
uni.navigateTo({
url: `/pages/statistics/xs/parentDetail?industry=${encodeURIComponent(industry)}`
});
};
onMounted(() => {
loadStatistics();
});
</script>
<style scoped lang="scss">
.student-scale-page {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f8f9fa;
}
.statistics-scroll {
flex: 1;
padding: 10px 0 20px 0;
background-color: #f8f9fa;
}
.section {
background-color: #fff;
margin: 0 15px 15px;
border-radius: 12px;
padding: 15px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.section-header {
margin-bottom: 15px;
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 12px;
border-bottom: 1px solid #e5e7eb;
background: #fafbfc;
margin: -15px -15px 15px -15px;
padding: 15px;
.section-title {
font-size: 16px;
font-weight: 600;
color: #1f2933;
display: flex;
align-items: center;
position: relative;
padding-left: 12px;
&::before {
content: "";
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 4px;
height: 16px;
background-color: #3B82F6;
border-radius: 2px;
}
}
.section-subtitle {
font-size: 12px;
color: #999;
}
}
//
.student-count-stats {
display: flex;
flex-direction: column;
gap: 12px;
}
.stat-card-full {
width: 100%;
box-sizing: border-box;
.card-content {
max-width: calc(100% - 100px);
}
.card-value {
font-size: 24px !important;
}
}
.student-count-cards {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.stat-card {
display: flex;
align-items: center;
padding: 15px;
border-radius: 12px;
gap: 12px;
overflow: hidden;
box-sizing: border-box;
&.clickable {
cursor: pointer;
transition: all 0.3s ease;
&:active {
transform: scale(0.98);
opacity: 0.9;
}
}
.card-icon {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
.card-icon-text {
font-size: 24px;
}
}
.card-content {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
overflow: hidden;
.card-label {
font-size: 12px;
color: #666;
white-space: nowrap;
}
.card-value {
font-size: 20px;
font-weight: bold;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
//
.parent-content {
display: flex;
flex-direction: column;
gap: 20px;
padding: 15px;
}
.parent-chart-wrapper {
position: relative;
width: 100%;
max-width: 600rpx;
margin: 0 auto;
}
.parent-chart {
width: 100% !important;
height: 460rpx !important;
}
.parent-side {
width: 100%;
}
.parent-placeholder {
display: flex;
align-items: center;
justify-content: center;
min-height: 200px;
color: #999;
font-size: 14px;
}
.parent-legend {
display: flex;
flex-direction: column;
gap: 12px;
background: #f8faff;
border-radius: 12px;
padding: 15px;
}
.legend-item {
display: flex;
flex-direction: column;
gap: 8px;
padding: 12px;
border-radius: 8px;
background: #fff;
&.clickable {
cursor: pointer;
transition: all 0.3s ease;
&:active {
transform: scale(0.98);
background: rgba(59, 130, 246, 0.1);
}
}
}
.legend-info {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
}
.legend-left {
display: flex;
align-items: center;
gap: 12px;
flex: 1;
}
.legend-dot {
width: 12px;
height: 12px;
border-radius: 50%;
flex-shrink: 0;
}
.legend-name {
font-size: 14px;
color: #1f2933;
font-weight: 500;
}
.legend-name-large {
font-size: 15px;
color: #1f2933;
font-weight: 600;
}
.legend-right {
display: flex;
align-items: center;
gap: 12px;
font-size: 12px;
color: #4b5563;
margin-right: 8px;
}
.legend-value-group {
display: flex;
align-items: center;
gap: 8px;
}
.legend-count {
font-weight: 500;
color: #1f2933;
}
.legend-divider {
color: #d1d5db;
}
.legend-percent {
color: #2563eb;
font-weight: 500;
}
.legend-value {
font-size: 14px;
color: #3b82f6;
font-weight: 600;
}
//
.source-stats,
.status-stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
.stat-card {
justify-content: center;
.card-content {
align-items: center;
text-align: center;
}
}
}
//
.loading-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.loading-content {
background: #ffffff;
border-radius: 16rpx;
padding: 48rpx;
display: flex;
flex-direction: column;
align-items: center;
gap: 24rpx;
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.15);
}
.loading-spinner {
width: 60rpx;
height: 60rpx;
border: 4rpx solid #e5e7eb;
border-top-color: #3b82f6;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.loading-text {
font-size: 28rpx;
color: #6b7280;
font-weight: 500;
}
</style>

View File

@ -0,0 +1,178 @@
<!-- src/pages/statistics/xs/parentDetail.vue -->
<template>
<view class="parent-detail-page">
<!-- 页面头部 -->
<view class="page-header">
<text class="page-title">{{ industry }} - 家长明细</text>
<text class="refresh-btn" @click="refreshData">刷新</text>
</view>
<!-- 家长列表 -->
<view class="parent-list">
<view
class="parent-item"
v-for="parent in parentList"
:key="parent.jzId"
>
<view class="parent-main-info">
<text class="parent-name">{{ parent.jzxm }}</text>
<text class="parent-student">{{ parent.xsxm }}{{ parent.njmc }}{{ parent.bjmc }}</text>
<text class="parent-work" v-if="parent.gzdw">{{ parent.gzdw }}</text>
</view>
</view>
</view>
<!-- 空状态 -->
<view class="empty-state" v-if="parentList.length === 0 && !isLoading">
<text class="empty-text">暂无家长数据</text>
</view>
<!-- 加载状态 -->
<view class="loading-state" v-if="isLoading">
<text class="loading-text">加载中...</text>
</view>
</view>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { getParentListApi } from '@/api/statistics/studentScaleApi'
interface Parent {
jzId: string;
jzxm: string;
xsxm: string;
njmc: string;
bjmc: string;
gzdw?: string;
[key: string]: any;
}
const industry = ref('')
const parentList = ref<Parent[]>([])
const isLoading = ref(false)
//
const refreshData = async () => {
await loadParentData()
}
//
const loadParentData = async () => {
try {
isLoading.value = true
const result = await getParentListApi({
industry: industry.value
})
if (result.resultCode === 1 && result.result) {
parentList.value = Array.isArray(result.result) ? result.result : []
}
} catch (error) {
console.error('加载家长数据失败:', error)
uni.showToast({
title: '加载失败',
icon: 'error'
})
} finally {
isLoading.value = false
}
}
onLoad((options) => {
if (options.industry) {
industry.value = decodeURIComponent(options.industry)
loadParentData()
}
})
</script>
<style scoped lang="scss">
.parent-detail-page {
min-height: 100vh;
background-color: #f5f7fa;
padding: 12px;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
background-color: #fff;
border-radius: 12px;
padding: 16px;
margin-bottom: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.page-title {
font-size: 16px;
font-weight: 600;
color: #333;
}
.refresh-btn {
font-size: 14px;
color: #1890ff;
padding: 4px 8px;
border: 1px solid #1890ff;
border-radius: 4px;
}
.parent-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.parent-item {
background-color: #fff;
border-radius: 12px;
padding: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.parent-main-info {
display: flex;
flex-direction: column;
gap: 4px;
}
.parent-name {
font-size: 16px;
font-weight: 600;
color: #333;
}
.parent-student {
font-size: 14px;
color: #666;
}
.parent-work {
font-size: 12px;
color: #999;
margin-top: 4px;
}
.empty-state,
.loading-state {
display: flex;
justify-content: center;
align-items: center;
height: 200px;
background-color: #fff;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.empty-text,
.loading-text {
font-size: 14px;
color: #999;
}
</style>

View File

@ -0,0 +1,316 @@
<!-- src/pages/statistics/xs/studentList.vue -->
<template>
<view class="student-list-page">
<!-- 页面头部 -->
<view class="page-header">
<text class="page-title">{{ pageTitle }}</text>
<text class="refresh-btn" @click="refreshData">刷新</text>
</view>
<!-- 筛选信息 -->
<view class="filter-info" v-if="njmc || bjmc">
<text class="filter-text">{{ njmc }}{{ bjmc }}</text>
</view>
<!-- 学生列表 -->
<view class="student-list">
<view
class="student-item"
v-for="student in studentList"
:key="student.xsId"
@click="viewStudentDetail(student)"
>
<view class="student-main-info">
<text class="student-name">{{ student.xsxm }}</text>
<text class="student-class">{{ student.njmc }}{{ student.bjmc }}</text>
</view>
<view class="student-status" v-if="statType === 'count'">
<view class="status-tag" :class="getStatusClass(student)">
{{ getStatusText(student) }}
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view class="empty-state" v-if="studentList.length === 0 && !isLoading">
<text class="empty-text">暂无学生数据</text>
</view>
<!-- 加载状态 -->
<view class="loading-state" v-if="isLoading">
<text class="loading-text">加载中...</text>
</view>
</view>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { getStudentListApi } from '@/api/statistics/studentScaleApi'
import { useDataStore } from '@/store/modules/data'
interface Student {
xsId: string;
xsxm: string;
njmc: string;
bjmc: string;
jzxm?: string;
[key: string]: any;
}
const pageTitle = ref('')
const statType = ref('')
const statCode = ref('')
const subType = ref('')
const njId = ref('')
const njmc = ref('')
const bjId = ref('')
const bjmc = ref('')
const studentList = ref<Student[]>([])
const isLoading = ref(false)
const { setXs } = useDataStore()
//
const getStatusClass = (student: Student) => {
if (subType.value === 'followed') {
return 'completed'
} else if (subType.value === 'unfollowed') {
return 'unfollowed'
}
return ''
}
//
const getStatusText = (student: Student) => {
if (subType.value === 'followed') {
return '已关注'
} else if (subType.value === 'unfollowed') {
return '未关注'
}
return ''
}
//
const refreshData = async () => {
await loadStudentData()
}
//
const viewStudentDetail = (student: Student) => {
setXs({
xsId: student.xsId,
id: student.xsId,
xsxm: student.xsxm,
xm: student.xsxm,
njmc: student.njmc,
njmcName: student.njmc,
bjmc: student.bjmc,
jzxm: student.jzxm
})
uni.navigateTo({
url: '/pages/view/homeSchool/parentAddressBook/detail'
})
}
//
const loadStudentData = async () => {
try {
isLoading.value = true
const params: any = {
type: statType.value,
code: statCode.value
}
if (subType.value) {
params.subType = subType.value
}
if (njId.value) {
params.njId = njId.value
}
if (bjId.value) {
params.bjId = bjId.value
}
const result = await getStudentListApi(params)
if (result.resultCode === 1 && result.result) {
studentList.value = Array.isArray(result.result) ? result.result : []
}
} catch (error) {
console.error('加载学生数据失败:', error)
uni.showToast({
title: '加载失败',
icon: 'error'
})
} finally {
isLoading.value = false
}
}
onLoad((options) => {
if (options.type && options.code && options.title) {
statType.value = options.type
statCode.value = options.code
pageTitle.value = decodeURIComponent(options.title)
if (options.subType) {
subType.value = options.subType
}
if (options.njId) {
njId.value = options.njId
}
if (options.njmc) {
njmc.value = decodeURIComponent(options.njmc)
}
if (options.bjId) {
bjId.value = options.bjId
}
if (options.bjmc) {
bjmc.value = decodeURIComponent(options.bjmc)
}
loadStudentData()
}
})
</script>
<style scoped lang="scss">
.student-list-page {
min-height: 100vh;
background-color: #f5f7fa;
padding: 12px;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
background-color: #fff;
border-radius: 12px;
padding: 16px;
margin-bottom: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.page-title {
font-size: 16px;
font-weight: 600;
color: #333;
}
.refresh-btn {
font-size: 14px;
color: #1890ff;
padding: 4px 8px;
border: 1px solid #1890ff;
border-radius: 4px;
}
.filter-info {
background-color: #fff;
border-radius: 12px;
padding: 12px 16px;
margin-bottom: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.filter-text {
font-size: 14px;
color: #1890ff;
font-weight: 500;
}
.student-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.student-item {
background-color: #fff;
border-radius: 12px;
padding: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
transition: all 0.3s ease;
&:active {
transform: scale(0.98);
background-color: #f0f8ff;
}
}
.student-main-info {
flex: 1;
}
.student-name {
font-size: 16px;
font-weight: 600;
color: #333;
display: block;
margin-bottom: 4px;
}
.student-class {
font-size: 14px;
color: #666;
}
.student-status {
flex-shrink: 0;
}
.status-tag {
display: inline-flex;
align-items: center;
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
&.completed {
background-color: #e1f3d8;
color: #67c23a;
border: 1px solid #b3e19d;
}
&.unfollowed {
background-color: #fff7e6;
color: #faad14;
border: 1px solid #ffd591;
}
}
.empty-state,
.loading-state {
display: flex;
justify-content: center;
align-items: center;
height: 200px;
background-color: #fff;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.empty-text,
.loading-text {
font-size: 14px;
color: #999;
}
</style>

View File

@ -43,9 +43,9 @@
<!-- 表头 -->
<view class="table-header">
<view class="name-column header-cell">姓名</view>
<view class="scroll-column header-cell">班位置</view>
<view class="scroll-column header-cell">班行政</view>
<view class="scroll-column header-cell">班区域</view>
<view class="scroll-column header-cell">周点位</view>
<view class="scroll-column header-cell">周岗位</view>
<view class="scroll-column header-cell">周校区</view>
<view
v-for="date in pivotData.dateList"
:key="date"

View File

@ -22,40 +22,27 @@
v-model="formData.gzms"
class="form-textarea"
placeholder="请输入工作描述"
maxlength="500"
:show-count="true"
:maxlength="999999"
/>
</view>
<!-- 上传照片 -->
<!-- 上传照片和文档 -->
<view class="form-item">
<text class="form-label">上传照片</text>
<text class="form-label">上传照片和文档</text>
<view class="upload-section">
<view class="upload-list">
<view
v-for="(image, index) in imageList"
:key="index"
class="upload-item"
>
<image
:src="image.url ? imagUrl(image.url) : image.tempPath"
mode="aspectFill"
class="upload-image"
@click="previewImage(index)"
/>
<view class="upload-delete" @click="deleteImage(index)">
<text class="delete-icon">×</text>
</view>
</view>
<view
v-if="imageList.length < 9"
class="upload-add"
@click="chooseImage"
>
<text class="add-icon">+</text>
<text class="add-text">添加图片</text>
</view>
</view>
<ImageVideoUpload
v-model:image-list="imageList"
v-model:file-list="fileList"
:max-image-count="10"
:max-file-count="10"
:enable-video="false"
:enable-file="true"
:allowed-file-types="['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt']"
:compress-config="compressConfig"
:upload-api="attachmentUpload"
@image-upload-success="onImageUploadSuccess"
@file-upload-success="onFileUploadSuccess"
/>
</view>
</view>
@ -104,8 +91,8 @@ import { hcSaveApi } from "@/api/base/hcApi";
import { attachmentUpload } from "@/api/system/upload";
import { useUserStore } from "@/store/modules/user";
import { showLoading, hideLoading, showToast } from "@/utils/uniapp";
import { imagUrl } from "@/utils";
import { ref, onMounted } from "vue";
import { ImageVideoUpload, type ImageItem, type FileItem, COMPRESS_PRESETS } from '@/components/ImageVideoUpload';
const { getJs } = useUserStore();
@ -119,14 +106,13 @@ const formData = ref({
tjfj: '' //
});
// -
interface ImageItem {
tempPath?: string; //
url?: string; //
name?: string; //
}
//
const compressConfig = ref(COMPRESS_PRESETS.high);
//
const imageList = ref<ImageItem[]>([]);
//
const fileList = ref<FileItem[]>([]);
//
const submitting = ref(false);
@ -142,89 +128,13 @@ onMounted(() => {
formData.value.tjtime = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
});
//
const chooseImage = () => {
uni.chooseImage({
count: 9 - imageList.value.length,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: async (res) => {
//
const tempFilePaths = res.tempFilePaths as string[];
const newImages = tempFilePaths.map((path: string) => ({
tempPath: path,
name: path.split('/').pop() || 'image.jpg'
}));
imageList.value = [...imageList.value, ...newImages];
//
await uploadImages(newImages);
}
});
//
const onImageUploadSuccess = (image: ImageItem, index: number) => {
console.log('图片上传成功:', image, index);
};
//
const uploadImages = async (images: ImageItem[]) => {
try {
showLoading('上传图片中...');
for (let i = 0; i < images.length; i++) {
const image = images[i];
if (image.tempPath) {
try {
//
const uploadResult: any = await attachmentUpload(image.tempPath as any);
if (uploadResult && uploadResult.resultCode === 1 && uploadResult.result && uploadResult.result.length > 0) {
//
const serverPath = uploadResult.result[0].filePath;
//
const index = imageList.value.findIndex(img => img.tempPath === image.tempPath);
if (index !== -1) {
imageList.value[index].url = serverPath;
//
delete imageList.value[index].tempPath;
}
} else {
throw new Error('上传响应格式异常');
}
} catch (error) {
console.error('图片上传失败:', error);
showToast({ title: `${image.name || '图片'}上传失败`, icon: 'none' });
//
const index = imageList.value.findIndex(img => img.tempPath === image.tempPath);
if (index !== -1) {
imageList.value.splice(index, 1);
}
}
}
}
hideLoading();
showToast({ title: '图片上传完成', icon: 'success' });
} catch (error) {
hideLoading();
console.error('批量上传图片失败:', error);
showToast({ title: '图片上传失败,请重试', icon: 'none' });
}
};
//
const previewImage = (index: number) => {
const urls = imageList.value.map(img =>
img.url ? imagUrl(img.url) : img.tempPath
).filter((url): url is string => !!url);
uni.previewImage({
urls: urls,
current: index
});
};
//
const deleteImage = (index: number) => {
imageList.value.splice(index, 1);
const onFileUploadSuccess = (file: FileItem, index: number) => {
console.log('文档上传成功:', file, index);
};
//
@ -245,10 +155,11 @@ const handleSubmit = async () => {
return;
}
//
const hasUploadingImages = imageList.value.some(img => img.tempPath && !img.url);
if (hasUploadingImages) {
showToast({ title: '请等待图片上传完成', icon: 'none' });
//
const hasUploadingImages = imageList.value.some(img => !img.url);
const hasUploadingFiles = fileList.value.some(file => !file.url);
if (hasUploadingImages || hasUploadingFiles) {
showToast({ title: '请等待上传完成', icon: 'none' });
return;
}
@ -256,18 +167,26 @@ const handleSubmit = async () => {
showLoading('提交中...');
try {
//
//
let tjfj = '';
const uploadedImages = imageList.value.filter(img => img.url);
if (uploadedImages.length > 0) {
// 使 undefined
const imageUrls = uploadedImages.map(img => img.url).filter((url): url is string => !!url);
tjfj = imageUrls.join(',');
}
// wd
let wd = '';
const uploadedFiles = fileList.value.filter(file => file.url);
if (uploadedFiles.length > 0) {
const fileUrls = uploadedFiles.map(file => file.url).filter((url): url is string => !!url);
wd = fileUrls.join(',');
}
const submitData = {
...formData.value,
tjfj
tjfj,
wd
};
await hcSaveApi(submitData);
@ -384,72 +303,6 @@ const handleSubmit = async () => {
}
}
.upload-section {
.upload-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.upload-item {
position: relative;
width: 80px;
height: 80px;
border-radius: 8px;
overflow: hidden;
border: 1px solid #e9ecef;
.upload-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.upload-delete {
position: absolute;
top: 4px;
right: 4px;
width: 20px;
height: 20px;
background-color: rgba(0, 0, 0, 0.6);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
.delete-icon {
color: #ffffff;
font-size: 14px;
font-weight: bold;
}
}
}
.upload-add {
width: 80px;
height: 80px;
border: 2px dashed #e9ecef;
border-radius: 8px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: #f8f9fa;
cursor: pointer;
.add-icon {
font-size: 24px;
color: #6c757d;
margin-bottom: 4px;
}
.add-text {
font-size: 12px;
color: #6c757d;
}
}
}
.submit-section {
padding: 0 16px;

View File

@ -33,6 +33,29 @@
</view>
</view>
<!-- 文档展示 -->
<view v-if="fileList.length > 0" class="detail-section">
<text class="section-title">附件文档</text>
<view class="file-list">
<view
v-for="(file, index) in fileList"
:key="index"
class="file-item"
@click="downloadFile(file)"
>
<view class="file-icon">
<uni-icons type="paperclip" size="20" color="#4080ff"></uni-icons>
</view>
<view class="file-info">
<text class="file-name">{{ getFileName(file) }}</text>
</view>
<view class="file-action">
<uni-icons type="download" size="18" color="#999"></uni-icons>
</view>
</view>
</view>
</view>
<!-- 提交信息 -->
<view class="detail-section">
<text class="section-title">提交信息</text>
@ -75,6 +98,7 @@ interface DetailData {
jsxm: string;
tjtime: string;
tjfj: string;
wd: string;
}
//
@ -85,15 +109,18 @@ const detailData = ref<DetailData>({
jsId: '',
jsxm: '',
tjtime: '',
tjfj: ''
tjfj: '',
wd: ''
});
//
const imageList = ref<string[]>([]);
//
const fileList = ref<string[]>([]);
//
onLoad((options) => {
if (options.id) {
onLoad((options: any) => {
if (options && options.id) {
loadDetail(options.id);
}
});
@ -109,10 +136,20 @@ const loadDetail = async (id: string) => {
if (res && res.rows && res.rows.length > 0) {
detailData.value = res.rows[0];
//
// tjfj
if (detailData.value.tjfj) {
const images = detailData.value.tjfj.split(',').filter(img => img.trim());
imageList.value = images.map(img => imagUrl(img));
const imageUrls = detailData.value.tjfj.split(',').filter((url: string) => url.trim());
imageList.value = imageUrls.map((url: string) => imagUrl(url));
} else {
imageList.value = [];
}
// wd
if (detailData.value.wd) {
const fileUrls = detailData.value.wd.split(',').filter((url: string) => url.trim());
fileList.value = fileUrls;
} else {
fileList.value = [];
}
}
} catch (error) {
@ -139,6 +176,53 @@ const formatTime = (timeStr: string) => {
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
};
//
const getFileName = (url: string) => {
if (!url) return '未知文件';
const parts = url.split('/');
return parts[parts.length - 1] || '未知文件';
};
//
const formatFileSize = (bytes: number) => {
if (!bytes || bytes === 0) return '';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
};
//
const downloadFile = (url: string) => {
uni.downloadFile({
url: imagUrl(url),
success: (res) => {
if (res.statusCode === 200) {
uni.openDocument({
filePath: res.tempFilePath,
success: () => {
console.log('打开文档成功');
},
fail: (err) => {
console.error('打开文档失败:', err);
uni.showToast({
title: '无法打开该文件',
icon: 'none'
});
}
});
}
},
fail: (err) => {
console.error('下载文件失败:', err);
uni.showToast({
title: '下载失败',
icon: 'none'
});
}
});
};
//
const goBack = () => {
uni.navigateBack();
@ -285,4 +369,55 @@ const goBack = () => {
width: calc(50% - 4px);
}
}
.file-list {
.file-item {
display: flex;
align-items: center;
padding: 12px;
margin-bottom: 8px;
background-color: #f8f9fa;
border-radius: 8px;
border: 1px solid #e9ecef;
transition: all 0.3s ease;
&:active {
background-color: #e9ecef;
}
.file-icon {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
background-color: #e3f2fd;
border-radius: 6px;
margin-right: 12px;
}
.file-info {
flex: 1;
display: flex;
flex-direction: column;
.file-name {
font-size: 14px;
color: #2c3e50;
font-weight: 500;
margin-bottom: 4px;
word-break: break-all;
}
.file-size {
font-size: 12px;
color: #999;
}
}
.file-action {
margin-left: 8px;
}
}
}
</style>

View File

@ -22,40 +22,27 @@
v-model="formData.gzms"
class="form-textarea"
placeholder="请输入工作描述"
maxlength="500"
:show-count="true"
:maxlength="999999"
/>
</view>
<!-- 上传照片 -->
<!-- 上传照片和文档 -->
<view class="form-item">
<text class="form-label">上传照片</text>
<text class="form-label">上传照片和文档</text>
<view class="upload-section">
<view class="upload-list">
<view
v-for="(image, index) in imageList"
:key="index"
class="upload-item"
>
<image
:src="image.url ? imagUrl(image.url) : image.tempPath"
mode="aspectFill"
class="upload-image"
@click="previewImage(index)"
/>
<view class="upload-delete" @click="deleteImage(index)">
<text class="delete-icon">×</text>
</view>
</view>
<view
v-if="imageList.length < 9"
class="upload-add"
@click="chooseImage"
>
<text class="add-icon">+</text>
<text class="add-text">添加图片</text>
</view>
</view>
<ImageVideoUpload
v-model:image-list="imageList"
v-model:file-list="fileList"
:max-image-count="10"
:max-file-count="10"
:enable-video="false"
:enable-file="true"
:allowed-file-types="['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt']"
:compress-config="compressConfig"
:upload-api="attachmentUpload"
@image-upload-success="onImageUploadSuccess"
@file-upload-success="onFileUploadSuccess"
/>
</view>
</view>
@ -104,9 +91,9 @@ import { hcSaveApi, hcFindPageApi } from "@/api/base/hcApi";
import { attachmentUpload } from "@/api/system/upload";
import { useUserStore } from "@/store/modules/user";
import { showLoading, hideLoading, showToast } from "@/utils/uniapp";
import { imagUrl } from "@/utils";
import { ref, onMounted } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import { ImageVideoUpload, type ImageItem, type FileItem, COMPRESS_PRESETS } from '@/components/ImageVideoUpload';
const { getJs } = useUserStore();
@ -121,17 +108,17 @@ const formData = ref({
jsId: '', // ID
jsxm: '', //
tjtime: '', //
tjfj: '' //
tjfj: '', //
wd: '' //
});
// -
interface ImageItem {
tempPath?: string; //
url?: string; //
name?: string; //
}
//
const compressConfig = ref(COMPRESS_PRESETS.high);
//
const imageList = ref<ImageItem[]>([]);
//
const fileList = ref<FileItem[]>([]);
//
const submitting = ref(false);
@ -173,16 +160,40 @@ const loadData = async () => {
jsId: data.jsId || '',
jsxm: data.jsxm || '',
tjtime: data.tjtime ? data.tjtime.split(' ')[0] : '', //
tjfj: data.tjfj || ''
tjfj: data.tjfj || '',
wd: data.wd || ''
};
//
// tjfj
if (data.tjfj) {
const imageUrls = data.tjfj.split(',').filter((url: string) => url.trim());
imageList.value = imageUrls.map((url: string) => ({
url: url.trim(),
name: url.split('/').pop() || 'image.jpg'
}));
imageList.value = imageUrls.map((url: string) => {
const trimmedUrl = url.trim();
const fileName = trimmedUrl.split('/').pop() || 'image.jpg';
return {
url: trimmedUrl,
name: fileName,
originalName: fileName
};
});
} else {
imageList.value = [];
}
// wd
if (data.wd) {
const fileUrls = data.wd.split(',').filter((url: string) => url.trim());
fileList.value = fileUrls.map((url: string) => {
const trimmedUrl = url.trim();
const fileName = trimmedUrl.split('/').pop() || 'file';
return {
url: trimmedUrl,
name: fileName,
originalName: fileName
};
});
} else {
fileList.value = [];
}
} else {
throw new Error('获取数据失败');
@ -205,89 +216,13 @@ onMounted(() => {
formData.value.jsxm = js.jsxm;
});
//
const chooseImage = () => {
uni.chooseImage({
count: 9 - imageList.value.length,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: async (res) => {
//
const tempFilePaths = res.tempFilePaths as string[];
const newImages = tempFilePaths.map((path: string) => ({
tempPath: path,
name: path.split('/').pop() || 'image.jpg'
}));
imageList.value = [...imageList.value, ...newImages];
//
await uploadImages(newImages);
}
});
//
const onImageUploadSuccess = (image: ImageItem, index: number) => {
console.log('图片上传成功:', image, index);
};
//
const uploadImages = async (images: ImageItem[]) => {
try {
showLoading('上传图片中...');
for (let i = 0; i < images.length; i++) {
const image = images[i];
if (image.tempPath) {
try {
//
const uploadResult: any = await attachmentUpload(image.tempPath as any);
if (uploadResult && uploadResult.resultCode === 1 && uploadResult.result && uploadResult.result.length > 0) {
//
const serverPath = uploadResult.result[0].filePath;
//
const index = imageList.value.findIndex(img => img.tempPath === image.tempPath);
if (index !== -1) {
imageList.value[index].url = serverPath;
//
delete imageList.value[index].tempPath;
}
} else {
throw new Error('上传响应格式异常');
}
} catch (error) {
console.error('图片上传失败:', error);
showToast({ title: `${image.name || '图片'}上传失败`, icon: 'none' });
//
const index = imageList.value.findIndex(img => img.tempPath === image.tempPath);
if (index !== -1) {
imageList.value.splice(index, 1);
}
}
}
}
hideLoading();
showToast({ title: '图片上传完成', icon: 'success' });
} catch (error) {
hideLoading();
console.error('批量上传图片失败:', error);
showToast({ title: '图片上传失败,请重试', icon: 'none' });
}
};
//
const previewImage = (index: number) => {
const urls = imageList.value.map(img =>
img.url ? imagUrl(img.url) : img.tempPath
).filter((url): url is string => !!url);
uni.previewImage({
urls: urls,
current: index
});
};
//
const deleteImage = (index: number) => {
imageList.value.splice(index, 1);
const onFileUploadSuccess = (file: FileItem, index: number) => {
console.log('文档上传成功:', file, index);
};
//
@ -308,10 +243,11 @@ const handleSubmit = async () => {
return;
}
//
const hasUploadingImages = imageList.value.some(img => img.tempPath && !img.url);
if (hasUploadingImages) {
showToast({ title: '请等待图片上传完成', icon: 'none' });
//
const hasUploadingImages = imageList.value.some(img => !img.url);
const hasUploadingFiles = fileList.value.some(file => !file.url);
if (hasUploadingImages || hasUploadingFiles) {
showToast({ title: '请等待上传完成', icon: 'none' });
return;
}
@ -319,18 +255,26 @@ const handleSubmit = async () => {
showLoading('提交中...');
try {
//
//
let tjfj = '';
const uploadedImages = imageList.value.filter(img => img.url);
if (uploadedImages.length > 0) {
// 使 undefined
const imageUrls = uploadedImages.map(img => img.url).filter((url): url is string => !!url);
tjfj = imageUrls.join(',');
}
// wd
let wd = '';
const uploadedFiles = fileList.value.filter(file => file.url);
if (uploadedFiles.length > 0) {
const fileUrls = uploadedFiles.map(file => file.url).filter((url): url is string => !!url);
wd = fileUrls.join(',');
}
const submitData = {
...formData.value,
tjfj
tjfj,
wd
};
await hcSaveApi(submitData);
@ -448,69 +392,7 @@ const handleSubmit = async () => {
}
.upload-section {
.upload-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.upload-item {
position: relative;
width: 80px;
height: 80px;
border-radius: 8px;
overflow: hidden;
border: 1px solid #e9ecef;
.upload-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.upload-delete {
position: absolute;
top: 4px;
right: 4px;
width: 20px;
height: 20px;
background-color: rgba(0, 0, 0, 0.6);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
.delete-icon {
color: #ffffff;
font-size: 14px;
font-weight: bold;
}
}
}
.upload-add {
width: 80px;
height: 80px;
border: 2px dashed #e9ecef;
border-radius: 8px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: #f8f9fa;
cursor: pointer;
.add-icon {
font-size: 24px;
color: #6c757d;
margin-bottom: 4px;
}
.add-text {
font-size: 12px;
color: #6c757d;
}
}
// ImageVideoUpload
}
.submit-section {

View File

@ -154,19 +154,24 @@ interface QdItem {
qdry: string; //
}
//
const getCurrentMonthRange = () => {
//
const getCurrentWeekRange = () => {
const now = new Date();
const year = now.getFullYear();
const month = now.getMonth();
const day = now.getDay(); // 0-6, 0
const diff = day === 0 ? -6 : 1 - day; // 6
//
const firstDay = new Date(year, month, 1);
const startTime = `${year}-${String(month + 1).padStart(2, '0')}-01`;
//
const firstDay = new Date(now);
firstDay.setDate(now.getDate() + diff);
firstDay.setHours(0, 0, 0, 0);
//
const lastDay = new Date(year, month + 1, 0);
const endTime = `${year}-${String(month + 1).padStart(2, '0')}-${String(lastDay.getDate()).padStart(2, '0')}`;
//
const lastDay = new Date(firstDay);
lastDay.setDate(firstDay.getDate() + 6);
lastDay.setHours(23, 59, 59, 999);
const startTime = `${firstDay.getFullYear()}-${String(firstDay.getMonth() + 1).padStart(2, '0')}-${String(firstDay.getDate()).padStart(2, '0')}`;
const endTime = `${lastDay.getFullYear()}-${String(lastDay.getMonth() + 1).padStart(2, '0')}-${String(lastDay.getDate()).padStart(2, '0')}`;
return { startTime, endTime };
};
@ -177,7 +182,7 @@ const pageParams = ref({
});
//
const defaultRange = getCurrentMonthRange();
const defaultRange = getCurrentWeekRange();
//
const searchForm = reactive({
@ -209,7 +214,7 @@ const handleSearch = () => {
//
const handleReset = () => {
const resetRange = getCurrentMonthRange();
const resetRange = getCurrentWeekRange();
searchForm.startTime = resetRange.startTime;
searchForm.endTime = resetRange.endTime;
getQdList();

View File

@ -575,11 +575,14 @@ onLoad((options: any) => {
if (options.qdlx) {
formData.qdlx = options.qdlx;
console.log('新增签到页面接收到qdlx参数:', options.qdlx);
// ysycxkjy/
if (options.qdlx === 'ysyc' || options.qdlx === 'xkjy') {
loadCourseList();
}
} else {
//
formData.qdlx = 'cghy';
console.log('未收到qdlx参数默认设置为 cghy');
}
});
@ -731,7 +734,7 @@ const handlePublish = async () => {
qdmc: formData.qdmc,
qdwz: formData.qdwz,
qdry: uniqueIds.join(','), // 使ID
qdlx: formData.qdlx, //
qdlx: formData.qdlx || 'cghy', //
mdqz: formData.mdqz,
qdkstime: formatDateTime(formData.qdkstime),
qdjstime: formatDateTime(formData.qdjstime),

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -22,6 +22,7 @@ interface TeacherAnalyticsState {
position: TeacherDistributionItem[];
political: TeacherDistributionItem[];
workingYears: TeacherDistributionItem[];
zdqk: TeacherDistributionItem[];
};
list: TeacherAnalyticsListItem[];
pagination: {
@ -43,6 +44,7 @@ const defaultDistributions = {
position: [] as TeacherDistributionItem[],
political: [] as TeacherDistributionItem[],
workingYears: [] as TeacherDistributionItem[],
zdqk: [] as TeacherDistributionItem[],
};
export const useTeacherAnalyticsStore = defineStore("teacherAnalytics", {
@ -124,6 +126,7 @@ export const useTeacherAnalyticsStore = defineStore("teacherAnalytics", {
position: data.position || [],
political: data.political || [],
workingYears: data.workingYears || [],
zdqk: data.zdqk || [],
};
} else {
this.composition = [];