feat(mobile): redesign supervisor team and assign flow with member detail and result pages
This commit is contained in:
@@ -127,12 +127,24 @@
|
|||||||
"navigationBarTitleText": "团队画像"
|
"navigationBarTitleText": "团队画像"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/supervisor/member/index",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "成员详情"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/supervisor/assign/index",
|
"path": "pages/supervisor/assign/index",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "指派专项"
|
"navigationBarTitleText": "指派专项"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/supervisor/assign/result",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "派发结果"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/supervisor/reviews/index",
|
"path": "pages/supervisor/reviews/index",
|
||||||
"style": {
|
"style": {
|
||||||
|
|||||||
@@ -28,33 +28,38 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<view class="panel assignment-form">
|
<view class="panel form-panel">
|
||||||
<view class="field-heading">
|
<view class="field-heading">
|
||||||
<text class="field-label">训练对象</text>
|
<text class="panel-title">训练对象</text>
|
||||||
<view v-if="teamMembers.length" class="member-actions">
|
<view v-if="teamMembers.length" class="member-actions">
|
||||||
<button class="text-button" :disabled="assigning" @click="selectAllMembers">{{ selectAllLabel }}</button>
|
<button class="text-button" :disabled="assigning" @click="selectAllMembers">{{ selectAllLabel }}</button>
|
||||||
<button class="text-button" :disabled="assigning || !selectedMemberIds.length" @click="clearSelectedMembers">清空</button>
|
<button class="text-button" :disabled="assigning || !selectedMemberIds.length" @click="clearSelectedMembers">清空</button>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<scroll-view v-if="teamMembers.length" class="member-list" scroll-y>
|
<scroll-view v-if="teamMembers.length" class="member-list" scroll-y>
|
||||||
<button
|
<view class="member-chip-wrap">
|
||||||
v-for="member in teamMembers"
|
<button
|
||||||
:key="member.extPartyId"
|
v-for="member in teamMembers"
|
||||||
class="member-option"
|
:key="member.extPartyId"
|
||||||
:class="{ selected: isMemberSelected(member.extPartyId) }"
|
class="member-option"
|
||||||
:aria-pressed="isMemberSelected(member.extPartyId)"
|
:class="{ selected: isMemberSelected(member.extPartyId) }"
|
||||||
:disabled="assigning || historyLoading"
|
:aria-pressed="isMemberSelected(member.extPartyId)"
|
||||||
@click="toggleTeamMember(member.extPartyId)"
|
:disabled="assigning || historyLoading"
|
||||||
>
|
@click="toggleTeamMember(member.extPartyId)"
|
||||||
<text class="member-check" aria-hidden="true">{{ isMemberSelected(member.extPartyId) ? '✓' : '' }}</text>
|
>
|
||||||
<text class="member-name">{{ member.name || '未命名员工' }}</text>
|
<text class="member-check" aria-hidden="true">{{ isMemberSelected(member.extPartyId) ? '✓' : '' }}</text>
|
||||||
</button>
|
<text class="member-name">{{ member.name || '未命名员工' }}</text>
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
<text v-else class="muted block">当前项目暂无可指派的在职员工,请先同步人力系统组织数据。</text>
|
<text v-else class="muted block">当前项目暂无可指派的在职员工,请先同步人力系统组织数据。</text>
|
||||||
<text v-if="selectedMemberIds.length" class="selected-summary">已选择 {{ selectedMemberIds.length }} 人:{{ selectedMemberNames }}</text>
|
<text v-if="selectedMemberIds.length" class="selected-summary">已选择 {{ selectedMemberIds.length }} 人:{{ selectedMemberNames }}</text>
|
||||||
<text v-if="teamMembers.length > maxAssignmentTargets" class="selection-limit">单次最多派发 {{ maxAssignmentTargets }} 人,超出人员请分批派发。</text>
|
<text v-if="teamMembers.length > maxAssignmentTargets" class="selection-limit">单次最多派发 {{ maxAssignmentTargets }} 人,超出人员请分批派发。</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
<text class="field-label section-label">岗位内容</text>
|
<view class="panel form-panel">
|
||||||
|
<text class="panel-title">岗位与场景</text>
|
||||||
|
<text class="sub-label">岗位</text>
|
||||||
<view class="scenario-row position-row">
|
<view class="scenario-row position-row">
|
||||||
<button
|
<button
|
||||||
v-for="position in assignmentPositions"
|
v-for="position in assignmentPositions"
|
||||||
@@ -66,9 +71,7 @@
|
|||||||
@click="selectPosition(position)"
|
@click="selectPosition(position)"
|
||||||
>{{ position }}</button>
|
>{{ position }}</button>
|
||||||
</view>
|
</view>
|
||||||
<text class="position-note">场景、SOP 与题目按岗位版本加载,不跨岗位混用。</text>
|
<text class="sub-label scenario-sub">场景</text>
|
||||||
|
|
||||||
<text class="field-label section-label">训练场景</text>
|
|
||||||
<view v-if="scenarioOptions.length" class="scenario-row">
|
<view v-if="scenarioOptions.length" class="scenario-row">
|
||||||
<button
|
<button
|
||||||
v-for="item in scenarioOptions"
|
v-for="item in scenarioOptions"
|
||||||
@@ -83,55 +86,49 @@
|
|||||||
</button>
|
</button>
|
||||||
</view>
|
</view>
|
||||||
<text v-else class="muted block">暂无可派发的已启用场景</text>
|
<text v-else class="muted block">暂无可派发的已启用场景</text>
|
||||||
|
<text class="position-note">场景、SOP 与题目按岗位版本加载,不跨岗位混用。</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
<text class="field-label section-label">截止日期</text>
|
<view class="panel form-panel">
|
||||||
<view class="due-date-row">
|
<text class="panel-title">完成时间</text>
|
||||||
<picker mode="date" :value="dueDate" :start="todayDate" :disabled="assigning" @change="selectDueDate">
|
<picker mode="date" :value="dueDate" :start="todayDate" :disabled="assigning" @change="selectDueDate">
|
||||||
<view class="input due-date-picker">{{ dueDate }}</view>
|
<view class="due-date-picker">
|
||||||
</picker>
|
<text class="due-date-text">{{ dueDate }}</text>
|
||||||
<view class="quick-date-row">
|
<text class="due-date-arrow">›</text>
|
||||||
<button
|
|
||||||
v-for="option in dueDateOptions"
|
|
||||||
:key="option.days"
|
|
||||||
class="chip quick-date-chip"
|
|
||||||
:class="{ active: dueDate === dateAfterDays(option.days) }"
|
|
||||||
:aria-pressed="dueDate === dateAfterDays(option.days)"
|
|
||||||
:disabled="assigning"
|
|
||||||
@click="setDueDate(option.days)"
|
|
||||||
>
|
|
||||||
{{ option.label }}
|
|
||||||
</button>
|
|
||||||
</view>
|
</view>
|
||||||
|
</picker>
|
||||||
|
<view class="quick-date-row">
|
||||||
|
<button
|
||||||
|
v-for="option in dueDateOptions"
|
||||||
|
:key="option.days"
|
||||||
|
class="chip quick-date-chip"
|
||||||
|
:class="{ active: dueDate === dateAfterDays(option.days) }"
|
||||||
|
:aria-pressed="dueDate === dateAfterDays(option.days)"
|
||||||
|
:disabled="assigning"
|
||||||
|
@click="setDueDate(option.days)"
|
||||||
|
>
|
||||||
|
{{ option.label }}
|
||||||
|
</button>
|
||||||
</view>
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
<text class="field-label section-label">指派说明(选填)</text>
|
<view class="panel form-panel">
|
||||||
|
<view class="field-heading">
|
||||||
|
<text class="panel-title">指派说明<text class="panel-title-note">(选填)</text></text>
|
||||||
|
<text class="reason-counter">{{ assignmentReason.length }}/200</text>
|
||||||
|
</view>
|
||||||
<textarea
|
<textarea
|
||||||
v-model="assignmentReason"
|
v-model="assignmentReason"
|
||||||
class="textarea reason-input"
|
class="textarea reason-input"
|
||||||
maxlength="180"
|
maxlength="200"
|
||||||
placeholder="例如:针对近期投诉沟通进行巩固"
|
placeholder="例如:针对近期投诉沟通进行巩固"
|
||||||
@input="touchDraft"
|
@input="touchDraft"
|
||||||
/>
|
/>
|
||||||
<button class="primary-button submit-button" :disabled="assigning || historyLoading || !scenarioOptions.length || !selectedMemberIds.length" @click="assignPractice">
|
|
||||||
{{ assigning ? '指派中' : `确认派发(${selectedMemberIds.length}人)` }}
|
|
||||||
</button>
|
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view v-if="batchResultItems.length" class="panel batch-result-panel">
|
<button class="primary-button submit-button" :disabled="assigning || historyLoading || !scenarioOptions.length || !selectedMemberIds.length" @click="assignPractice">
|
||||||
<view class="section-head">
|
{{ assigning ? '指派中' : `确认派发(${selectedMemberIds.length}人)` }}
|
||||||
<text class="panel-title">本次派发结果</text>
|
</button>
|
||||||
<text class="muted">{{ batchResultSummary }}</text>
|
|
||||||
</view>
|
|
||||||
<view class="batch-result-list">
|
|
||||||
<view v-for="item in batchResultItems" :key="`${item.extPartyId}-${item.status}`" class="batch-result-row">
|
|
||||||
<view>
|
|
||||||
<text class="batch-result-name">{{ item.traineeName || '未命名员工' }}</text>
|
|
||||||
<text class="batch-result-message">{{ item.message }}</text>
|
|
||||||
</view>
|
|
||||||
<text class="batch-result-status" :class="item.status.toLowerCase()">{{ batchStatusLabel(item.status) }}</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="panel history-panel">
|
<view class="panel history-panel">
|
||||||
<view class="section-head">
|
<view class="section-head">
|
||||||
@@ -186,7 +183,6 @@ import type {
|
|||||||
PracticeAlert,
|
PracticeAlert,
|
||||||
PracticeAssignment,
|
PracticeAssignment,
|
||||||
PracticeAssignmentBatchItemResult,
|
PracticeAssignmentBatchItemResult,
|
||||||
PracticeAssignmentBatchItemStatus,
|
|
||||||
PracticeTeamMember
|
PracticeTeamMember
|
||||||
} from '@/types/api';
|
} from '@/types/api';
|
||||||
import { clearAuth, getAuth, isLoggedIn, rememberLoginRedirect } from '@/services/auth';
|
import { clearAuth, getAuth, isLoggedIn, rememberLoginRedirect } from '@/services/auth';
|
||||||
@@ -205,6 +201,7 @@ import {
|
|||||||
|
|
||||||
const basePath = '/pages/supervisor/assign/index';
|
const basePath = '/pages/supervisor/assign/index';
|
||||||
const maxAssignmentTargets = 100;
|
const maxAssignmentTargets = 100;
|
||||||
|
const assignResultStorageKey = 'aihr_supervisor_assign_result_v1';
|
||||||
const pendingBatchStoragePrefix = 'aihr_supervisor_assignment_pending_v1';
|
const pendingBatchStoragePrefix = 'aihr_supervisor_assignment_pending_v1';
|
||||||
const pendingBatchTtlMs = 24 * 60 * 60 * 1000;
|
const pendingBatchTtlMs = 24 * 60 * 60 * 1000;
|
||||||
interface PendingBatchDraft {
|
interface PendingBatchDraft {
|
||||||
@@ -226,7 +223,6 @@ const scenarioLoading = ref(false);
|
|||||||
const assignmentReason = ref('');
|
const assignmentReason = ref('');
|
||||||
const dueDate = ref('');
|
const dueDate = ref('');
|
||||||
const pendingRequestId = ref('');
|
const pendingRequestId = ref('');
|
||||||
const batchResultItems = ref<PracticeAssignmentBatchItemResult[]>([]);
|
|
||||||
const assignments = ref<PracticeAssignment[]>([]);
|
const assignments = ref<PracticeAssignment[]>([]);
|
||||||
const teamMembers = ref<PracticeTeamMember[]>([]);
|
const teamMembers = ref<PracticeTeamMember[]>([]);
|
||||||
const alerts = ref<PracticeAlert[]>([]);
|
const alerts = ref<PracticeAlert[]>([]);
|
||||||
@@ -251,17 +247,10 @@ const selectedMemberNames = computed(() => {
|
|||||||
return names.length > 3 ? `${preview} 等 ${names.length} 人` : preview;
|
return names.length > 3 ? `${preview} 等 ${names.length} 人` : preview;
|
||||||
});
|
});
|
||||||
const selectAllLabel = computed(() => teamMembers.value.length > maxAssignmentTargets ? `选前${maxAssignmentTargets}人` : '全选');
|
const selectAllLabel = computed(() => teamMembers.value.length > maxAssignmentTargets ? `选前${maxAssignmentTargets}人` : '全选');
|
||||||
const batchResultSummary = computed(() => {
|
|
||||||
const created = batchResultItems.value.filter((item) => item.status === 'CREATED').length;
|
|
||||||
const duplicate = batchResultItems.value.filter((item) => item.status === 'DUPLICATE').length;
|
|
||||||
const failed = batchResultItems.value.filter((item) => item.status === 'FAILED').length;
|
|
||||||
return `成功 ${created + duplicate} · 失败 ${failed}`;
|
|
||||||
});
|
|
||||||
const dueDateOptions = [
|
const dueDateOptions = [
|
||||||
{ label: '当天', days: 0 },
|
|
||||||
{ label: '明天', days: 1 },
|
{ label: '明天', days: 1 },
|
||||||
{ label: '3天内', days: 3 },
|
{ label: '3天后', days: 3 },
|
||||||
{ label: '7天内', days: 7 }
|
{ label: '7天后', days: 7 }
|
||||||
];
|
];
|
||||||
const formatDate = (date: Date) => {
|
const formatDate = (date: Date) => {
|
||||||
const year = date.getFullYear();
|
const year = date.getFullYear();
|
||||||
@@ -395,7 +384,6 @@ const clearPrivateState = () => {
|
|||||||
resetDraft();
|
resetDraft();
|
||||||
assignments.value = [];
|
assignments.value = [];
|
||||||
teamMembers.value = [];
|
teamMembers.value = [];
|
||||||
batchResultItems.value = [];
|
|
||||||
alerts.value = [];
|
alerts.value = [];
|
||||||
historyLoading.value = false;
|
historyLoading.value = false;
|
||||||
historyRequests.invalidate();
|
historyRequests.invalidate();
|
||||||
@@ -516,19 +504,21 @@ const assignPractice = async () => {
|
|||||||
reason: assignmentReason.value.trim() || undefined,
|
reason: assignmentReason.value.trim() || undefined,
|
||||||
dueDate: dueDate.value
|
dueDate: dueDate.value
|
||||||
});
|
});
|
||||||
batchResultItems.value = result.results || [];
|
const resultItems: PracticeAssignmentBatchItemResult[] = result.results || [];
|
||||||
|
const resultDueDate = dueDate.value;
|
||||||
|
const resultScenarioName = scenarioOptions.value.find((item) => item.id === assignmentScenarioId.value)?.name || '专项训练';
|
||||||
clearPendingBatchDraft();
|
clearPendingBatchDraft();
|
||||||
const completedIds = new Set(batchResultItems.value
|
selectedMemberIds.value = [];
|
||||||
.filter((item) => item.status !== 'FAILED')
|
|
||||||
.map((item) => item.extPartyId));
|
|
||||||
selectedMemberIds.value = selectedMemberIds.value.filter((id) => !completedIds.has(id));
|
|
||||||
if (!result.failed) {
|
|
||||||
resetDraft();
|
|
||||||
uni.showToast({ title: `已派发${result.created + result.duplicate}人`, icon: 'none' });
|
|
||||||
} else {
|
|
||||||
message.value = `${result.created + result.duplicate}人派发成功,${result.failed}人失败,请查看明细`;
|
|
||||||
}
|
|
||||||
await loadAssignments(true);
|
await loadAssignments(true);
|
||||||
|
uni.setStorageSync(assignResultStorageKey, {
|
||||||
|
scenarioName: resultScenarioName,
|
||||||
|
dueDate: resultDueDate,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
items: resultItems,
|
||||||
|
progress: { ...progressCounts.value }
|
||||||
|
});
|
||||||
|
resetDraft();
|
||||||
|
uni.redirectTo({ url: '/pages/supervisor/assign/result' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleRequestError(error, '指派失败');
|
handleRequestError(error, '指派失败');
|
||||||
} finally {
|
} finally {
|
||||||
@@ -543,7 +533,6 @@ const createBatchRequestId = () => {
|
|||||||
|
|
||||||
const touchDraft = () => {
|
const touchDraft = () => {
|
||||||
clearPendingBatchDraft();
|
clearPendingBatchDraft();
|
||||||
batchResultItems.value = [];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const isMemberSelected = (extPartyId: string) => selectedMemberIds.value.includes(extPartyId);
|
const isMemberSelected = (extPartyId: string) => selectedMemberIds.value.includes(extPartyId);
|
||||||
@@ -601,12 +590,6 @@ const selectDueDate = (event: { detail?: { value?: string } }) => {
|
|||||||
touchDraft();
|
touchDraft();
|
||||||
};
|
};
|
||||||
|
|
||||||
const batchStatusLabel = (status: PracticeAssignmentBatchItemStatus) => {
|
|
||||||
if (status === 'CREATED') return '成功';
|
|
||||||
if (status === 'DUPLICATE') return '已存在';
|
|
||||||
return '失败';
|
|
||||||
};
|
|
||||||
|
|
||||||
const assignmentSourceLabel = (source: string) => {
|
const assignmentSourceLabel = (source: string) => {
|
||||||
if (source === 'retry') return '再练';
|
if (source === 'retry') return '再练';
|
||||||
if (source === 'camp') return '训练营';
|
if (source === 'camp') return '训练营';
|
||||||
@@ -709,15 +692,34 @@ onBeforeUnmount(() => {
|
|||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.field-label {
|
.form-panel {
|
||||||
display: block;
|
margin-top: 16px;
|
||||||
color: #334155;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 800;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-label {
|
.form-panel + .form-panel {
|
||||||
margin-top: 16px;
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-title-note {
|
||||||
|
color: var(--employee-muted);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reason-counter {
|
||||||
|
color: var(--employee-muted);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-label {
|
||||||
|
display: block;
|
||||||
|
margin-top: 12px;
|
||||||
|
color: var(--employee-muted);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-label.scenario-sub {
|
||||||
|
margin-top: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.field-heading,
|
.field-heading,
|
||||||
@@ -741,6 +743,7 @@ onBeforeUnmount(() => {
|
|||||||
min-height: 32px;
|
min-height: 32px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
margin: 0;
|
||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
border: 0;
|
border: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
@@ -749,37 +752,45 @@ onBeforeUnmount(() => {
|
|||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text-button::after {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.field-input,
|
.field-input,
|
||||||
.reason-input,
|
.reason-input,
|
||||||
.scenario-row,
|
.scenario-row {
|
||||||
.submit-button {
|
|
||||||
margin-top: 9px;
|
margin-top: 9px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.member-list {
|
.member-list {
|
||||||
max-height: 216px;
|
max-height: 190px;
|
||||||
margin-top: 10px;
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-chip-wrap {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.member-option {
|
.member-option {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: calc(50% - 5px);
|
min-height: 38px;
|
||||||
min-height: 44px;
|
margin: 0;
|
||||||
margin: 0 10px 8px 0;
|
|
||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
border: 1px solid #dbe4f0;
|
border: 1px solid #dbe4f0;
|
||||||
border-radius: 10px;
|
border-radius: 19px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
font-size: 14px;
|
font-size: 13px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
line-height: 1.25;
|
line-height: 1;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.member-option:nth-child(2n) {
|
.member-option::after {
|
||||||
margin-right: 0;
|
border: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.member-option.selected {
|
.member-option.selected {
|
||||||
@@ -792,13 +803,14 @@ onBeforeUnmount(() => {
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 18px;
|
width: 16px;
|
||||||
height: 18px;
|
height: 16px;
|
||||||
margin-right: 8px;
|
margin-right: 6px;
|
||||||
border: 1px solid #cbd5e1;
|
border: 1px solid #cbd5e1;
|
||||||
border-radius: 5px;
|
border-radius: 50%;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 12px;
|
font-size: 11px;
|
||||||
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.member-option.selected .member-check {
|
.member-option.selected .member-check {
|
||||||
@@ -829,22 +841,29 @@ onBeforeUnmount(() => {
|
|||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.due-date-row {
|
|
||||||
margin-top: 9px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.due-date-picker {
|
.due-date-picker {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
min-height: 44px;
|
min-height: 44px;
|
||||||
|
margin-top: 10px;
|
||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
border: 1px solid #dbe4f0;
|
border: 1px solid #dbe4f0;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.due-date-text {
|
||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.due-date-arrow {
|
||||||
|
color: #c0c4cc;
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.quick-date-row {
|
.quick-date-row {
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
@@ -859,8 +878,10 @@ onBeforeUnmount(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.quick-date-chip.active {
|
.quick-date-chip.active {
|
||||||
background: var(--employee-primary-soft);
|
border-color: var(--employee-primary);
|
||||||
color: var(--employee-primary);
|
background: var(--employee-primary);
|
||||||
|
color: #ffffff;
|
||||||
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scenario-row {
|
.scenario-row {
|
||||||
@@ -891,65 +912,15 @@ onBeforeUnmount(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.scenario-chip.active {
|
.scenario-chip.active {
|
||||||
background: var(--employee-primary-soft);
|
border-color: var(--employee-primary);
|
||||||
color: var(--employee-primary);
|
background: var(--employee-primary);
|
||||||
|
color: #ffffff;
|
||||||
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
|
|
||||||
.submit-button {
|
.submit-button {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
margin-top: 16px;
|
||||||
|
|
||||||
.batch-result-panel {
|
|
||||||
margin-top: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.batch-result-list {
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.batch-result-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 12px;
|
|
||||||
padding: 11px 0;
|
|
||||||
border-bottom: 1px solid #e2e8f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.batch-result-row:last-child {
|
|
||||||
border-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.batch-result-name,
|
|
||||||
.batch-result-message {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.batch-result-name {
|
|
||||||
color: #0f172a;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 800;
|
|
||||||
}
|
|
||||||
|
|
||||||
.batch-result-message {
|
|
||||||
margin-top: 3px;
|
|
||||||
color: #64748b;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.batch-result-status {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
color: #64748b;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 800;
|
|
||||||
}
|
|
||||||
|
|
||||||
.batch-result-status.created {
|
|
||||||
color: #047857;
|
|
||||||
}
|
|
||||||
|
|
||||||
.batch-result-status.failed {
|
|
||||||
color: #dc2626;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.assignment-list {
|
.assignment-list {
|
||||||
|
|||||||
@@ -0,0 +1,464 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page assign-result-page">
|
||||||
|
<view class="subpage-header">
|
||||||
|
<button class="subpage-back-button" aria-label="返回指派专项" title="返回指派专项" @click="goToAssign"><uni-icons type="back" size="20" color="#25282d" /></button>
|
||||||
|
<view class="subpage-heading">
|
||||||
|
<text class="page-title">派发结果</text>
|
||||||
|
<text class="page-subtitle">{{ payload ? `${payload.scenarioName}专项` : '本次派发' }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<template v-if="payload">
|
||||||
|
<view class="result-banner">
|
||||||
|
<view class="result-banner-icon">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>
|
||||||
|
</view>
|
||||||
|
<view class="result-banner-copy">
|
||||||
|
<text class="result-banner-title">已完成派发</text>
|
||||||
|
<text class="result-banner-desc">{{ bannerSummary }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="panel result-panel">
|
||||||
|
<view class="section-head">
|
||||||
|
<text class="panel-title">本次结果</text>
|
||||||
|
<text class="muted">{{ payload.items.length }} 人</text>
|
||||||
|
</view>
|
||||||
|
<view class="result-list">
|
||||||
|
<view v-for="item in payload.items" :key="`${item.extPartyId}-${item.status}`" class="result-row" @click="openMember(item)">
|
||||||
|
<view class="member-avatar">
|
||||||
|
<text class="member-avatar-text">{{ memberInitial(item.traineeName) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="result-main">
|
||||||
|
<text class="result-name">{{ item.traineeName || '未命名员工' }}</text>
|
||||||
|
<text class="result-desc">{{ resultDesc(item) }}</text>
|
||||||
|
</view>
|
||||||
|
<text class="result-badge" :class="badgeClass(item.status)">{{ badgeLabel(item.status) }}</text>
|
||||||
|
<text class="result-arrow">›</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="panel progress-panel">
|
||||||
|
<view class="section-head">
|
||||||
|
<text class="panel-title">任务进度</text>
|
||||||
|
<text class="muted">团队当前</text>
|
||||||
|
</view>
|
||||||
|
<view class="progress-metrics">
|
||||||
|
<view class="progress-cell">
|
||||||
|
<text class="progress-value pending">{{ payload.progress.pending }}</text>
|
||||||
|
<text class="progress-label">待训练</text>
|
||||||
|
</view>
|
||||||
|
<view class="progress-cell">
|
||||||
|
<text class="progress-value">{{ payload.progress.completed }}</text>
|
||||||
|
<text class="progress-label">已完成</text>
|
||||||
|
</view>
|
||||||
|
<view class="progress-cell">
|
||||||
|
<text class="progress-value">{{ payload.progress.retry }}</text>
|
||||||
|
<text class="progress-label">低分再练</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="panel reminder-panel">
|
||||||
|
<view class="section-head">
|
||||||
|
<text class="panel-title">后续提醒</text>
|
||||||
|
</view>
|
||||||
|
<view class="reminder-row">
|
||||||
|
<text class="reminder-label">截止前1天提醒未完成人员</text>
|
||||||
|
<switch :checked="reminders.beforeDue" color="#e60012" @change="toggleReminder('beforeDue', $event)" />
|
||||||
|
</view>
|
||||||
|
<view class="reminder-row">
|
||||||
|
<text class="reminder-label">完成后自动进入待复盘</text>
|
||||||
|
<switch :checked="reminders.autoReview" color="#e60012" @change="toggleReminder('autoReview', $event)" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="result-actions">
|
||||||
|
<button class="result-action-primary" @click="goToTeam">查看团队任务</button>
|
||||||
|
<button class="result-action-secondary" @click="goToAssign">继续派发</button>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<view v-else class="panel empty-panel">
|
||||||
|
<view class="empty">暂无本次派发结果,请先在指派页完成派发。</view>
|
||||||
|
<button class="primary-button empty-action" @click="goToAssign">去指派专项</button>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<SupervisorTabBar active="team" />
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { onShow } from '@dcloudio/uni-app';
|
||||||
|
import { resetPageScroll } from '@/services/navigation';
|
||||||
|
import SupervisorTabBar from '@/components/SupervisorTabBar.vue';
|
||||||
|
|
||||||
|
interface AssignResultItem {
|
||||||
|
extPartyId: string;
|
||||||
|
traineeName: string;
|
||||||
|
status: string;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AssignResultPayload {
|
||||||
|
scenarioName: string;
|
||||||
|
dueDate: string;
|
||||||
|
createdAt: number;
|
||||||
|
items: AssignResultItem[];
|
||||||
|
progress: { pending: number; completed: number; retry: number };
|
||||||
|
}
|
||||||
|
|
||||||
|
const assignResultStorageKey = 'aihr_supervisor_assign_result_v1';
|
||||||
|
const reminderStorageKey = 'aihr_supervisor_assign_reminders_v1';
|
||||||
|
const payload = ref<AssignResultPayload | null>(null);
|
||||||
|
const reminders = ref({ beforeDue: true, autoReview: true });
|
||||||
|
|
||||||
|
const isPayload = (value: unknown): value is AssignResultPayload => {
|
||||||
|
if (!value || typeof value !== 'object') return false;
|
||||||
|
const candidate = value as Record<string, unknown>;
|
||||||
|
return typeof candidate.scenarioName === 'string'
|
||||||
|
&& typeof candidate.dueDate === 'string'
|
||||||
|
&& Array.isArray(candidate.items)
|
||||||
|
&& Boolean(candidate.progress) && typeof candidate.progress === 'object';
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadPayload = () => {
|
||||||
|
const stored = uni.getStorageSync(assignResultStorageKey) as unknown;
|
||||||
|
try {
|
||||||
|
const parsed: unknown = typeof stored === 'string' ? JSON.parse(stored) : stored;
|
||||||
|
payload.value = isPayload(parsed) ? parsed : null;
|
||||||
|
} catch {
|
||||||
|
payload.value = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadReminders = () => {
|
||||||
|
const stored = uni.getStorageSync(reminderStorageKey) as unknown;
|
||||||
|
try {
|
||||||
|
const parsed: unknown = typeof stored === 'string' ? JSON.parse(stored) : stored;
|
||||||
|
if (parsed && typeof parsed === 'object') {
|
||||||
|
const candidate = parsed as Record<string, unknown>;
|
||||||
|
reminders.value = {
|
||||||
|
beforeDue: candidate.beforeDue !== false,
|
||||||
|
autoReview: candidate.autoReview !== false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 保持默认开启
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleReminder = (key: 'beforeDue' | 'autoReview', event: { detail?: { value?: boolean } }) => {
|
||||||
|
reminders.value = { ...reminders.value, [key]: Boolean(event.detail?.value) };
|
||||||
|
uni.setStorageSync(reminderStorageKey, reminders.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const createdCount = computed(() => payload.value?.items.filter((item) => item.status === 'CREATED').length ?? 0);
|
||||||
|
const duplicateCount = computed(() => payload.value?.items.filter((item) => item.status === 'DUPLICATE').length ?? 0);
|
||||||
|
const failedCount = computed(() => payload.value?.items.filter((item) => item.status === 'FAILED').length ?? 0);
|
||||||
|
const bannerSummary = computed(() => {
|
||||||
|
const parts = [`成功${createdCount.value}人`];
|
||||||
|
if (duplicateCount.value) parts.push(`跳过${duplicateCount.value}人`);
|
||||||
|
if (failedCount.value) parts.push(`失败${failedCount.value}人`);
|
||||||
|
return parts.join(' · ');
|
||||||
|
});
|
||||||
|
|
||||||
|
const memberInitial = (name: string) => (name || '').trim().charAt(0) || '员';
|
||||||
|
|
||||||
|
const resultDesc = (item: AssignResultItem) => {
|
||||||
|
if (item.status === 'CREATED') return payload.value?.dueDate ? `截止 ${payload.value.dueDate}` : '已生成训练任务';
|
||||||
|
if (item.status === 'DUPLICATE') return '未重复派发';
|
||||||
|
return item.message?.trim() || '派发失败,请稍后重试';
|
||||||
|
};
|
||||||
|
|
||||||
|
const badgeClass = (status: string) => (status === 'CREATED' ? 'created' : status === 'DUPLICATE' ? 'duplicate' : 'failed');
|
||||||
|
const badgeLabel = (status: string) => (status === 'CREATED' ? '派发成功' : status === 'DUPLICATE' ? '已存在相同任务' : '派发失败');
|
||||||
|
|
||||||
|
const openMember = (item: AssignResultItem) => {
|
||||||
|
if (!item.extPartyId) return;
|
||||||
|
const query = [
|
||||||
|
`extPartyId=${encodeURIComponent(item.extPartyId)}`,
|
||||||
|
`name=${encodeURIComponent(item.traineeName || '成员')}`
|
||||||
|
].join('&');
|
||||||
|
uni.navigateTo({ url: `/pages/supervisor/member/index?${query}` });
|
||||||
|
};
|
||||||
|
|
||||||
|
const goToTeam = () => {
|
||||||
|
uni.redirectTo({ url: '/pages/supervisor/team/index' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const goToAssign = () => {
|
||||||
|
uni.redirectTo({ url: '/pages/supervisor/assign/index' });
|
||||||
|
};
|
||||||
|
|
||||||
|
onShow(() => {
|
||||||
|
resetPageScroll();
|
||||||
|
loadPayload();
|
||||||
|
loadReminders();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.assign-result-page {
|
||||||
|
padding-bottom: 112px;
|
||||||
|
background: linear-gradient(180deg, var(--employee-bg-soft) 0%, var(--employee-bg) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.subpage-header {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-banner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 16px;
|
||||||
|
padding: 16px 14px;
|
||||||
|
border: 1px solid #bbf7d0;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #f0fdf4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-banner-icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #16a34a;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-banner-icon svg {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-banner-copy {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-banner-title {
|
||||||
|
display: block;
|
||||||
|
color: #15803d;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-banner-desc {
|
||||||
|
display: block;
|
||||||
|
margin-top: 4px;
|
||||||
|
color: #166534;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-panel,
|
||||||
|
.progress-panel,
|
||||||
|
.reminder-panel {
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-list {
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 12px 0;
|
||||||
|
border-bottom: 1px solid #e5edf7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-row:last-child {
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-avatar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--employee-primary-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-avatar-text {
|
||||||
|
color: var(--employee-primary);
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-main {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-name {
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
color: #0f172a;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 900;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-desc {
|
||||||
|
display: block;
|
||||||
|
margin-top: 4px;
|
||||||
|
color: var(--employee-muted);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-badge {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-badge.created {
|
||||||
|
background: #f0fdf4;
|
||||||
|
color: #16a34a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-badge.duplicate {
|
||||||
|
background: #f1f5f9;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-badge.failed {
|
||||||
|
background: var(--employee-primary-soft);
|
||||||
|
color: var(--employee-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-arrow {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
padding: 0 2px;
|
||||||
|
color: #c0c4cc;
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-metrics {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
margin-top: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid #e5edf7;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-cell {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
padding: 13px 8px;
|
||||||
|
border-right: 1px solid #e5edf7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-cell:last-child {
|
||||||
|
border-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-value {
|
||||||
|
color: #0f172a;
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 900;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-value.pending {
|
||||||
|
color: var(--employee-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-label {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reminder-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px 0;
|
||||||
|
border-bottom: 1px solid #e5edf7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reminder-row:last-child {
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reminder-label {
|
||||||
|
color: var(--employee-text);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-action-primary,
|
||||||
|
.result-action-secondary {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 46px;
|
||||||
|
margin: 0;
|
||||||
|
border-radius: 23px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-action-primary::after,
|
||||||
|
.result-action-secondary::after {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-action-primary {
|
||||||
|
border: 0;
|
||||||
|
background: var(--brand);
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-action-secondary {
|
||||||
|
border: 1px solid var(--brand);
|
||||||
|
background: #ffffff;
|
||||||
|
color: var(--brand);
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-panel {
|
||||||
|
margin-top: 16px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-action {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,708 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page member-page">
|
||||||
|
<view class="subpage-header">
|
||||||
|
<button class="subpage-back-button" aria-label="返回团队画像" title="返回团队画像" @click="goBack"><uni-icons type="back" size="20" color="#25282d" /></button>
|
||||||
|
<view class="subpage-heading">
|
||||||
|
<text class="page-title">成员详情</text>
|
||||||
|
<text class="page-subtitle">{{ displayName }}</text>
|
||||||
|
</view>
|
||||||
|
<button v-if="loggedIn && !accessDenied" class="ghost-button refresh-button" :disabled="loading" @click="loadPage">
|
||||||
|
{{ loading ? '刷新中' : '刷新' }}
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="message && !accessDenied" class="status-message">{{ message }}</view>
|
||||||
|
|
||||||
|
<view v-if="accessDenied" class="panel supervisor-access-panel">
|
||||||
|
<text class="panel-title">无主管权限</text>
|
||||||
|
<text class="muted supervisor-access-copy">当前账号不是主管或项目经理,无法查看成员训练数据。</text>
|
||||||
|
<view class="supervisor-access-actions">
|
||||||
|
<button class="secondary-button" @click="goToEmployee">返回员工端</button>
|
||||||
|
<button class="primary-button" @click="switchAccount">切换账号</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-else-if="!loggedIn" class="panel login-panel">
|
||||||
|
<view>
|
||||||
|
<text class="panel-title">需要登录</text>
|
||||||
|
<text class="muted block">登录后查看成员详情。</text>
|
||||||
|
</view>
|
||||||
|
<button class="primary-button login-button" @click="goLogin">去登录</button>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<view class="panel member-head">
|
||||||
|
<view class="member-avatar large">
|
||||||
|
<text class="member-avatar-text">{{ memberInitial }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="member-head-main">
|
||||||
|
<view class="member-head-top">
|
||||||
|
<text class="risk-badge" :class="profile?.risk || 'low'">{{ riskBadgeText }}</text>
|
||||||
|
<text class="member-head-score">最近训练 <text class="member-head-score-value" :class="profile?.risk || 'low'">{{ profile?.latestScoreText || '暂无评分' }}</text></text>
|
||||||
|
</view>
|
||||||
|
<text class="member-head-reason">{{ headReason }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="member-stats">
|
||||||
|
<view class="member-stat">
|
||||||
|
<text class="member-stat-value">{{ totalSessions }}</text>
|
||||||
|
<text class="member-stat-label">训练次数</text>
|
||||||
|
</view>
|
||||||
|
<view class="member-stat">
|
||||||
|
<text class="member-stat-value medium">{{ profile?.pendingReview ?? 0 }}</text>
|
||||||
|
<text class="member-stat-label">待复盘</text>
|
||||||
|
</view>
|
||||||
|
<view class="member-stat">
|
||||||
|
<text class="member-stat-value low">{{ profile?.pendingAssignment ?? 0 }}</text>
|
||||||
|
<text class="member-stat-label">待训练</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="panel trend-panel">
|
||||||
|
<view class="section-head">
|
||||||
|
<text class="panel-title">训练趋势</text>
|
||||||
|
<text class="muted">最近 {{ trendItems.length }} 次</text>
|
||||||
|
</view>
|
||||||
|
<view v-if="trendItems.length" class="trend-list">
|
||||||
|
<view v-for="(item, index) in trendItems" :key="`${item.scene}-${index}`" class="trend-row">
|
||||||
|
<text class="trend-scene">{{ item.scene }}</text>
|
||||||
|
<view class="trend-track">
|
||||||
|
<view class="trend-fill" :class="scoreBand(item.score)" :style="{ width: `${Math.min(Math.max(item.score, 0), 100)}%` }"></view>
|
||||||
|
</view>
|
||||||
|
<text class="trend-score" :class="scoreBand(item.score)">{{ item.score }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-else class="empty compact-empty">暂无训练记录</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="panel evidence-panel">
|
||||||
|
<view class="section-head">
|
||||||
|
<text class="panel-title">成长证据</text>
|
||||||
|
</view>
|
||||||
|
<view class="evidence-list">
|
||||||
|
<view class="evidence-row">
|
||||||
|
<text class="evidence-text">已完成 <text class="evidence-strong">{{ totalSessions }}</text> 次训练</text>
|
||||||
|
</view>
|
||||||
|
<view class="evidence-row">
|
||||||
|
<text class="evidence-text" :class="{ warn: lowScoreScene }">{{ lowScoreScene ? `连续低分场景:${lowScoreScene}` : '暂无连续低分场景' }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="evidence-row">
|
||||||
|
<text class="evidence-text ok">{{ mainProblem }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="pendingTasks.length" class="panel task-panel">
|
||||||
|
<view class="section-head">
|
||||||
|
<text class="panel-title">当前任务</text>
|
||||||
|
<text class="muted">{{ pendingTasks.length }} 项待完成</text>
|
||||||
|
</view>
|
||||||
|
<view class="task-list">
|
||||||
|
<view v-for="task in pendingTasks" :key="task.id" class="task-row">
|
||||||
|
<view class="entry-icon soft">
|
||||||
|
<uni-icons type="compose" size="18" color="#e60012" />
|
||||||
|
</view>
|
||||||
|
<view class="task-main">
|
||||||
|
<text class="task-title">{{ task.scenarioName }}专项</text>
|
||||||
|
<text class="task-meta">{{ task.dueDate ? `截止 ${task.dueDate}` : '长期有效' }} · <text class="task-status">{{ task.status }}</text></text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="member-actions">
|
||||||
|
<button class="member-action-primary" :disabled="!profile" @click="assignMember">派发专项</button>
|
||||||
|
<button class="member-action-secondary" @click="goToReviews">查看复盘记录</button>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
<SupervisorTabBar v-if="loggedIn && !accessDenied" active="team" />
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { onHide, onLoad, onShow, onUnload } from '@dcloudio/uni-app';
|
||||||
|
import type { PracticeAssignment, PracticeRecord } from '@/types/api';
|
||||||
|
import { clearAuth, isLoggedIn, rememberLoginRedirect } from '@/services/auth';
|
||||||
|
import { resetPageScroll } from '@/services/navigation';
|
||||||
|
import { getPracticeTeamSnapshot } from '@/services/practice';
|
||||||
|
import SupervisorTabBar from '@/components/SupervisorTabBar.vue';
|
||||||
|
import {
|
||||||
|
buildTeamProfiles,
|
||||||
|
inferSupervisorScenarioId,
|
||||||
|
isSupervisorAccessDenied,
|
||||||
|
manualPracticeAssignments,
|
||||||
|
type TeamProfile
|
||||||
|
} from '@/services/supervisor-view';
|
||||||
|
import { createPageRequestScope, currentAccountKey } from '@/services/request-scope';
|
||||||
|
|
||||||
|
const currentPath = '/pages/supervisor/member/index';
|
||||||
|
const extPartyId = ref('');
|
||||||
|
const displayName = ref('成员');
|
||||||
|
const records = ref<PracticeRecord[]>([]);
|
||||||
|
const assignments = ref<PracticeAssignment[]>([]);
|
||||||
|
const memberNames = ref<Record<string, string>>({});
|
||||||
|
const loggedIn = ref(false);
|
||||||
|
const accessDenied = ref(false);
|
||||||
|
const loading = ref(false);
|
||||||
|
const message = ref('');
|
||||||
|
const requests = createPageRequestScope();
|
||||||
|
let visibleAccountKey = '';
|
||||||
|
|
||||||
|
const profiles = computed(() => {
|
||||||
|
const members = Object.entries(memberNames.value).map(([id, name]) => ({ extPartyId: id, name }));
|
||||||
|
return buildTeamProfiles(records.value, assignments.value, members);
|
||||||
|
});
|
||||||
|
const profile = computed<TeamProfile | null>(() =>
|
||||||
|
profiles.value.find((item) => item.extPartyId === extPartyId.value)
|
||||||
|
|| profiles.value.find((item) => item.name === displayName.value)
|
||||||
|
|| null
|
||||||
|
);
|
||||||
|
const memberRecords = computed(() => records.value
|
||||||
|
.filter((item) => (item.extPartyId || item.trainee) === extPartyId.value || item.trainee === displayName.value)
|
||||||
|
.slice(0, 6));
|
||||||
|
const totalSessions = computed(() => records.value
|
||||||
|
.filter((item) => (item.extPartyId || item.trainee) === extPartyId.value || item.trainee === displayName.value)
|
||||||
|
.length);
|
||||||
|
const trendItems = computed(() => memberRecords.value.map((item) => ({
|
||||||
|
scene: item.scene || '对练',
|
||||||
|
score: Number.isFinite(item.score) ? Math.round(item.score) : 0
|
||||||
|
})));
|
||||||
|
const memberAssignments = computed(() => manualPracticeAssignments(assignments.value)
|
||||||
|
.filter((item) => item.extPartyId === extPartyId.value || item.traineeName === displayName.value));
|
||||||
|
const pendingTasks = computed(() => memberAssignments.value
|
||||||
|
.filter((item) => item.status !== '已完成')
|
||||||
|
.slice(0, 2));
|
||||||
|
const lowScoreScene = computed(() => {
|
||||||
|
const lowByScene = new Map<string, number>();
|
||||||
|
records.value
|
||||||
|
.filter((item) => ((item.extPartyId || item.trainee) === extPartyId.value || item.trainee === displayName.value)
|
||||||
|
&& Number.isFinite(item.score) && item.score < 60)
|
||||||
|
.forEach((item) => {
|
||||||
|
const scene = item.scene.trim() || '对练';
|
||||||
|
lowByScene.set(scene, (lowByScene.get(scene) || 0) + 1);
|
||||||
|
});
|
||||||
|
for (const [scene, count] of lowByScene) {
|
||||||
|
if (count >= 2) return scene;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
});
|
||||||
|
const memberInitial = computed(() => displayName.value.trim().charAt(0) || '员');
|
||||||
|
const riskBadgeText = computed(() => {
|
||||||
|
const risk = profile.value?.risk;
|
||||||
|
return risk === 'high' ? '高风险' : risk === 'medium' ? '待复盘' : '正常';
|
||||||
|
});
|
||||||
|
const headReason = computed(() => {
|
||||||
|
if (profile.value?.reason) return `原因:${profile.value.reason}`;
|
||||||
|
return loading.value ? '加载中…' : '暂无风险说明';
|
||||||
|
});
|
||||||
|
const mainProblem = computed(() => {
|
||||||
|
const item = profile.value;
|
||||||
|
if (!item) return '训练表现待观察';
|
||||||
|
if (item.risk === 'high') return `主要问题:低分未复盘,建议定向再练`;
|
||||||
|
if (item.risk === 'medium') return `主要问题:有 ${item.pendingReview} 条待复盘,先完成点评`;
|
||||||
|
if (item.pendingAssignment > 0) return `待完成 ${item.pendingAssignment} 项专项训练`;
|
||||||
|
return '训练表现稳定,可按需提升';
|
||||||
|
});
|
||||||
|
|
||||||
|
const scoreBand = (score: number) => (score < 60 ? 'low' : score < 80 ? 'medium' : 'high');
|
||||||
|
|
||||||
|
const clearData = () => {
|
||||||
|
records.value = [];
|
||||||
|
assignments.value = [];
|
||||||
|
memberNames.value = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
const refreshAuthState = () => {
|
||||||
|
loggedIn.value = isLoggedIn();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRequestError = (error: unknown, fallback: string) => {
|
||||||
|
if (isSupervisorAccessDenied(error)) {
|
||||||
|
clearData();
|
||||||
|
accessDenied.value = true;
|
||||||
|
message.value = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
refreshAuthState();
|
||||||
|
if (!loggedIn.value) {
|
||||||
|
accessDenied.value = false;
|
||||||
|
clearData();
|
||||||
|
message.value = '登录已过期,请重新登录';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
message.value = error instanceof Error ? error.message : fallback;
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadPage = async () => {
|
||||||
|
const request = requests.begin('page');
|
||||||
|
refreshAuthState();
|
||||||
|
message.value = '';
|
||||||
|
if (!loggedIn.value) {
|
||||||
|
accessDenied.value = false;
|
||||||
|
clearData();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const snapshot = await getPracticeTeamSnapshot();
|
||||||
|
if (!requests.isCurrent(request)) return;
|
||||||
|
accessDenied.value = false;
|
||||||
|
records.value = snapshot.records;
|
||||||
|
assignments.value = snapshot.assignments;
|
||||||
|
memberNames.value = Object.fromEntries(snapshot.members.map((item) => [item.extPartyId, item.name]));
|
||||||
|
if (!displayName.value || displayName.value === '成员') {
|
||||||
|
const mapped = memberNames.value[extPartyId.value];
|
||||||
|
if (mapped) displayName.value = mapped;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (!requests.isCurrent(request)) return;
|
||||||
|
handleRequestError(error, '成员详情加载失败');
|
||||||
|
} finally {
|
||||||
|
if (requests.isCurrent(request)) loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const assignMember = () => {
|
||||||
|
const item = profile.value;
|
||||||
|
if (!item) return;
|
||||||
|
const scenarioId = inferSupervisorScenarioId(`${item.latestScene} ${item.reason}`);
|
||||||
|
const query = [
|
||||||
|
`extPartyId=${encodeURIComponent(item.extPartyId)}`,
|
||||||
|
`name=${encodeURIComponent(item.name)}`,
|
||||||
|
`scenarioId=${encodeURIComponent(scenarioId)}`,
|
||||||
|
`reason=${encodeURIComponent(item.reason)}`
|
||||||
|
].join('&');
|
||||||
|
uni.navigateTo({ url: `/pages/supervisor/assign/index?${query}` });
|
||||||
|
};
|
||||||
|
|
||||||
|
const goToReviews = () => {
|
||||||
|
uni.navigateTo({ url: '/pages/supervisor/reviews/index' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const invalidateRequests = () => {
|
||||||
|
requests.invalidate();
|
||||||
|
loading.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const goLogin = () => {
|
||||||
|
rememberLoginRedirect(`${currentPath}?extPartyId=${encodeURIComponent(extPartyId.value)}&name=${encodeURIComponent(displayName.value)}`);
|
||||||
|
uni.redirectTo({ url: '/pages/auth/login/index' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const goToEmployee = () => {
|
||||||
|
uni.switchTab({ url: '/pages/user/today/index' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const switchAccount = () => {
|
||||||
|
clearAuth();
|
||||||
|
rememberLoginRedirect(currentPath);
|
||||||
|
uni.redirectTo({ url: '/pages/auth/login/index' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const goBack = () => {
|
||||||
|
if (getCurrentPages().length <= 1) {
|
||||||
|
uni.redirectTo({ url: '/pages/supervisor/team/index' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
uni.navigateBack({
|
||||||
|
delta: 1,
|
||||||
|
fail: () => uni.redirectTo({ url: '/pages/supervisor/team/index' })
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onLoad((query) => {
|
||||||
|
extPartyId.value = typeof query?.extPartyId === 'string' ? query.extPartyId : '';
|
||||||
|
const name = typeof query?.name === 'string' ? query.name : '';
|
||||||
|
if (name) displayName.value = name;
|
||||||
|
});
|
||||||
|
|
||||||
|
onShow(() => {
|
||||||
|
const nextAccountKey = currentAccountKey();
|
||||||
|
if (visibleAccountKey && visibleAccountKey !== nextAccountKey) {
|
||||||
|
clearData();
|
||||||
|
message.value = '';
|
||||||
|
accessDenied.value = false;
|
||||||
|
}
|
||||||
|
visibleAccountKey = nextAccountKey;
|
||||||
|
requests.activate();
|
||||||
|
resetPageScroll();
|
||||||
|
void loadPage();
|
||||||
|
});
|
||||||
|
|
||||||
|
onHide(invalidateRequests);
|
||||||
|
onUnload(invalidateRequests);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.member-page {
|
||||||
|
padding-bottom: 112px;
|
||||||
|
background: linear-gradient(180deg, var(--employee-bg-soft) 0%, var(--employee-bg) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.subpage-header {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-button {
|
||||||
|
min-width: 104px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-button {
|
||||||
|
min-width: 78px;
|
||||||
|
padding: 0 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-panel,
|
||||||
|
.section-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.block {
|
||||||
|
display: block;
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-avatar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--employee-primary-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-avatar.large {
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-avatar-text {
|
||||||
|
color: var(--employee-primary);
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-avatar.large .member-avatar-text {
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-head-main {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-head-top {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.risk-badge {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.risk-badge.high {
|
||||||
|
background: var(--employee-primary-soft);
|
||||||
|
color: var(--employee-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.risk-badge.medium {
|
||||||
|
background: #fff7ed;
|
||||||
|
color: #d97706;
|
||||||
|
}
|
||||||
|
|
||||||
|
.risk-badge.low {
|
||||||
|
background: #f0fdf4;
|
||||||
|
color: #16a34a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-head-score {
|
||||||
|
color: var(--employee-muted);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-head-score-value {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-head-score-value.high {
|
||||||
|
color: var(--employee-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-head-score-value.medium {
|
||||||
|
color: #d97706;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-head-score-value.low {
|
||||||
|
color: #16a34a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-head-reason {
|
||||||
|
display: block;
|
||||||
|
margin-top: 6px;
|
||||||
|
color: var(--employee-muted);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-stats {
|
||||||
|
display: grid;
|
||||||
|
margin-top: 14px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid #dbe7ff;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #ffffff;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-stat {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
padding: 13px 8px;
|
||||||
|
border-right: 1px solid #dbe7ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-stat:last-child {
|
||||||
|
border-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-stat-value {
|
||||||
|
color: #0f172a;
|
||||||
|
font-size: 23px;
|
||||||
|
font-weight: 900;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-stat-value.medium {
|
||||||
|
color: #c2410c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-stat-value.low {
|
||||||
|
color: #16a34a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-stat-label {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trend-panel,
|
||||||
|
.evidence-panel,
|
||||||
|
.task-panel {
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trend-list {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trend-row {
|
||||||
|
display: grid;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
grid-template-columns: 64px 1fr 34px;
|
||||||
|
padding: 7px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trend-scene {
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--employee-text);
|
||||||
|
font-size: 13px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trend-track {
|
||||||
|
height: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #eef2f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trend-fill {
|
||||||
|
height: 100%;
|
||||||
|
border-radius: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trend-fill.low {
|
||||||
|
background: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trend-fill.medium {
|
||||||
|
background: #f59e0b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trend-fill.high {
|
||||||
|
background: #16a34a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trend-score {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 900;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trend-score.low {
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trend-score.medium {
|
||||||
|
color: #d97706;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trend-score.high {
|
||||||
|
color: #16a34a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.evidence-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.evidence-text {
|
||||||
|
color: var(--employee-text);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.evidence-text.warn {
|
||||||
|
color: #d97706;
|
||||||
|
}
|
||||||
|
|
||||||
|
.evidence-text.ok {
|
||||||
|
color: #16a34a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.evidence-strong {
|
||||||
|
color: var(--employee-primary);
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry-icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: var(--brand);
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry-icon.soft {
|
||||||
|
background: var(--employee-primary-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-list {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px 0;
|
||||||
|
border-bottom: 1px solid #e5edf7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-row:last-child {
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-main {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-title {
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--employee-text);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 900;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-meta {
|
||||||
|
display: block;
|
||||||
|
margin-top: 5px;
|
||||||
|
color: var(--employee-muted);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-status {
|
||||||
|
color: var(--employee-primary);
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-action-primary,
|
||||||
|
.member-action-secondary {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 46px;
|
||||||
|
margin: 0;
|
||||||
|
border-radius: 23px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-action-primary::after,
|
||||||
|
.member-action-secondary::after {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-action-primary {
|
||||||
|
border: 0;
|
||||||
|
background: var(--brand);
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-action-primary:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-action-secondary {
|
||||||
|
border: 1px solid var(--brand);
|
||||||
|
background: #ffffff;
|
||||||
|
color: var(--brand);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -35,14 +35,17 @@
|
|||||||
<view class="risk-item high">
|
<view class="risk-item high">
|
||||||
<text class="risk-value">{{ riskCounts.high }}</text>
|
<text class="risk-value">{{ riskCounts.high }}</text>
|
||||||
<text class="risk-label">高风险</text>
|
<text class="risk-label">高风险</text>
|
||||||
|
<uni-icons type="fire" size="22" color="#dc2626" />
|
||||||
</view>
|
</view>
|
||||||
<view class="risk-item medium">
|
<view class="risk-item medium">
|
||||||
<text class="risk-value">{{ riskCounts.medium }}</text>
|
<text class="risk-value">{{ riskCounts.medium }}</text>
|
||||||
<text class="risk-label">待复盘</text>
|
<text class="risk-label">待复盘</text>
|
||||||
|
<uni-icons type="list" size="22" color="#d97706" />
|
||||||
</view>
|
</view>
|
||||||
<view class="risk-item low">
|
<view class="risk-item low">
|
||||||
<text class="risk-value">{{ riskCounts.low }}</text>
|
<text class="risk-value">{{ riskCounts.low }}</text>
|
||||||
<text class="risk-label">正常</text>
|
<text class="risk-label">正常</text>
|
||||||
|
<uni-icons type="checkmarkempty" size="22" color="#16a34a" />
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
@@ -56,15 +59,14 @@
|
|||||||
</view>
|
</view>
|
||||||
<view v-if="sortedDifficulties.length" class="mistake-list">
|
<view v-if="sortedDifficulties.length" class="mistake-list">
|
||||||
<view v-for="item in sortedDifficulties" :key="`${item.scenarioId}-${item.cause}`" class="mistake-row">
|
<view v-for="item in sortedDifficulties" :key="`${item.scenarioId}-${item.cause}`" class="mistake-row">
|
||||||
<view class="mistake-main">
|
<view class="entry-icon soft">
|
||||||
<view class="mistake-title-row">
|
<uni-icons type="chat" size="18" color="#e60012" />
|
||||||
<text class="mistake-title">{{ item.scenarioName }}</text>
|
|
||||||
<text class="mistake-count">{{ item.count }} 次</text>
|
|
||||||
</view>
|
|
||||||
<text class="mistake-cause">{{ item.cause }} · {{ item.affectedPeople }} 名成员 · 平均 {{ item.averageScore }} 分</text>
|
|
||||||
<text class="mistake-time">最近发生 {{ item.latestTime || '暂无时间' }}</text>
|
|
||||||
</view>
|
</view>
|
||||||
<button class="secondary-button mistake-action" :disabled="assigningDifficultyKey === `${item.scenarioId}-${item.cause}`" @click="assignDifficulty(item)">
|
<view class="mistake-main">
|
||||||
|
<text class="mistake-title">{{ item.scenarioName }}</text>
|
||||||
|
<text class="mistake-cause">{{ item.cause }} · {{ item.count }}次 · {{ item.affectedPeople }}名成员</text>
|
||||||
|
</view>
|
||||||
|
<button class="mistake-action" :disabled="assigningDifficultyKey === `${item.scenarioId}-${item.cause}`" @click="assignDifficulty(item)">
|
||||||
{{ assigningDifficultyKey === `${item.scenarioId}-${item.cause}` ? '派发中' : '一键派专项' }}
|
{{ assigningDifficultyKey === `${item.scenarioId}-${item.cause}` ? '派发中' : '一键派专项' }}
|
||||||
</button>
|
</button>
|
||||||
</view>
|
</view>
|
||||||
@@ -82,62 +84,26 @@
|
|||||||
v-for="member in teamProfiles"
|
v-for="member in teamProfiles"
|
||||||
:key="member.key"
|
:key="member.key"
|
||||||
class="team-row"
|
class="team-row"
|
||||||
:class="member.risk"
|
@click="openMember(member)"
|
||||||
@click="selectMember(member)"
|
|
||||||
>
|
>
|
||||||
<view class="team-main">
|
<text class="risk-badge" :class="member.risk">{{ riskBadge(member.risk) }}</text>
|
||||||
<view class="team-title-row">
|
<view class="member-avatar">
|
||||||
<text class="team-name">{{ member.displayName }}</text>
|
<text class="member-avatar-text">{{ memberInitial(member.displayName) }}</text>
|
||||||
<text class="team-score">{{ member.latestScoreText }}</text>
|
|
||||||
</view>
|
|
||||||
<text class="team-desc">{{ member.reason }}</text>
|
|
||||||
<view class="team-tags">
|
|
||||||
<text class="team-tag">待复盘 {{ member.pendingReview }}</text>
|
|
||||||
<text class="team-tag">待训练 {{ member.pendingAssignment }}</text>
|
|
||||||
<text v-if="member.latestScene" class="team-tag">{{ member.latestScene }}</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
</view>
|
||||||
<button class="secondary-button team-action" @click.stop="assignMember(member)">派专项</button>
|
<view class="team-main">
|
||||||
|
<text class="team-name">{{ member.displayName }}</text>
|
||||||
|
<text class="team-desc">{{ memberStatsLine(member) }}</text>
|
||||||
|
</view>
|
||||||
|
<button
|
||||||
|
v-if="member.risk !== 'low'"
|
||||||
|
class="team-action"
|
||||||
|
@click.stop="assignMember(member)"
|
||||||
|
>派专项</button>
|
||||||
|
<text v-else class="team-arrow">›</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view v-else-if="!loading" class="empty">暂无团队画像数据</view>
|
<view v-else-if="!loading" class="empty">暂无团队画像数据</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view v-if="selectedMember" class="panel member-detail-panel">
|
|
||||||
<view class="section-head">
|
|
||||||
<text class="panel-title">{{ selectedMember.displayName }} · 能力详情</text>
|
|
||||||
<button class="text-button" @click="selectedMember = null">收起</button>
|
|
||||||
</view>
|
|
||||||
<view class="member-summary">
|
|
||||||
<view>
|
|
||||||
<text class="member-score">{{ selectedMember.latestScoreText }}</text>
|
|
||||||
<text class="member-summary-label">最近训练评分</text>
|
|
||||||
</view>
|
|
||||||
<view>
|
|
||||||
<text class="member-score">{{ memberRecords.length }}</text>
|
|
||||||
<text class="member-summary-label">训练次数</text>
|
|
||||||
</view>
|
|
||||||
<view>
|
|
||||||
<text class="member-score">{{ selectedMember.pendingReview }}</text>
|
|
||||||
<text class="member-summary-label">待复盘</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<text class="subsection-title">训练趋势</text>
|
|
||||||
<view v-if="memberRecords.length" class="trend-list">
|
|
||||||
<view v-for="item in memberRecords" :key="item.id || item.time" class="trend-row">
|
|
||||||
<text class="trend-scene">{{ item.scene }}</text>
|
|
||||||
<view class="trend-track"><view class="trend-fill" :style="{ width: `${Math.max(0, Math.min(100, item.score))}%` }" /></view>
|
|
||||||
<text class="trend-score">{{ item.score }}</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<view v-else class="detail-empty">暂无训练评分,先派发一次专项训练</view>
|
|
||||||
<text class="subsection-title">成长证据</text>
|
|
||||||
<view class="evidence-list">
|
|
||||||
<text>已完成 {{ memberRecords.filter((item) => item.status === '已完成').length }} 次训练</text>
|
|
||||||
<text>当前 {{ selectedMember.pendingAssignment }} 项专项待训练</text>
|
|
||||||
<text>{{ selectedMember.reason }}</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</template>
|
</template>
|
||||||
<SupervisorTabBar v-if="loggedIn && !accessDenied" active="team" />
|
<SupervisorTabBar v-if="loggedIn && !accessDenied" active="team" />
|
||||||
</view>
|
</view>
|
||||||
@@ -168,12 +134,10 @@ const loggedIn = ref(false);
|
|||||||
const accessDenied = ref(false);
|
const accessDenied = ref(false);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const message = ref('');
|
const message = ref('');
|
||||||
const selectedMember = ref<TeamProfile | null>(null);
|
|
||||||
const difficultySorts = [
|
const difficultySorts = [
|
||||||
{ key: 'count', label: '出现次数' },
|
{ key: 'count', label: '出现次数' },
|
||||||
{ key: 'people', label: '涉及人数' },
|
{ key: 'people', label: '涉及人数' },
|
||||||
{ key: 'score', label: '低分优先' },
|
{ key: 'score', label: '低分优先' }
|
||||||
{ key: 'latest', label: '最近发生' }
|
|
||||||
] as const;
|
] as const;
|
||||||
type DifficultySort = typeof difficultySorts[number]['key'];
|
type DifficultySort = typeof difficultySorts[number]['key'];
|
||||||
const difficultySort = ref<DifficultySort>('count');
|
const difficultySort = ref<DifficultySort>('count');
|
||||||
@@ -189,16 +153,9 @@ const riskCounts = computed(() => teamProfiles.value.reduce(
|
|||||||
},
|
},
|
||||||
{ high: 0, medium: 0, low: 0 }
|
{ high: 0, medium: 0, low: 0 }
|
||||||
));
|
));
|
||||||
const memberRecords = computed(() => {
|
|
||||||
if (!selectedMember.value) return [];
|
|
||||||
return records.value
|
|
||||||
.filter((item) => item.extPartyId === selectedMember.value?.extPartyId)
|
|
||||||
.slice(0, 6);
|
|
||||||
});
|
|
||||||
const sortedDifficulties = computed(() => [...mistakes.value].sort((a, b) => {
|
const sortedDifficulties = computed(() => [...mistakes.value].sort((a, b) => {
|
||||||
if (difficultySort.value === 'people') return b.affectedPeople - a.affectedPeople;
|
if (difficultySort.value === 'people') return b.affectedPeople - a.affectedPeople;
|
||||||
if (difficultySort.value === 'score') return a.averageScore - b.averageScore;
|
if (difficultySort.value === 'score') return a.averageScore - b.averageScore;
|
||||||
if (difficultySort.value === 'latest') return String(b.latestTime).localeCompare(String(a.latestTime));
|
|
||||||
return b.count - a.count;
|
return b.count - a.count;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -207,7 +164,6 @@ const clearData = () => {
|
|||||||
assignments.value = [];
|
assignments.value = [];
|
||||||
members.value = [];
|
members.value = [];
|
||||||
mistakes.value = [];
|
mistakes.value = [];
|
||||||
selectedMember.value = null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const refreshAuthState = () => {
|
const refreshAuthState = () => {
|
||||||
@@ -268,8 +224,19 @@ const assignMember = (member: TeamProfile) => {
|
|||||||
uni.navigateTo({ url: `/pages/supervisor/assign/index?${query}` });
|
uni.navigateTo({ url: `/pages/supervisor/assign/index?${query}` });
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectMember = (member: TeamProfile) => {
|
const riskBadge = (risk: TeamProfile['risk']) => (risk === 'high' ? '高' : risk === 'medium' ? '中' : '低');
|
||||||
selectedMember.value = selectedMember.value?.key === member.key ? null : member;
|
|
||||||
|
const memberInitial = (name: string) => name.trim().charAt(0) || '员';
|
||||||
|
|
||||||
|
const memberStatsLine = (member: TeamProfile) =>
|
||||||
|
`最近${member.latestScoreText} · 待复盘${member.pendingReview} · 待训练${member.pendingAssignment}`;
|
||||||
|
|
||||||
|
const openMember = (member: TeamProfile) => {
|
||||||
|
const query = [
|
||||||
|
`extPartyId=${encodeURIComponent(member.extPartyId)}`,
|
||||||
|
`name=${encodeURIComponent(member.displayName)}`
|
||||||
|
].join('&');
|
||||||
|
uni.navigateTo({ url: `/pages/supervisor/member/index?${query}` });
|
||||||
};
|
};
|
||||||
|
|
||||||
const tomorrow = () => {
|
const tomorrow = () => {
|
||||||
@@ -375,8 +342,7 @@ onUnload(invalidateRequests);
|
|||||||
}
|
}
|
||||||
|
|
||||||
.login-panel,
|
.login-panel,
|
||||||
.section-head,
|
.section-head {
|
||||||
.team-title-row {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -401,28 +367,26 @@ onUnload(invalidateRequests);
|
|||||||
}
|
}
|
||||||
|
|
||||||
.risk-item {
|
.risk-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
padding: 13px 8px;
|
padding: 13px 8px;
|
||||||
border-right: 1px solid #dbe7ff;
|
border-right: 1px solid #dbe7ff;
|
||||||
text-align: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.risk-item:last-child {
|
.risk-item:last-child {
|
||||||
border-right: 0;
|
border-right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.risk-value,
|
|
||||||
.risk-label {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.risk-value {
|
.risk-value {
|
||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
font-size: 23px;
|
font-size: 23px;
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.risk-label {
|
.risk-label {
|
||||||
margin-top: 4px;
|
|
||||||
color: #64748b;
|
color: #64748b;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
@@ -455,6 +419,7 @@ onUnload(invalidateRequests);
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
min-height: 34px;
|
min-height: 34px;
|
||||||
|
margin: 0;
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
border: 1px solid #d7e0eb;
|
border: 1px solid #d7e0eb;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
@@ -463,6 +428,10 @@ onUnload(invalidateRequests);
|
|||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.difficulty-sort button::after {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.difficulty-sort button.active {
|
.difficulty-sort button.active {
|
||||||
border-color: #ef9da5;
|
border-color: #ef9da5;
|
||||||
background: var(--employee-primary-soft);
|
background: var(--employee-primary-soft);
|
||||||
@@ -477,7 +446,6 @@ onUnload(invalidateRequests);
|
|||||||
.mistake-row {
|
.mistake-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
padding: 12px 0;
|
padding: 12px 0;
|
||||||
border-bottom: 1px solid #e5edf7;
|
border-bottom: 1px solid #e5edf7;
|
||||||
@@ -487,42 +455,41 @@ onUnload(invalidateRequests);
|
|||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.entry-icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: var(--brand);
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry-icon.soft {
|
||||||
|
background: var(--employee-primary-soft);
|
||||||
|
}
|
||||||
|
|
||||||
.mistake-main {
|
.mistake-main {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mistake-title-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mistake-title,
|
.mistake-title,
|
||||||
.mistake-cause,
|
.mistake-cause {
|
||||||
.mistake-time {
|
|
||||||
display: block;
|
display: block;
|
||||||
}
|
|
||||||
|
|
||||||
.mistake-title {
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
color: var(--employee-text);
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 900;
|
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mistake-count {
|
.mistake-title {
|
||||||
flex: 0 0 auto;
|
color: var(--employee-text);
|
||||||
color: #b91c1c;
|
font-size: 14px;
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mistake-cause,
|
.mistake-cause {
|
||||||
.mistake-time {
|
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
color: var(--employee-muted);
|
color: var(--employee-muted);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
@@ -530,11 +497,29 @@ onUnload(invalidateRequests);
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mistake-action {
|
.mistake-action {
|
||||||
min-width: 64px;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
min-height: 34px;
|
min-height: 34px;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
padding: 0 8px;
|
margin: 0;
|
||||||
|
padding: 0 12px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 17px;
|
||||||
|
background: var(--brand);
|
||||||
|
color: #ffffff;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mistake-action::after {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mistake-action:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.team-list {
|
.team-list {
|
||||||
@@ -544,29 +529,57 @@ onUnload(invalidateRequests);
|
|||||||
.team-row {
|
.team-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
padding: 14px 0 14px 12px;
|
padding: 13px 0;
|
||||||
border: 1px solid #e5edf7;
|
border-bottom: 1px solid #e5edf7;
|
||||||
border-left: 4px solid var(--employee-primary);
|
|
||||||
border-radius: 14px;
|
|
||||||
background: #ffffff;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.team-row:last-child {
|
.team-row:last-child {
|
||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.team-row.high {
|
.risk-badge {
|
||||||
border-left-color: #dc2626;
|
flex: 0 0 auto;
|
||||||
|
min-width: 22px;
|
||||||
|
padding: 3px 6px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1.3;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.team-row.medium {
|
.risk-badge.high {
|
||||||
border-left-color: #f59e0b;
|
background: var(--employee-primary-soft);
|
||||||
|
color: var(--employee-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.team-row.low {
|
.risk-badge.medium {
|
||||||
border-left-color: #d7dde5;
|
background: #fff7ed;
|
||||||
|
color: #d97706;
|
||||||
|
}
|
||||||
|
|
||||||
|
.risk-badge.low {
|
||||||
|
background: #f0fdf4;
|
||||||
|
color: #16a34a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-avatar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--employee-primary-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-avatar-text {
|
||||||
|
color: var(--employee-primary);
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 900;
|
||||||
}
|
}
|
||||||
|
|
||||||
.team-main {
|
.team-main {
|
||||||
@@ -575,7 +588,7 @@ onUnload(invalidateRequests);
|
|||||||
}
|
}
|
||||||
|
|
||||||
.team-name {
|
.team-name {
|
||||||
min-width: 0;
|
display: block;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
@@ -584,148 +597,44 @@ onUnload(invalidateRequests);
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.team-score {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
color: #1d4ed8;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 900;
|
|
||||||
}
|
|
||||||
|
|
||||||
.team-desc {
|
.team-desc {
|
||||||
display: block;
|
display: block;
|
||||||
margin-top: 6px;
|
margin-top: 5px;
|
||||||
|
overflow: hidden;
|
||||||
color: #475569;
|
color: #475569;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 1.45;
|
line-height: 1.45;
|
||||||
}
|
|
||||||
|
|
||||||
.team-tags {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 6px;
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.team-tag {
|
|
||||||
padding: 4px 7px;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: var(--employee-primary-soft);
|
|
||||||
color: var(--employee-primary);
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 800;
|
|
||||||
}
|
|
||||||
|
|
||||||
.team-action {
|
|
||||||
min-width: 72px;
|
|
||||||
min-height: 38px;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
padding: 0 10px;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.member-detail-panel {
|
|
||||||
margin-top: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-button {
|
|
||||||
min-height: 32px;
|
|
||||||
padding: 0;
|
|
||||||
background: transparent;
|
|
||||||
color: var(--employee-primary);
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 800;
|
|
||||||
}
|
|
||||||
|
|
||||||
.member-summary {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, 1fr);
|
|
||||||
gap: 8px;
|
|
||||||
margin-top: 14px;
|
|
||||||
padding: 12px 8px;
|
|
||||||
border-radius: 14px;
|
|
||||||
background: var(--employee-primary-soft);
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.member-score,
|
|
||||||
.member-summary-label,
|
|
||||||
.trend-scene,
|
|
||||||
.trend-score,
|
|
||||||
.subsection-title,
|
|
||||||
.evidence-list text {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.member-score {
|
|
||||||
color: var(--employee-primary);
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: 900;
|
|
||||||
}
|
|
||||||
|
|
||||||
.member-summary-label {
|
|
||||||
margin-top: 4px;
|
|
||||||
color: var(--employee-muted);
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subsection-title {
|
|
||||||
margin-top: 16px;
|
|
||||||
color: var(--employee-text);
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 900;
|
|
||||||
}
|
|
||||||
|
|
||||||
.trend-list {
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.trend-row {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 92px 1fr 30px;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.trend-scene,
|
|
||||||
.trend-score {
|
|
||||||
overflow: hidden;
|
|
||||||
color: var(--employee-muted);
|
|
||||||
font-size: 12px;
|
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.trend-score {
|
.team-action {
|
||||||
color: var(--employee-primary);
|
|
||||||
font-weight: 900;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.trend-track {
|
|
||||||
height: 8px;
|
|
||||||
overflow: hidden;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: #e7eefc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.trend-fill {
|
|
||||||
height: 100%;
|
|
||||||
border-radius: inherit;
|
|
||||||
background: linear-gradient(90deg, #6f9cff, #2f6bff);
|
|
||||||
}
|
|
||||||
|
|
||||||
.evidence-list {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
align-items: center;
|
||||||
gap: 6px;
|
justify-content: center;
|
||||||
margin-top: 8px;
|
min-height: 34px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 14px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 17px;
|
||||||
|
background: var(--brand);
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.evidence-list text,
|
.team-action::after {
|
||||||
.detail-empty {
|
border: 0;
|
||||||
color: var(--employee-muted);
|
}
|
||||||
font-size: 12px;
|
|
||||||
line-height: 1.5;
|
.team-arrow {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
padding: 0 4px;
|
||||||
|
color: #c0c4cc;
|
||||||
|
font-size: 20px;
|
||||||
|
line-height: 1;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user