统计调整
This commit is contained in:
parent
6839157a49
commit
6161a2fa8d
@ -34,3 +34,6 @@ export const findAllZw = () => {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
101
src/api/statistics/studentScaleApi.ts
Normal file
101
src/api/statistics/studentScaleApi.ts
Normal 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);
|
||||
};
|
||||
|
||||
@ -48,6 +48,7 @@ export interface TeacherAnalyticsDashboardResponse {
|
||||
position?: TeacherDistributionItem[];
|
||||
political?: TeacherDistributionItem[];
|
||||
workingYears?: TeacherDistributionItem[];
|
||||
zdqk?: TeacherDistributionItem[];
|
||||
}
|
||||
|
||||
export interface TeacherAnalyticsListRequest extends TeacherAnalyticsFilter {
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
280
src/pages/statistics/xs/classDetail.vue
Normal file
280
src/pages/statistics/xs/classDetail.vue
Normal 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>
|
||||
|
||||
311
src/pages/statistics/xs/gradeDetail.vue
Normal file
311
src/pages/statistics/xs/gradeDetail.vue
Normal 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>
|
||||
|
||||
909
src/pages/statistics/xs/index.vue
Normal file
909
src/pages/statistics/xs/index.vue
Normal 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>
|
||||
|
||||
178
src/pages/statistics/xs/parentDetail.vue
Normal file
178
src/pages/statistics/xs/parentDetail.vue
Normal 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>
|
||||
|
||||
316
src/pages/statistics/xs/studentList.vue
Normal file
316
src/pages/statistics/xs/studentList.vue
Normal 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>
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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>
|
||||
@ -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 {
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -575,11 +575,14 @@ onLoad((options: any) => {
|
||||
if (options.qdlx) {
|
||||
formData.qdlx = options.qdlx;
|
||||
console.log('新增签到页面接收到qdlx参数:', options.qdlx);
|
||||
|
||||
// 如果是ysyc或xkjy类型,加载课程/教研组列表
|
||||
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),
|
||||
|
||||
BIN
src/static/base/home/xsgm.png
Normal file
BIN
src/static/base/home/xsgm.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
@ -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 = [];
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user