diff --git a/mobile-uni/src/components/StateBlock.vue b/mobile-uni/src/components/StateBlock.vue
new file mode 100644
index 00000000..aa266890
--- /dev/null
+++ b/mobile-uni/src/components/StateBlock.vue
@@ -0,0 +1,253 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ title || '数据加载中…' }}
+
+
+
+
+
+
+
+
+
+ {{ title || defaultTitle }}
+ {{ desc || defaultDesc }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mobile-uni/src/pages/supervisor/assign/index.vue b/mobile-uni/src/pages/supervisor/assign/index.vue
index 4d88e0bd..d632573a 100644
--- a/mobile-uni/src/pages/supervisor/assign/index.vue
+++ b/mobile-uni/src/pages/supervisor/assign/index.vue
@@ -10,14 +10,14 @@
{{ message }}
-
- 无主管权限
- 当前账号不是主管或项目经理,无法查看团队训练数据。
-
-
-
-
-
+
@@ -189,6 +189,7 @@ import { clearAuth, getAuth, isLoggedIn, rememberLoginRedirect } from '@/service
import { resetPageScroll } from '@/services/navigation';
import { createPracticeAssignmentsBatch, getPracticeAlerts, getPracticeScenarios, getPracticeTeamSnapshot } from '@/services/practice';
import SupervisorTabBar from '@/components/SupervisorTabBar.vue';
+import StateBlock from '@/components/StateBlock.vue';
import {
createAsyncGenerationGuard,
inferSupervisorScenarioId,
diff --git a/mobile-uni/src/pages/supervisor/assign/result.vue b/mobile-uni/src/pages/supervisor/assign/result.vue
index 3481a9ce..5fd94400 100644
--- a/mobile-uni/src/pages/supervisor/assign/result.vue
+++ b/mobile-uni/src/pages/supervisor/assign/result.vue
@@ -80,10 +80,14 @@
-
- 暂无本次派发结果,请先在指派页完成派发。
-
-
+
@@ -94,6 +98,7 @@ import { computed, ref } from 'vue';
import { onShow } from '@dcloudio/uni-app';
import { resetPageScroll } from '@/services/navigation';
import SupervisorTabBar from '@/components/SupervisorTabBar.vue';
+import StateBlock from '@/components/StateBlock.vue';
interface AssignResultItem {
extPartyId: string;
@@ -451,14 +456,4 @@ onShow(() => {
background: #ffffff;
color: var(--brand);
}
-
-.empty-panel {
- margin-top: 16px;
- text-align: center;
-}
-
-.empty-action {
- width: 100%;
- margin-top: 14px;
-}
diff --git a/mobile-uni/src/pages/supervisor/exams/index.vue b/mobile-uni/src/pages/supervisor/exams/index.vue
index bd918d24..28c9c8e0 100644
--- a/mobile-uni/src/pages/supervisor/exams/index.vue
+++ b/mobile-uni/src/pages/supervisor/exams/index.vue
@@ -12,7 +12,7 @@
+
新建考试
- {{ message }}
+ {{ message }}
@@ -40,7 +40,8 @@
>{{ tab.label }}
- 考试列表加载中…
+
+
@@ -63,7 +64,7 @@
- {{ emptyLabel }}
+
@@ -75,6 +76,7 @@ import { onShow } from '@dcloudio/uni-app';
import type { ExamSummary } from '@/types/api';
import { listSupervisorExams } from '@/services/exam';
import SupervisorTabBar from '@/components/SupervisorTabBar.vue';
+import StateBlock from '@/components/StateBlock.vue';
type ExamStateKey = 'active' | 'draft' | 'ended';
@@ -111,6 +113,7 @@ const filteredExams = computed(() => (statusFilter.value === 'all'
: exams.value.filter((item) => examState(item).key === statusFilter.value)));
const emptyLabel = computed(() => (statusFilter.value === 'all' ? '暂无考试,先创建岗位题目并派发。' : '当前状态下暂无考试'));
+const loadFailed = computed(() => !loading.value && !!message.value && !exams.value.length);
const dueDateLabel = (value?: string | null) => {
const due = value?.slice(0, 10) || '';
diff --git a/mobile-uni/src/pages/supervisor/exams/results.vue b/mobile-uni/src/pages/supervisor/exams/results.vue
index af77d7ee..ed389a43 100644
--- a/mobile-uni/src/pages/supervisor/exams/results.vue
+++ b/mobile-uni/src/pages/supervisor/exams/results.vue
@@ -9,7 +9,7 @@
- {{ message }}
+ {{ message }}
{{ examTitle }}
@@ -47,7 +47,9 @@
>{{ tab.label }}
-
+
+
+
@@ -60,9 +62,7 @@
{{ statusLabel(item) }}
-
- 暂无符合筛选的派发结果
-
+
@@ -73,6 +73,7 @@ import { onLoad } from '@dcloudio/uni-app';
import type { ExamResultItem } from '@/types/api';
import { getSupervisorExam, getSupervisorExamResults } from '@/services/exam';
import SupervisorTabBar from '@/components/SupervisorTabBar.vue';
+import StateBlock from '@/components/StateBlock.vue';
type ResultFilter = 'all' | 'passed' | 'failed' | 'pending';
@@ -103,6 +104,8 @@ const filteredResults = computed(() => results.value.filter((item) => {
if (resultFilter.value === 'pending') return isPending(item);
return true;
}));
+const loadFailed = computed(() => !loading.value && !!message.value && !results.value.length);
+const firstLoading = computed(() => loading.value && !results.value.length && !message.value);
const statusLabel = (item: ExamResultItem) => (isPending(item) ? '未完成' : isPassed(item) ? '通过' : '未通过');
const statusClass = (item: ExamResultItem) => (isPending(item) ? 'pending' : isPassed(item) ? 'passed' : 'failed');
@@ -386,8 +389,4 @@ onLoad((options) => {
background: #f1f5f9;
color: #64748b;
}
-
-.empty-panel {
- margin-top: 12px;
-}
diff --git a/mobile-uni/src/pages/supervisor/index/index.vue b/mobile-uni/src/pages/supervisor/index/index.vue
index a655b467..88ce14e5 100644
--- a/mobile-uni/src/pages/supervisor/index/index.vue
+++ b/mobile-uni/src/pages/supervisor/index/index.vue
@@ -22,16 +22,16 @@
- {{ message }}
+ {{ message }}
-
- 无主管权限
- 当前账号不是主管或项目经理,无法查看团队训练数据。
-
-
-
-
-
+
@@ -41,6 +41,10 @@
+
+
+
+
@@ -148,6 +152,7 @@ import { getPracticeAlerts, getPracticeReviews, getPracticeTeamSnapshot } from '
import { getMobileHome } from '@/services/home';
import { apiRequest } from '@/services/api';
import SupervisorTabBar from '@/components/SupervisorTabBar.vue';
+import StateBlock from '@/components/StateBlock.vue';
import {
buildSupervisorMetrics,
buildTeamProfiles,
@@ -195,6 +200,13 @@ const completionRate = computed(() =>
teamTotal.value ? Math.min(100, Math.round((todayDone.value / teamTotal.value) * 100)) : 0);
const attentionCount = computed(() =>
teamProfiles.value.filter((item) => item.risk === 'high' || item.risk === 'medium').length);
+const hasData = computed(() =>
+ members.value.length > 0
+ || records.value.length > 0
+ || assignments.value.length > 0
+ || pendingReviews.value.length > 0);
+const loadFailed = computed(() => !loading.value && !!message.value && !hasData.value);
+const firstLoading = computed(() => loading.value && !hasData.value && !message.value);
interface TodoRow {
key: string;
@@ -907,21 +919,4 @@ onUnload(invalidateRequests);
font-size: 14px;
line-height: 1.5;
}
-
-.supervisor-access-panel {
- padding: 18px;
-}
-
-.supervisor-access-copy {
- display: block;
- margin-top: 8px;
- font-size: 14px;
- line-height: 1.5;
-}
-
-.supervisor-access-actions {
- display: flex;
- gap: 10px;
- margin-top: 14px;
-}
diff --git a/mobile-uni/src/pages/supervisor/member/index.vue b/mobile-uni/src/pages/supervisor/member/index.vue
index 668a540e..c7f7b459 100644
--- a/mobile-uni/src/pages/supervisor/member/index.vue
+++ b/mobile-uni/src/pages/supervisor/member/index.vue
@@ -11,16 +11,16 @@
- {{ message }}
+ {{ message }}
-
- 无主管权限
- 当前账号不是主管或项目经理,无法查看成员训练数据。
-
-
-
-
-
+
@@ -30,6 +30,10 @@
+
+
+
+
@@ -128,6 +132,7 @@ import { clearAuth, isLoggedIn, rememberLoginRedirect } from '@/services/auth';
import { resetPageScroll } from '@/services/navigation';
import { getPracticeTeamSnapshot } from '@/services/practice';
import SupervisorTabBar from '@/components/SupervisorTabBar.vue';
+import StateBlock from '@/components/StateBlock.vue';
import {
buildTeamProfiles,
inferSupervisorScenarioId,
@@ -189,6 +194,9 @@ const lowScoreScene = computed(() => {
return '';
});
const memberInitial = computed(() => displayName.value.trim().charAt(0) || '员');
+const hasData = computed(() => records.value.length > 0 || assignments.value.length > 0);
+const loadFailed = computed(() => !loading.value && !!message.value && !hasData.value);
+const firstLoading = computed(() => loading.value && !hasData.value && !message.value);
const riskBadgeText = computed(() => {
const risk = profile.value?.risk;
return risk === 'high' ? '高风险' : risk === 'medium' ? '待复盘' : '正常';
diff --git a/mobile-uni/src/pages/supervisor/review/done.vue b/mobile-uni/src/pages/supervisor/review/done.vue
index 7d789ce8..836667a8 100644
--- a/mobile-uni/src/pages/supervisor/review/done.vue
+++ b/mobile-uni/src/pages/supervisor/review/done.vue
@@ -8,7 +8,7 @@
- {{ message }}
+ {{ message }}
@@ -75,10 +75,17 @@
-
- 复盘结果暂不可用
-
-
+
+
+
+
+
@@ -91,6 +98,7 @@ import { isLoggedIn, rememberLoginRedirect } from '@/services/auth';
import { resetPageScroll } from '@/services/navigation';
import { getPracticeAssignments, getPracticeReviewDetail, getPracticeTeamSnapshot } from '@/services/practice';
import SupervisorTabBar from '@/components/SupervisorTabBar.vue';
+import StateBlock from '@/components/StateBlock.vue';
const basePath = '/pages/supervisor/review/done';
const reviewId = ref(0);
@@ -105,6 +113,8 @@ const weekNames = ['周日', '周一', '周二', '周三', '周四', '周五', '
const now = new Date();
const doneTimeLabel = `今天 ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')} · ${now.getMonth() + 1}月${now.getDate()}日 ${weekNames[now.getDay()]}`;
+const loadFailed = computed(() => !loading.value && !!message.value && !detail.value);
+
const displayName = computed(() => {
const identity = (detail.value?.extPartyId || '').trim();
const mapped = identity ? memberNames.value[identity] : '';
@@ -427,12 +437,4 @@ onShow(() => {
background: #ffffff;
color: #3a3d44;
}
-
-.empty-panel {
- padding-bottom: 18px;
-}
-
-.back-list-button {
- width: 100%;
-}
diff --git a/mobile-uni/src/pages/supervisor/review/index.vue b/mobile-uni/src/pages/supervisor/review/index.vue
index 6a960b2b..ed3ed3f2 100644
--- a/mobile-uni/src/pages/supervisor/review/index.vue
+++ b/mobile-uni/src/pages/supervisor/review/index.vue
@@ -11,16 +11,16 @@
- {{ message }}
+ {{ message }}
-
- 无主管权限
- 当前账号不是主管或项目经理,无法查看团队训练数据。
-
-
-
-
-
+
@@ -30,6 +30,10 @@
+
+
+
+
@@ -206,10 +210,13 @@
-
- {{ reviewId ? '复盘详情暂不可用' : '缺少有效的复盘ID' }}
-
-
+
@@ -223,6 +230,7 @@ import { clearAuth, isLoggedIn, rememberLoginRedirect } from '@/services/auth';
import { resetPageScroll } from '@/services/navigation';
import { getPracticeReviewDetail, getPracticeScenarios, getPracticeTeamSnapshot, markPracticeReviewed } from '@/services/practice';
import SupervisorTabBar from '@/components/SupervisorTabBar.vue';
+import StateBlock from '@/components/StateBlock.vue';
import {
createAsyncGenerationGuard,
isReviewEditable,
@@ -251,6 +259,8 @@ const audioErrors = ref>({});
const audioRequests = createAsyncGenerationGuard();
const memberNames = ref>({});
const editable = computed(() => isReviewEditable(detail.value?.status));
+const loadFailed = computed(() => !loading.value && !!message.value && !detail.value);
+const firstLoading = computed(() => loading.value && !detail.value && !message.value);
const displayName = computed(() => {
const identity = (detail.value?.extPartyId || '').trim();
@@ -588,19 +598,6 @@ onBeforeUnmount(() => {
line-height: 1.5;
}
-.supervisor-access-copy {
- display: block;
- margin-top: 8px;
- font-size: 14px;
- line-height: 1.5;
-}
-
-.supervisor-access-actions {
- display: flex;
- gap: 10px;
- margin-top: 14px;
-}
-
.panel-head {
display: flex;
align-items: center;
@@ -1161,12 +1158,4 @@ onBeforeUnmount(() => {
font-size: 13px;
line-height: 1.5;
}
-
-.empty-panel {
- padding-bottom: 18px;
-}
-
-.back-list-button {
- width: 100%;
-}
diff --git a/mobile-uni/src/pages/supervisor/reviews/index.vue b/mobile-uni/src/pages/supervisor/reviews/index.vue
index 438856ae..34a40bb4 100644
--- a/mobile-uni/src/pages/supervisor/reviews/index.vue
+++ b/mobile-uni/src/pages/supervisor/reviews/index.vue
@@ -11,16 +11,16 @@
- {{ message }}
+ {{ message }}
-
- 无主管权限
- 当前账号不是主管或项目经理,无法查看团队训练数据。
-
-
-
-
-
+
@@ -30,6 +30,10 @@
+
+
+
+
@@ -99,6 +103,7 @@ import { clearAuth, isLoggedIn, rememberLoginRedirect } from '@/services/auth';
import { resetPageScroll } from '@/services/navigation';
import { getPracticeAlerts, getPracticeReviews, getPracticeTeamSnapshot } from '@/services/practice';
import SupervisorTabBar from '@/components/SupervisorTabBar.vue';
+import StateBlock from '@/components/StateBlock.vue';
import { buildSupervisorMetrics, isSupervisorAccessDenied } from '@/services/supervisor-view';
type FilterKey = 'all' | 'below60' | 'repeatLow';
@@ -186,6 +191,8 @@ const displayedReviews = computed(() => {
? a.record.score - b.record.score
: b.record.time.localeCompare(a.record.time));
});
+const loadFailed = computed(() => !loading.value && !!message.value && !reviews.value.length);
+const firstLoading = computed(() => loading.value && !reviews.value.length && !message.value);
const clearData = () => {
reviews.value = [];
@@ -358,23 +365,6 @@ onShow(() => {
line-height: 1.5;
}
-.supervisor-access-panel {
- padding: 18px;
-}
-
-.supervisor-access-copy {
- display: block;
- margin-top: 8px;
- font-size: 14px;
- line-height: 1.5;
-}
-
-.supervisor-access-actions {
- display: flex;
- gap: 10px;
- margin-top: 14px;
-}
-
/* 筛选与排序 */
.filter-bar {
display: flex;
diff --git a/mobile-uni/src/pages/supervisor/team/index.vue b/mobile-uni/src/pages/supervisor/team/index.vue
index 52204cd8..eb358dc7 100644
--- a/mobile-uni/src/pages/supervisor/team/index.vue
+++ b/mobile-uni/src/pages/supervisor/team/index.vue
@@ -11,16 +11,16 @@
- {{ message }}
+ {{ message }}
-
- 无主管权限
- 当前账号不是主管或项目经理,无法查看团队训练数据。
-
-
-
-
-
+
@@ -30,6 +30,10 @@
+
+
+
+
@@ -102,7 +106,7 @@
›
- 暂无团队画像数据
+
@@ -117,6 +121,7 @@ import { clearAuth, isLoggedIn, rememberLoginRedirect } from '@/services/auth';
import { resetPageScroll } from '@/services/navigation';
import { assignPracticeDifficulty, getPracticeDifficulties, getPracticeTeamSnapshot } from '@/services/practice';
import SupervisorTabBar from '@/components/SupervisorTabBar.vue';
+import StateBlock from '@/components/StateBlock.vue';
import {
buildTeamProfiles,
inferSupervisorScenarioId,
@@ -158,6 +163,9 @@ const sortedDifficulties = computed(() => [...mistakes.value].sort((a, b) => {
if (difficultySort.value === 'score') return a.averageScore - b.averageScore;
return b.count - a.count;
}));
+const hasData = computed(() => teamProfiles.value.length > 0 || mistakes.value.length > 0);
+const loadFailed = computed(() => !loading.value && !!message.value && !hasData.value);
+const firstLoading = computed(() => loading.value && !hasData.value && !message.value);
const clearData = () => {
records.value = [];
diff --git a/mobile-uni/src/pages/user/cases/index.vue b/mobile-uni/src/pages/user/cases/index.vue
index 44af7acf..8af4cca4 100644
--- a/mobile-uni/src/pages/user/cases/index.vue
+++ b/mobile-uni/src/pages/user/cases/index.vue
@@ -101,7 +101,7 @@
- 暂无入库案例
+
@@ -147,6 +147,7 @@ import { releaseAuthenticatedFileUrl } from '@/services/api';
import { isLoggedIn, rememberLoginRedirect } from '@/services/auth';
import { navigateBackToToday, resetPageScroll } from '@/services/navigation';
import { ensureEmployeePosition } from '@/services/position';
+import StateBlock from '@/components/StateBlock.vue';
const loggedIn = ref(false);
const selectedFile = ref(null);
diff --git a/mobile-uni/src/pages/user/history/index.vue b/mobile-uni/src/pages/user/history/index.vue
index 7befe66b..c702aef7 100644
--- a/mobile-uni/src/pages/user/history/index.vue
+++ b/mobile-uni/src/pages/user/history/index.vue
@@ -71,10 +71,13 @@
-
- {{ message || '训练记录不存在或不属于当前账号' }}
-
-
+
@@ -86,6 +89,7 @@ import { authenticatedFileUrl, releaseAuthenticatedFileUrl } from '@/services/ap
import { isLoggedIn, rememberLoginRedirect } from '@/services/auth';
import { getPracticeHistoryDetail } from '@/services/practice';
import { resetPageScroll } from '@/services/navigation';
+import StateBlock from '@/components/StateBlock.vue';
const detail = ref(null);
const reviewId = ref(0);
@@ -335,13 +339,11 @@ onUnload(releaseAudio);
color: #fff;
}
-.login-panel,
-.empty-panel {
+.login-panel {
text-align: center;
}
-.login-button,
-.empty-panel button {
+.login-button {
margin-top: 14px;
}
diff --git a/mobile-uni/src/styles/base.css b/mobile-uni/src/styles/base.css
index 99899cd4..14814d66 100644
--- a/mobile-uni/src/styles/base.css
+++ b/mobile-uni/src/styles/base.css
@@ -293,31 +293,6 @@ textarea:focus-visible {
line-height: 1.5;
}
-.supervisor-access-panel {
- border-left: 4px solid var(--brand);
-}
-
-.supervisor-access-copy {
- display: block;
- margin-top: 8px;
- font-size: 14px;
- line-height: 1.6;
-}
-
-.supervisor-access-actions {
- display: grid;
- margin-top: 18px;
- grid-template-columns: 1fr 1fr;
- gap: 10px;
-}
-
-.supervisor-access-actions button {
- width: 100%;
- min-width: 0;
- padding: 0 10px;
- font-size: 14px;
-}
-
.empty {
padding: 28px 12px;
color: var(--muted);