|
@@ -0,0 +1,217 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <div class="p-4 md:p-8">
|
|
|
|
|
+ <a-card class="rounded-xl shadow-md !border-0">
|
|
|
|
|
+ <a-tabs v-model:activeKey="activeTab" @change="handleTabChange">
|
|
|
|
|
+ <!-- 生产进度 -->
|
|
|
|
|
+ <a-tab-pane key="production" tab="生产进度">
|
|
|
|
|
+ <a-input-search
|
|
|
|
|
+ v-model:value="prodKeyword"
|
|
|
|
|
+ placeholder="搜索款号或工单号"
|
|
|
|
|
+ class="w-full sm:w-80 mb-4"
|
|
|
|
|
+ @search="loadProduction"
|
|
|
|
|
+ />
|
|
|
|
|
+ <a-table
|
|
|
|
|
+ :columns="prodColumns"
|
|
|
|
|
+ :dataSource="productionData"
|
|
|
|
|
+ :loading="loadingProduction"
|
|
|
|
|
+ rowKey="work_order_id"
|
|
|
|
|
+ >
|
|
|
|
|
+ <template #progress="{ record }">
|
|
|
|
|
+ <a-progress :percent="Math.round(record.progress)" :status="record.progress >= 100 ? 'success' : 'active'" />
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </a-table>
|
|
|
|
|
+ </a-tab-pane>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 员工排行 -->
|
|
|
|
|
+ <a-tab-pane key="ranking" tab="员工排行">
|
|
|
|
|
+ <a-space class="mb-4" wrap>
|
|
|
|
|
+ <a-select v-model:value="rankingPeriod" placeholder="统计周期" class="w-40" @change="loadRanking">
|
|
|
|
|
+ <a-select-option value="month">本月</a-select-option>
|
|
|
|
|
+ <a-select-option value="week">本周</a-select-option>
|
|
|
|
|
+ <a-select-option value="day">今日</a-select-option>
|
|
|
|
|
+ </a-select>
|
|
|
|
|
+ </a-space>
|
|
|
|
|
+ <a-table
|
|
|
|
|
+ :columns="rankColumns"
|
|
|
|
|
+ :dataSource="rankingData"
|
|
|
|
|
+ :loading="loadingRanking"
|
|
|
|
|
+ rowKey="user_id"
|
|
|
|
|
+ >
|
|
|
|
|
+ <template #rank="{ record }">
|
|
|
|
|
+ <a-tag :color="getRankColor(record.rank)" class="!rounded-full">
|
|
|
|
|
+ 第 {{ record.rank }} 名
|
|
|
|
|
+ </a-tag>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ <template #amount="{ record }">
|
|
|
|
|
+ ¥{{ record.total_amount.toFixed(2) }}
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </a-table>
|
|
|
|
|
+ </a-tab-pane>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 进度矩阵 -->
|
|
|
|
|
+ <a-tab-pane key="matrix" tab="进度矩阵">
|
|
|
|
|
+ <a-space class="mb-4" wrap>
|
|
|
|
|
+ <a-select v-model:value="groupBy" placeholder="分组维度" class="w-40" @change="loadMatrix">
|
|
|
|
|
+ <a-select-option value="work_order">按工单</a-select-option>
|
|
|
|
|
+ <a-select-option value="style">按款号</a-select-option>
|
|
|
|
|
+ <a-select-option value="batch">按批次</a-select-option>
|
|
|
|
|
+ <a-select-option value="name">按员工</a-select-option>
|
|
|
|
|
+ </a-select>
|
|
|
|
|
+ <a-input-search
|
|
|
|
|
+ v-model:value="matrixKeyword"
|
|
|
|
|
+ placeholder="搜索关键词"
|
|
|
|
|
+ class="w-full sm:w-64"
|
|
|
|
|
+ @search="loadMatrix"
|
|
|
|
|
+ />
|
|
|
|
|
+ </a-space>
|
|
|
|
|
+ <a-table
|
|
|
|
|
+ :columns="matrixColumns"
|
|
|
|
|
+ :dataSource="matrixData"
|
|
|
|
|
+ :loading="loadingMatrix"
|
|
|
|
|
+ rowKey="group_key"
|
|
|
|
|
+ :scroll="{ x: 800 }"
|
|
|
|
|
+ >
|
|
|
|
|
+ <template #progress="{ record }">
|
|
|
|
|
+ <a-progress
|
|
|
|
|
+ :percent="record.total_qty > 0 ? Math.round((record.completed_qty / record.total_qty) * 100) : 0"
|
|
|
|
|
+ :status="record.completed_qty >= record.total_qty ? 'success' : 'active'"
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ />
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </a-table>
|
|
|
|
|
+ </a-tab-pane>
|
|
|
|
|
+ </a-tabs>
|
|
|
|
|
+ </a-card>
|
|
|
|
|
+ </div>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script setup lang="ts">
|
|
|
|
|
+import { ref, onMounted } from 'vue';
|
|
|
|
|
+import { message } from 'ant-design-vue';
|
|
|
|
|
+import { StatsApi, createApiClient } from '@smartcut/api-client';
|
|
|
|
|
+import { STORAGE_KEYS } from '@smartcut/shared-utils';
|
|
|
|
|
+import { getApiBaseUrl } from '@/apiConfig';
|
|
|
|
|
+import type { ProductionData, RankingData, ProgressMatrixData } from '@smartcut/types';
|
|
|
|
|
+
|
|
|
|
|
+const apiClient = createApiClient({
|
|
|
|
|
+ baseURL: getApiBaseUrl(),
|
|
|
|
|
+ tokenKey: STORAGE_KEYS.FACTORY_TOKEN
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+const statsApi = new StatsApi(apiClient);
|
|
|
|
|
+
|
|
|
|
|
+const activeTab = ref('production');
|
|
|
|
|
+
|
|
|
|
|
+// 生产进度
|
|
|
|
|
+const productionData = ref<ProductionData[]>([]);
|
|
|
|
|
+const loadingProduction = ref(false);
|
|
|
|
|
+const prodKeyword = ref('');
|
|
|
|
|
+
|
|
|
|
|
+const prodColumns = [
|
|
|
|
|
+ { title: '工单号', dataIndex: 'order_no', key: 'order_no', width: 150 },
|
|
|
|
|
+ { title: '款号', dataIndex: 'style', key: 'style', width: 120 },
|
|
|
|
|
+ { title: '款号名称', dataIndex: 'style_name', key: 'style_name', width: 200 },
|
|
|
|
|
+ { title: '总数量', dataIndex: 'total_qty', key: 'total_qty', width: 100 },
|
|
|
|
|
+ { title: '已完成', dataIndex: 'completed_qty', key: 'completed_qty', width: 100 },
|
|
|
|
|
+ { title: '进度', key: 'progress', width: 200, slots: { customRender: 'progress' } }
|
|
|
|
|
+];
|
|
|
|
|
+
|
|
|
|
|
+// 员工排行
|
|
|
|
|
+const rankingData = ref<RankingData[]>([]);
|
|
|
|
|
+const loadingRanking = ref(false);
|
|
|
|
|
+const rankingPeriod = ref('month');
|
|
|
|
|
+
|
|
|
|
|
+const rankColumns = [
|
|
|
|
|
+ { title: '排名', key: 'rank', width: 100, slots: { customRender: 'rank' } },
|
|
|
|
|
+ { title: '员工', dataIndex: 'user_name', key: 'user_name', width: 150 },
|
|
|
|
|
+ { title: '总数量', dataIndex: 'total_quantity', key: 'total_quantity', width: 120 },
|
|
|
|
|
+ { title: '总金额', key: 'amount', width: 150, slots: { customRender: 'amount' } }
|
|
|
|
|
+];
|
|
|
|
|
+
|
|
|
|
|
+// 进度矩阵
|
|
|
|
|
+const matrixData = ref<ProgressMatrixData[]>([]);
|
|
|
|
|
+const loadingMatrix = ref(false);
|
|
|
|
|
+const groupBy = ref('work_order');
|
|
|
|
|
+const matrixKeyword = ref('');
|
|
|
|
|
+
|
|
|
|
|
+const matrixColumns = ref<any[]>([
|
|
|
|
|
+ { title: '分组', dataIndex: 'group_name', key: 'group_name', width: 200 },
|
|
|
|
|
+ { title: '总数量', dataIndex: 'total_qty', key: 'total_qty', width: 100 },
|
|
|
|
|
+ { title: '已完成', dataIndex: 'completed_qty', key: 'completed_qty', width: 100 },
|
|
|
|
|
+ { title: '进度', key: 'progress', width: 200, slots: { customRender: 'progress' } }
|
|
|
|
|
+]);
|
|
|
|
|
+
|
|
|
|
|
+function getRankColor(rank: number): string {
|
|
|
|
|
+ if (rank === 1) return 'gold';
|
|
|
|
|
+ if (rank === 2) return 'silver';
|
|
|
|
|
+ if (rank === 3) return 'orange';
|
|
|
|
|
+ return 'default';
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function loadProduction() {
|
|
|
|
|
+ loadingProduction.value = true;
|
|
|
|
|
+ try {
|
|
|
|
|
+ productionData.value = await statsApi.getProduction();
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ message.error('加载生产进度失败');
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ loadingProduction.value = false;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function loadRanking() {
|
|
|
|
|
+ loadingRanking.value = true;
|
|
|
|
|
+ try {
|
|
|
|
|
+ rankingData.value = await statsApi.getRanking({ period: rankingPeriod.value });
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ message.error('加载员工排行失败');
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ loadingRanking.value = false;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function loadMatrix() {
|
|
|
|
|
+ loadingMatrix.value = true;
|
|
|
|
|
+ try {
|
|
|
|
|
+ matrixData.value = await statsApi.getProgressMatrix({
|
|
|
|
|
+ group_by: groupBy.value,
|
|
|
|
|
+ keyword: matrixKeyword.value || undefined
|
|
|
|
|
+ });
|
|
|
|
|
+ // 动态生成工序列
|
|
|
|
|
+ const processSet = new Set<string>();
|
|
|
|
|
+ matrixData.value.forEach(item => {
|
|
|
|
|
+ Object.keys(item.process_progress || {}).forEach(k => processSet.add(k));
|
|
|
|
|
+ });
|
|
|
|
|
+ const processCols = Array.from(processSet).map(pname => ({
|
|
|
|
|
+ title: pname,
|
|
|
|
|
+ key: `proc_${pname}`,
|
|
|
|
|
+ width: 100,
|
|
|
|
|
+ customRender: ({ record }: { record: ProgressMatrixData }) => {
|
|
|
|
|
+ const val = record.process_progress?.[pname];
|
|
|
|
|
+ return val != null ? val : '-';
|
|
|
|
|
+ }
|
|
|
|
|
+ }));
|
|
|
|
|
+ matrixColumns.value = [
|
|
|
|
|
+ { title: '分组', dataIndex: 'group_name', key: 'group_name', width: 200 },
|
|
|
|
|
+ { title: '总数量', dataIndex: 'total_qty', key: 'total_qty', width: 100 },
|
|
|
|
|
+ { title: '已完成', dataIndex: 'completed_qty', key: 'completed_qty', width: 100 },
|
|
|
|
|
+ ...processCols,
|
|
|
|
|
+ { title: '进度', key: 'progress', width: 200, slots: { customRender: 'progress' } }
|
|
|
|
|
+ ];
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ message.error('加载进度矩阵失败');
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ loadingMatrix.value = false;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function handleTabChange(key: string) {
|
|
|
|
|
+ if (key === 'production' && productionData.value.length === 0) loadProduction();
|
|
|
|
|
+ if (key === 'ranking' && rankingData.value.length === 0) loadRanking();
|
|
|
|
|
+ if (key === 'matrix' && matrixData.value.length === 0) loadMatrix();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+onMounted(() => {
|
|
|
|
|
+ loadProduction();
|
|
|
|
|
+});
|
|
|
|
|
+</script>
|