Commit d3944a78 by Neo Turing

本地

parent 422ec1b4
<!doctype html>
<html lang="zh-cmn-Hans">
<!doctype html>
<html lang="zh-cmn-Hans">
<head>
<meta name="buildTime" content="2025-04-25 17:56:27">
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="color-scheme" content="light dark" />
<title>VueDashboard</title>
<script type="module" crossorigin src="/Content/VueDashboardUi/VueDashboard1/assets/index-CsI39EZy.js"></script>
<link rel="stylesheet" crossorigin href="/Content/VueDashboardUi/VueDashboard1/assets/index-DhhgO4aJ.css">
</head>
<body>
<div id="app"></div>
</body>
</html>
<meta name="buildTime" content="2025-06-10 14:01:51">
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="color-scheme" content="light dark" />
<title>VueDashboard</title>
<script type="module" crossorigin src="/Content/VueDashboardUi/VueDashboard1/assets/index-DQeQ-ejl.js"></script>
<link rel="stylesheet" crossorigin href="/Content/VueDashboardUi/VueDashboard1/assets/index-CIbUL_fo.css">
</head>
<body>
<div id="app"></div>
</body>
</html>
......
......@@ -19,6 +19,8 @@ export const views: Record<LastLevelRouteKey, RouteComponent | (() => Promise<Ro
404: () => import("@/views/_builtin/404/index.vue"),
500: () => import("@/views/_builtin/500/index.vue"),
"iframe-page": () => import("@/views/_builtin/iframe-page/[url].vue"),
lac_sample: () => import("@/views/_builtin/lac/Sample/index.vue"),
lac_supplier_applylist: () => import("@/views/_builtin/lac/Supplier/ApplyList/index.vue"),
login: () => import("@/views/_builtin/login/index.vue"),
test: () => import("@/views/_builtin/test/index.vue"),
test_test1: () => import("@/views/_builtin/test/test1/index.vue"),
......
......@@ -63,6 +63,45 @@ export const generatedRoutes: GeneratedRoute[] = [
}
},
{
name: 'lac',
path: '/lac',
component: 'layout.base',
meta: {
title: 'lac',
i18nKey: 'route.lac'
},
children: [
{
name: 'lac_sample',
path: '/lac/sample',
component: 'view.lac_sample',
meta: {
title: 'lac_sample',
i18nKey: 'route.lac_sample'
}
},
{
name: 'lac_supplier',
path: '/lac/supplier',
meta: {
title: 'lac_supplier',
i18nKey: 'route.lac_supplier'
},
children: [
{
name: 'lac_supplier_applylist',
path: '/lac/supplier/applylist',
component: 'view.lac_supplier_applylist',
meta: {
title: 'lac_supplier_applylist',
i18nKey: 'route.lac_supplier_applylist'
}
}
]
}
]
},
{
name: 'login',
path: '/login/:module(pwd-login|code-login|register|reset-pwd|bind-wechat)?',
component: 'layout.blank$view.login',
......
......@@ -181,6 +181,10 @@ const routeMap: RouteMap = {
"500": "/500",
"home": "/home",
"iframe-page": "/iframe-page/:url",
"lac": "/lac",
"lac_sample": "/lac/sample",
"lac_supplier": "/lac/supplier",
"lac_supplier_applylist": "/lac/supplier/applylist",
"login": "/login/:module(pwd-login|code-login|register|reset-pwd|bind-wechat)?",
"test": "/test",
"test_test1": "/test/test1",
......
......@@ -27,12 +27,17 @@ declare module 'vue' {
LangSwitch: typeof import('./../components/common/lang-switch.vue')['default']
LookForward: typeof import('./../components/custom/look-forward.vue')['default']
MenuToggler: typeof import('./../components/common/menu-toggler.vue')['default']
NAlert: typeof import('naive-ui')['NAlert']
NBreadcrumb: typeof import('naive-ui')['NBreadcrumb']
NBreadcrumbItem: typeof import('naive-ui')['NBreadcrumbItem']
NButton: typeof import('naive-ui')['NButton']
NCard: typeof import('naive-ui')['NCard']
NCheckbox: typeof import('naive-ui')['NCheckbox']
NColorPicker: typeof import('naive-ui')['NColorPicker']
NDataTable: typeof import('naive-ui')['NDataTable']
NDatePicker: typeof import('naive-ui')['NDatePicker']
NDescriptions: typeof import('naive-ui')['NDescriptions']
NDescriptionsItem: typeof import('naive-ui')['NDescriptionsItem']
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
NDivider: typeof import('naive-ui')['NDivider']
NDrawer: typeof import('naive-ui')['NDrawer']
......@@ -43,6 +48,8 @@ declare module 'vue' {
NFormItem: typeof import('naive-ui')['NFormItem']
NGi: typeof import('naive-ui')['NGi']
NGrid: typeof import('naive-ui')['NGrid']
NGridItem: typeof import('naive-ui')['NGridItem']
NIcon: typeof import('naive-ui')['NIcon']
NInput: typeof import('naive-ui')['NInput']
NInputGroup: typeof import('naive-ui')['NInputGroup']
NInputNumber: typeof import('naive-ui')['NInputNumber']
......@@ -51,14 +58,23 @@ declare module 'vue' {
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
NModal: typeof import('naive-ui')['NModal']
NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
NPopconfirm: typeof import('naive-ui')['NPopconfirm']
NProgress: typeof import('naive-ui')['NProgress']
NRadio: typeof import('naive-ui')['NRadio']
NRadioGroup: typeof import('naive-ui')['NRadioGroup']
NScrollbar: typeof import('naive-ui')['NScrollbar']
NSelect: typeof import('naive-ui')['NSelect']
NSpace: typeof import('naive-ui')['NSpace']
NStatistic: typeof import('naive-ui')['NStatistic']
NStep: typeof import('naive-ui')['NStep']
NSteps: typeof import('naive-ui')['NSteps']
NSwitch: typeof import('naive-ui')['NSwitch']
NTab: typeof import('naive-ui')['NTab']
NTabs: typeof import('naive-ui')['NTabs']
NTag: typeof import('naive-ui')['NTag']
NText: typeof import('naive-ui')['NText']
NTooltip: typeof import('naive-ui')['NTooltip']
NUpload: typeof import('naive-ui')['NUpload']
NWatermark: typeof import('naive-ui')['NWatermark']
PinToggler: typeof import('./../components/common/pin-toggler.vue')['default']
ReloadButton: typeof import('./../components/common/reload-button.vue')['default']
......
......@@ -35,6 +35,10 @@ declare module "@elegant-router/types" {
"500": "/500";
"home": "/home";
"iframe-page": "/iframe-page/:url";
"lac": "/lac";
"lac_sample": "/lac/sample";
"lac_supplier": "/lac/supplier";
"lac_supplier_applylist": "/lac/supplier/applylist";
"login": "/login/:module(pwd-login|code-login|register|reset-pwd|bind-wechat)?";
"test": "/test";
"test_test1": "/test/test1";
......@@ -89,6 +93,7 @@ declare module "@elegant-router/types" {
| "500"
| "home"
| "iframe-page"
| "lac"
| "login"
| "test"
>;
......@@ -113,6 +118,8 @@ declare module "@elegant-router/types" {
| "404"
| "500"
| "iframe-page"
| "lac_sample"
| "lac_supplier_applylist"
| "login"
| "test"
| "test_test1"
......
<script setup lang="ts">
import { reactive, ref } from 'vue';
import { request } from '@/service/request';
import ApplicationInformation from './applicationInformation.vue';
import FileReview from './fileReview.vue';
import ComparisonTest from './comparisonTest.vue';
// 定义文件项接口
interface FileItem {
id: number;
name: string;
type: string;
status: string;
size?: string;
StandardKvid: string;
Kvid: string;
}
// 定义测试项接口
interface TestItem {
id: number;
name: string;
selected: boolean;
}
// 定义emit事件
const emit = defineEmits(['close-and-refresh']);
// 返回列表并刷新数据
const handleClose = () => {
console.log('返回列表并刷新数据');
// 发送关闭并刷新数据的信号
emit('close-and-refresh');
};
// 主体框架内置的第三方方法通过window对象暴露、供外部组件使用
// 获取消息提示
const message = (window as any).$message;
// 当前步骤
const currentStep = ref(0);
// 加载状态
const loading = ref(false);
// 表单数据
const formModel = reactive({
labName: '',
labAddress: '',
contactName: '',
contactPhone: '',
contactEmail: '',
applyDate: null as number | null,
applyTime: null as number | null,
remarks: '',
kvid: ''
});
// 文件列表
const fileList = ref<FileItem[]>([]);
// 测试项目
const testItems = ref<TestItem[]>([
{ id: 1, name: '化学成分分析', selected: false },
{ id: 2, name: '物理性能测试', selected: false },
{ id: 3, name: '微生物检测', selected: false },
{ id: 4, name: '无损检测', selected: false },
{ id: 5, name: '电气安全测试', selected: false },
{ id: 6, name: '环境模拟测试', selected: false }
]);
// 保存为草稿状态
const saveAsDraft = ref(false);
// 验证规则
const rules = {
labName: {
required: true,
trigger: ['blur', 'input'],
message: '请输入实验室名称'
}
};
// 表单引用
const formRef = ref<any>(null);
// 评审资源可用性数据
const availabilityData = reactive({
morning: { total: 5, booked: 2 },
afternoon: { total: 5, booked: 1 },
evening: { total: 3, booked: 3 }
});
// 提取复杂的创建申请逻辑到单独函数
const createApplication = async () => {
try {
// 显示加载状态
loading.value = true;
// 构建请求参数
const requestData = {
// 这里是Item参数内容,用户可以自定义
Item: {
SupplierName: '江苏省纺织产品质量监督检验研究院',
PayeeName: '江苏省纺织产品质量监督检验研究院',
DemanderName: (window as any).$KiviiContext?.CurrentMember?.OrganizationName || '默认组织名称',
DemanderKvid: (window as any).$KiviiContext?.CurrentMember?.OrganizationKvid || '',
OperatorName: (window as any).$KiviiContext?.CurrentMember?.FullName || '默认用户',
OperatorKvid: (window as any).$KiviiContext?.CurrentMember?.Kvid || '',
TestNeedJudge: true,
Quantity: '1',
DealDate: formModel.applyDate,
Status: 0,
SampleManufacturerName: (window as any).$KiviiContext?.CurrentMember?.DepartmentName || '默认部门',
SampleManufacturerKvid: (window as any).$KiviiContext?.CurrentMember?.DepartmentKvid || '',
SampleManufacturerContactName: formModel.contactName,
SampleManufacturerContactNumber: formModel.contactPhone,
SampleManufacturerAddress: formModel.labAddress,
SampleName: formModel.labName,
TemplateName: '默认模板',
Remark: formModel.remarks
}
};
// 调用创建接口
const { data, error } = await request({
url: '/Restful/Kivii.Lims.Entities.Report/Create.json',
method: 'post',
data: requestData
});
if (error) {
console.error('保存申请信息失败:', error);
message.error('保存失败,请重试');
return;
}
if (data.Results.length > 0) {
// 保留完整的原始数据结构,同时保持展示字段的兼容性
Object.assign(formModel, {
...data.Results[0], // 保留所有原始字段
kvid: data.Results[0].Kvid, // 保持kvid字段兼容性
labName: formModel.labName, // 保持表单字段
labAddress: formModel.labAddress,
contactName: formModel.contactName,
contactPhone: formModel.contactPhone,
applyDate: formModel.applyDate,
remarks: formModel.remarks
});
message.success('申请信息保存成功');
// 保存成功后进入下一步
currentStep.value += 1;
} else {
message.error('保存失败,请重试');
}
} catch (error) {
console.error('保存申请信息失败:', error);
message.error('系统错误,请稍后重试');
} finally {
// 隐藏加载状态
loading.value = false;
}
};
// 提取复杂的创建申请逻辑到单独函数
const updateApplication = async () => {
try {
// 显示加载状态
loading.value = true;
// 构建请求参数
const requestData = {
// 这里是Item参数内容,用户可以自定义
Item: {
DealDate: formModel.applyDate,
SampleManufacturerContactName: formModel.contactName,
SampleManufacturerContactNumber: formModel.contactPhone,
SampleManufacturerAddress: formModel.labAddress,
SampleName: formModel.labName,
Remark: formModel.remarks,
Kvid: formModel.kvid
}
};
// 调用创建接口
const { data, error } = await request({
url: '/Restful/Kivii.Lims.Entities.Report/Update.json',
method: 'post',
data: requestData
});
if (error) {
console.error('保存申请信息失败:', error);
message.error('保存失败,请重试');
return;
}
if (data.Results.length > 0) {
// 保留完整的原始数据结构,同时保持展示字段的兼容性
Object.assign(formModel, {
...data.Results[0], // 保留所有原始字段
kvid: data.Results[0].Kvid, // 保持kvid字段兼容性
labName: formModel.labName, // 保持表单字段
labAddress: formModel.labAddress,
contactName: formModel.contactName,
contactPhone: formModel.contactPhone,
applyDate: formModel.applyDate,
remarks: formModel.remarks
});
message.success('申请信息保存成功');
// 保存成功后进入下一步
currentStep.value += 1;
} else {
message.error('保存失败,请重试');
}
} catch (error) {
console.error('保存申请信息失败:', error);
message.error('系统错误,请稍后重试');
} finally {
// 隐藏加载状态
loading.value = false;
}
};
// 下一步
function nextStep() {
if (currentStep.value < 2) {
// 如果当前是第一步(申请信息填写),则调用创建接口
if (currentStep.value === 0) {
// 如果已经存在Kvid,则直接进入下一步
if (formModel.kvid) {
updateApplication();
return;
}
// 验证表单
formRef.value?.validate(async (errors: any) => {
if (errors) {
message.error('请完善必填信息');
return;
}
await createApplication();
});
} else {
// 不是第一步,直接进入下一步
currentStep.value += 1;
}
}
}
// 上一步
function prevStep() {
if (currentStep.value > 0) {
currentStep.value -= 1;
}
}
// 保存申请
function saveApplication() {
formRef.value?.validate((errors: any) => {
if (errors) {
message.error('请完善必填信息');
return;
}
message.success('申请提交成功');
console.log('Submit application:', {
form: formModel,
files: fileList.value,
tests: testItems.value.filter(item => item.selected)
});
});
}
</script>
<template>
<div class="apply-page-container">
<!-- 添加顶部导航栏 -->
<div class="header">
<NButton quaternary class="back-button" @click="handleClose">
<template #icon>
<NIcon>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path fill="currentColor" d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z" />
</svg>
</NIcon>
</template>
返回
</NButton>
</div>
<NCard style="margin-bottom: 10px">
<div class="page-header">
<NSteps :current="currentStep" class="mt-8">
<NStep title="申请信息填写" :status="currentStep === 0 ? 'process' : 'finish'"></NStep>
<NStep
title="审核文件提交"
:status="currentStep === 1 ? 'process' : currentStep > 1 ? 'finish' : 'wait'"
></NStep>
<NStep title="比对测试选择" :status="currentStep === 2 ? 'process' : 'wait'"></NStep>
</NSteps>
</div>
</NCard>
<NCard :bordered="false" class="form-card" size="huge">
<NForm
ref="formRef"
:model="formModel"
:rules="rules"
label-placement="left"
label-width="120px"
require-mark-placement="right-hanging"
size="medium"
>
<!-- 步骤1: 申请信息填写 -->
<ApplicationInformation
v-if="currentStep === 0"
:form-model="formModel"
:availability-data="availabilityData"
></ApplicationInformation>
<!-- 步骤2: 审核文件提交 -->
<FileReview
v-if="currentStep === 1"
:file-list="fileList"
:report="formModel"
@update:file-list="(newList: any) => (fileList = newList)"
></FileReview>
<!-- 步骤3: 比对测试选择 -->
<ComparisonTest
v-if="currentStep === 2"
:test-items="testItems"
:save-as-draft="saveAsDraft"
@update:test-items="(newItems: TestItem[]) => (testItems = newItems)"
@update:save-as-draft="(value: boolean) => (saveAsDraft = value)"
></ComparisonTest>
</NForm>
<NDivider></NDivider>
<div>
<NSpace justify="center">
<NButton v-if="currentStep > 0" round @click="prevStep">上一步</NButton>
<NButton v-if="currentStep < 2" type="primary" round @click="nextStep">下一步</NButton>
<NPopconfirm v-if="currentStep === 2" @positive-click="saveApplication">
<template #trigger>
<NButton type="primary" round>
<template #icon>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16">
<path fill="currentColor" d="M2,21L23,12L2,3V10L17,12L2,14V21Z"></path>
</svg>
</template>
保存申请
</NButton>
</template>
确认保存此申请吗?
</NPopconfirm>
</NSpace>
</div>
</NCard>
</div>
</template>
<style scoped>
.apply-page-container {
padding: 24px;
min-height: 100vh;
}
.page-header {
text-align: center;
justify-content: center;
align-items: center;
/* margin-bottom: 0px; */
}
.form-card {
border-radius: 16px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05);
overflow: hidden;
}
/* 强制设置当前步骤样式 */
:deep(.n-step--process .n-step-indicator) {
background-color: #2080f0 !important;
color: white !important;
}
:deep(.n-step--process .n-step-header__title) {
color: #2080f0 !important;
font-weight: bold !important;
}
:deep(.n-step--process .n-step-header__description) {
color: #2080f0 !important;
}
.section-title {
display: flex;
align-items: center;
font-size: 18px;
font-weight: bold;
margin-bottom: 16px;
color: #333;
}
.section-icon {
margin-right: 8px;
color: #333;
display: flex;
align-items: center;
justify-content: center;
}
.contact-tags {
min-height: 32px;
}
.empty-tip {
color: #999;
font-size: 13px;
font-style: italic;
}
.time-slot-card {
transition: all 0.3s;
border: 1px solid #eaeaea;
}
.time-slot-card:hover {
transform: translateY(-3px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.08);
}
.time-slot-full {
opacity: 0.6;
}
.time-slot-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.availability {
font-size: 24px;
font-weight: bold;
color: #0072ff;
}
.summary-card {
background-color: #f9fbff;
}
.summary-item {
margin-bottom: 16px;
}
.summary-label {
font-size: 14px;
color: #666;
margin-bottom: 4px;
}
.summary-value {
font-size: 15px;
color: #333;
}
.upload-hint {
color: #999;
font-size: 13px;
margin-top: 8px;
}
.mt-4 {
margin-top: 16px;
}
.mt-6 {
margin-top: 24px;
}
.mt-8 {
/* margin-top: 20px; */
}
.text-sm {
font-size: 14px;
}
.text-gray-400 {
color: #9ca3af;
}
/* 隐藏最后一个步骤的横线 */
:deep(.n-steps .n-step:last-child .n-step-splitor) {
display: none;
}
/* 调整步骤组件布局,去除右侧空白 */
:deep(.n-steps) {
width: fit-content;
margin: 0 auto;
}
/* 确保步骤之间的连接线正常显示 */
:deep(.n-step-splitor) {
display: block !important;
width: 560px !important;
}
/* 只隐藏最后一个步骤的连接线 */
:deep(.n-steps .n-step:last-child .n-step-splitor) {
display: none !important;
}
/* 添加顶部导航栏样式 */
.header {
display: flex;
align-items: center;
margin-bottom: 20px;
gap: 16px;
}
.back-button {
font-size: 14px;
}
</style>
<script setup lang="ts">
import { computed, defineEmits, defineProps, onMounted, ref, watch } from 'vue';
import { request } from '@/service/request';
// 主体框架内置的第三方方法通过window对象暴露、供外部组件使用
// 获取主题状态
const themeStore = (window as any).$themeStore;
// 获取消息提示
const message = (window as any).$message;
const dialog = (window as any).$dialog;
// 通过计算属性确定当前的主体色
const isDarkMode = computed(() => themeStore?.darkMode || false);
// 根据主题计算文字颜色
const textColor = computed(() => (isDarkMode.value ? 'rgba(255, 255, 255, 0.85)' : 'rgba(0, 0, 0, 0.85)'));
const legendTextColor = computed(() => (isDarkMode.value ? 'rgba(255, 255, 255, 0.65)' : 'rgba(0, 0, 0, 0.65)'));
const borderColor = computed(() => (isDarkMode.value ? '#303030' : '#ffffff'));
// CSS变量
const cssVars = computed(() => ({
'--text-color': textColor.value,
'--bg-color': isDarkMode.value ? '#1f1f1f' : '#f5f5f5',
'--panel-bg': isDarkMode.value ? '#262626' : '#ffffff',
'--card-bg': isDarkMode.value ? '#303030' : '#ffffff',
'--border-color': isDarkMode.value ? '#434343' : '#e8e8e8'
}));
interface PlanDetailItem {
id: number;
testItem: string;
standardValue: string;
allowableRange: string;
key?: string;
children?: PlanDetailItem[];
}
const props = defineProps({
standardKvid: {
type: String,
required: true
}
});
const emit = defineEmits(['cancel', 'confirm']);
// 当前可用的方案明细数据
const planDetails = ref<PlanDetailItem[]>([]);
// 已选中的行键数组
const checkedRowKeys = ref<(string | number)[]>([]);
// 加载状态
const loading = ref<boolean>(false);
// 获取测试详情数据
async function fetchTestDetails() {
if (!props.standardKvid) return;
try {
loading.value = true;
const { data, error } = await request({
url: '/Restful/Kivii.Standards.Entities.Detection/Query.json',
method: 'get',
params: { StandardKvid: props.standardKvid }
});
if (error) {
console.error('获取测试详情失败:', error);
message.error('获取测试详情失败');
return;
}
if (data && data.Results) {
// 处理树形结构数据
const treeData = processTreeData(data.Results);
planDetails.value = treeData;
message.success(`已加载${data.Results.length}个测试项目`);
} else {
message.error('获取测试数据失败');
}
} catch (error) {
message.error('获取测试详情失败');
console.error(error);
} finally {
loading.value = false;
}
}
// 处理树形结构数据
function processTreeData(data: any[]): PlanDetailItem[] {
// 创建ID到节点的映射
const idMap: Record<string, any> = {};
const result: any[] = [];
// 首先将所有节点添加到映射中
data.forEach((item, index) => {
const node = {
id: index + 1,
key: item.Kvid || `item-${index}`, // 使用Kvid作为唯一标识,如果没有则使用索引
testItem: item.Title || '未命名项目',
standardValue: item.StandardValue || item.QualifiedValue || '未设置',
allowableRange: item.AllowableRange || '±5%',
parentId: item.ParentKvid || null, // 假设API返回ParentKvid表示父节点ID
children: []
};
idMap[node.key] = node;
});
// 构建树形结构
Object.values(idMap).forEach(node => {
if (node.parentId && idMap[node.parentId]) {
// 如果有父节点,将当前节点添加到父节点的children中
idMap[node.parentId].children.push(node);
} else {
// 如果没有父节点,则是顶级节点
result.push(node);
}
});
// 处理空children数组
const cleanEmptyChildren = (nodes: any[]) => {
for (const node of nodes) {
if (node.children.length === 0) {
delete node.children;
} else {
cleanEmptyChildren(node.children);
}
}
};
cleanEmptyChildren(result);
return result;
}
// 监听standardKvid变化,重新获取数据
watch(
() => props.standardKvid,
newValue => {
if (newValue) {
fetchTestDetails();
// 重置选中状态
checkedRowKeys.value = [];
} else {
planDetails.value = [];
}
},
{ immediate: true }
);
// 处理行选择变化
function handleCheckedRowKeysChange(keys: (string | number)[]) {
checkedRowKeys.value = keys;
}
// 表格列定义
const columns = [
{
type: 'selection' as const
},
{ title: '测试项目', key: 'testItem', align: 'center' as const },
{ title: '标准值', key: 'standardValue', align: 'center' as const },
{ title: '允许偏差范围', key: 'allowableRange', align: 'center' as const }
];
// 取消选择
function cancel() {
emit('cancel');
}
// 确认选择
function confirm() {
if (checkedRowKeys.value.length === 0) {
message.warning('请至少选择一个测试项目');
return;
}
// 提取完整树形结构中被选中的节点和它们的子节点
const cloneTreeData = JSON.parse(JSON.stringify(planDetails.value));
// 过滤并标记选中的树节点
const filterSelectedNodes = (nodes: any[]): any[] => {
const result: any[] = [];
for (const node of nodes) {
const isSelected = checkedRowKeys.value.includes(node.key);
// 如果当前节点被选中或者有被选中的子节点,则保留
if (isSelected) {
// 创建一个节点副本,只保留需要的属性
const newNode: any = {
id: node.id,
key: node.key,
testItem: node.testItem,
standardValue: node.standardValue,
allowableRange: node.allowableRange
};
// 如果有子节点,递归处理
if (node.children && node.children.length > 0) {
const selectedChildren = filterSelectedNodes(node.children);
if (selectedChildren.length > 0) {
newNode.children = selectedChildren;
}
}
result.push(newNode);
} else if (node.children && node.children.length > 0) {
// 如果当前节点未被选中,但可能有被选中的子节点
const selectedChildren = filterSelectedNodes(node.children);
if (selectedChildren.length > 0) {
// 创建一个节点副本,并添加被选中的子节点
const newNode: any = {
id: node.id,
key: node.key,
testItem: node.testItem,
standardValue: node.standardValue,
allowableRange: node.allowableRange,
children: selectedChildren
};
result.push(newNode);
}
}
}
return result;
};
const selectedTreeData = filterSelectedNodes(cloneTreeData);
// 计算选中的项目总数
const countSelectedItems = (nodes: any[]): number => {
let count = 0;
for (const node of nodes) {
count += 1; // 计算当前节点
if (node.children && node.children.length > 0) {
count += countSelectedItems(node.children); // 递归计算子节点
}
}
return count;
};
const selectedCount = countSelectedItems(selectedTreeData);
// 将处理后的树形结构传递给父组件
emit('confirm', selectedTreeData);
message.success(`已选择${selectedCount}个测试项目`);
}
// 页面加载时获取数据
onMounted(() => {
// 删除此处的fetchTestDetails调用,避免重复调用API
// watch中的immediate:true已经确保了初始加载数据
});
</script>
<template>
<div class="plan-detail-selector">
<!--
<div class="header">
<div class="title">测试方案明细选择</div>
<n-space style="display: flex; align-items: center;">
<span class="plan-name">{{ planName }}</span>
<n-tag type="info">{{ checkedRowKeys.length }}项已选择</n-tag>
</n-space>
</div>
-->
<NAlert type="info" style="margin-bottom: 16px">
请选择需要进行比对测试的项目,选中的项目将被添加到测试数据表格中。
</NAlert>
<NDataTable
:columns="columns"
:data="planDetails"
:bordered="false"
:row-key="row => row.key"
:checked-row-keys="checkedRowKeys"
children-key="children"
:indent="20"
:max-height="300"
:min-height="300"
:pagination="false"
striped
:loading="loading"
@update:checked-row-keys="handleCheckedRowKeysChange"
></NDataTable>
<div class="footer">
<NSpace justify="end">
<NButton @click="cancel">取消</NButton>
<NButton type="primary" :disabled="checkedRowKeys.length === 0" @click="confirm">
确认选择({{ checkedRowKeys.length }})
</NButton>
</NSpace>
</div>
</div>
</template>
<style scoped>
.plan-detail-selector {
/* padding: 0 0 16px 0; */
height: 450px;
display: flex;
flex-direction: column;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
}
.title {
font-size: 18px;
font-weight: bold;
color: #333;
}
.plan-name {
font-weight: bold;
color: #2080f0;
}
.footer {
padding-top: 10px;
/* padding-bottom: 16px; */
border-top: 1px solid #f0f0f0;
margin-top: auto;
}
</style>
<script setup lang="ts">
import { computed, defineEmits, defineProps, onMounted, ref, watch } from 'vue';
import { request } from '@/service/request';
const props = defineProps({
formModel: {
type: Object,
required: true
},
availabilityData: {
type: Object,
required: true
}
});
const emit = defineEmits(['update:formModel']);
// 主体框架内置的第三方方法通过window对象暴露、供外部组件使用
// 获取主题状态
const themeStore = (window as any).$themeStore;
// 获取消息提示
const message = (window as any).$message;
const dialog = (window as any).$dialog;
// 通过计算属性确定当前的主体色
const isDarkMode = computed(() => themeStore?.darkMode || false);
// 根据主题计算文字颜色
const textColor = computed(() => (isDarkMode.value ? 'rgba(255, 255, 255, 0.85)' : 'rgba(0, 0, 0, 0.85)'));
const legendTextColor = computed(() => (isDarkMode.value ? 'rgba(255, 255, 255, 0.65)' : 'rgba(0, 0, 0, 0.65)'));
const borderColor = computed(() => (isDarkMode.value ? '#303030' : '#ffffff'));
// CSS变量
const cssVars = computed(() => ({
'--text-color': textColor.value,
'--bg-color': isDarkMode.value ? '#1f1f1f' : '#f5f5f5',
'--panel-bg': isDarkMode.value ? '#262626' : '#ffffff',
'--card-bg': isDarkMode.value ? '#303030' : '#ffffff',
'--border-color': isDarkMode.value ? '#434343' : '#e8e8e8'
}));
// 实验室名称相关状态
const labNameOptions = ref<Array<{ label: string; value: string }>>([]);
// 存储 API 获取的实验室完整数据(包含地址等信息)
const apiLabOptions = ref<
Array<{
label: string;
value: string;
address?: string;
kvid?: string;
}>
>([]);
const searchInputValue = ref('');
const loading = ref(false);
// 后缀列表,用于生成模糊匹配选项
const suffixList = ['实验室', '研究中心', '技术研究所', '检测中心', '认证中心'];
// 监听输入值变化,实时更新选项
watch(searchInputValue, newVal => {
if (newVal) {
updateOptions(newVal);
}
});
// 更新选项列表,包括API数据和模糊匹配
const updateOptions = (query: string) => {
console.log('更新选项:', query);
// 首先基于API获取的数据进行过滤
const filteredApiOptions = query
? apiLabOptions.value.filter(item => item.label.toLowerCase().includes(query.toLowerCase()))
: [...apiLabOptions.value];
// 添加带后缀的动态选项
const dynamicOptions = query
? suffixList.map(suffix => ({
label: `${query}${suffix}`,
value: `${query}${suffix}`
}))
: [];
// 合并去重,优先显示API数据
const combinedOptions = [...filteredApiOptions];
// 仅添加不在API过滤结果中的动态选项
dynamicOptions.forEach(option => {
if (!combinedOptions.some(item => item.value === option.value)) {
combinedOptions.push(option);
}
});
console.log('更新后的选项:', combinedOptions);
labNameOptions.value = combinedOptions;
};
// 获取实验室数据
const fetchLabData = async () => {
loading.value = true;
try {
console.log('开始获取检测机构数据');
// 第一步:获取检测机构数据
const { data: orgData, error: orgError } = await request({
url: '/Restful/Kivii.Organizations.Entities.Organization/Query.json',
method: 'get',
params: { InternalCode: '检测机构' }
});
console.log('检测机构数据响应:', orgData);
if (orgError) {
console.error('获取检测机构数据失败:', orgError);
message.error('获取检测机构数据失败');
return;
}
if (!orgData?.Results || orgData.Results.length === 0) {
message.error('未找到检测机构数据');
return;
}
const orgKvid = orgData.Results[0].Kvid;
console.log('获取到检测机构Kvid:', orgKvid);
// 第二步:根据ParentKvid获取实验室列表
const { data: labData, error: labError } = await request({
url: '/Restful/Kivii.Organizations.Entities.Organization/Query.json',
method: 'get',
params: { ParentKvid: orgKvid }
});
console.log('实验室数据响应:', labData);
if (labError) {
console.error('获取实验室列表失败:', labError);
message.error('获取实验室列表失败');
return;
}
if (!labData?.Results) {
message.error('获取实验室列表失败:响应格式错误');
return;
}
// 转换为下拉框数据格式,同时保存更多信息用于自动填充
const results = labData.Results || [];
apiLabOptions.value = results.map((item: any) => ({
label: item.Name,
value: item.Name,
address: item.Address || '',
kvid: item.Kvid
}));
// 初始化选项列表
labNameOptions.value = [...apiLabOptions.value];
console.log('转换后的实验室列表(包含地址):', apiLabOptions.value);
// 如果没有数据,显示提示
if (apiLabOptions.value.length === 0) {
message.info('暂无实验室数据');
} else {
message.success(`成功获取${apiLabOptions.value.length}个实验室数据`);
}
} catch (error) {
console.error('获取实验室数据失败', error);
message.error('获取实验室数据失败');
} finally {
loading.value = false;
}
};
// 实验室名称搜索处理
const handleLabSearch = (query: string) => {
console.log('搜索实验室:', query);
searchInputValue.value = query;
updateOptions(query);
};
// 实验室名称选择处理
const handleLabSelect = (value: string) => {
console.log('选择实验室:', value);
if (value) {
// 设置实验室名称
props.formModel.labName = value;
// 查找是否有匹配的实验室数据(来自API)
const selected = apiLabOptions.value.find(item => item.value === value);
// 如果找到匹配的实验室数据且有地址信息,则自动填充地址
if (selected && selected.address) {
console.log('自动填充地址:', selected.address);
props.formModel.labAddress = selected.address;
message.success('已自动填充实验室地址');
}
}
};
// 组件加载时获取实验室数据
onMounted(() => {
console.log('applicationInformation组件已挂载,开始获取实验室数据');
fetchLabData();
});
</script>
<template>
<div>
<!-- 实验室信息 -->
<div class="section-title">
<svg class="section-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<path
fill="currentColor"
d="M12,11.5A2.5,2.5 0 0,1 9.5,9A2.5,2.5 0 0,1 12,6.5A2.5,2.5 0 0,1 14.5,9A2.5,2.5 0 0,1 12,11.5M12,2A7,7 0 0,0 5,9C5,14.25 12,22 12,22C12,22 19,14.25 19,9A7,7 0 0,0 12,2Z"
></path>
</svg>
<span>实验室信息</span>
</div>
<div style="display: flex; width: 100%">
<NFormItem path="labName" label="实验室名称" style="width: 50%">
<NSelect
v-model:value="formModel.labName"
:options="labNameOptions"
:loading="loading"
placeholder="请输入或选择实验室名称"
clearable
filterable
remote
size="medium"
@update:value="handleLabSelect"
@input="handleLabSearch"
>
<template #empty>
<NEmpty description="暂无实验室数据" size="small" />
</template>
</NSelect>
</NFormItem>
<NFormItem path="labAddress" label="实验室地址" style="width: 50%">
<NInput v-model:value="formModel.labAddress" placeholder="请输入实验室详细地址"></NInput>
</NFormItem>
</div>
<NDivider></NDivider>
<!-- 联系人信息 -->
<div class="section-title">
<svg class="section-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<path
fill="currentColor"
d="M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,14C16.42,14 20,15.79 20,18V20H4V18C4,15.79 7.58,14 12,14Z"
></path>
</svg>
<span>联系人信息</span>
</div>
<div style="display: flex; width: 100%">
<NFormItem path="contactName" label="联系人姓名" style="width: 33%">
<NInput v-model:value="formModel.contactName" placeholder="请输入联系人姓名"></NInput>
</NFormItem>
<NFormItem path="contactPhone" label="联系电话" style="width: 33%">
<NInput v-model:value="formModel.contactPhone" placeholder="请输入联系电话"></NInput>
</NFormItem>
</div>
<NDivider></NDivider>
<!-- 申请时间 -->
<div class="section-title">
<svg class="section-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<path
fill="currentColor"
d="M19,19H5V8H19M16,1V3H8V1H6V3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5C21,3.89 20.1,3 19,3H18V1M17,12H12V17H17V12Z"
></path>
</svg>
<span>申请时间设置</span>
</div>
<NFormItem path="applyDate" label="申请时间">
<NDatePicker
v-model:value="formModel.applyDate"
type="datetime"
clearable
style="width: 100%"
format="yyyy-MM-dd HH:mm"
:disabled-date="(ts: number) => ts < Date.now() - 86400000"
></NDatePicker>
</NFormItem>
<NFormItem path="remarks" label="备注说明" class="last-form-item">
<NInput
v-model:value="formModel.remarks"
type="textarea"
placeholder="如有特殊需求,请在此说明"
:autosize="{ minRows: 3, maxRows: 5 }"
></NInput>
</NFormItem>
</div>
</template>
<style scoped>
.section-title {
display: flex;
align-items: center;
font-size: 18px;
font-weight: bold;
margin-bottom: 16px;
color: #333;
}
.section-icon {
margin-right: 8px;
color: #333;
display: flex;
align-items: center;
justify-content: center;
}
.time-slot-card {
transition: all 0.3s;
border: 1px solid #eaeaea;
}
.time-slot-card:hover {
transform: translateY(-3px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.08);
}
.time-slot-full {
opacity: 0.6;
}
.time-slot-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.availability {
font-size: 24px;
font-weight: bold;
color: #0072ff;
}
.mt-4 {
margin-top: 16px;
}
:deep(.last-form-item .n-form-item-feedback-wrapper) {
display: none;
}
</style>
<script setup lang="ts">
import { computed, defineEmits, defineProps, h, onMounted, ref, resolveComponent } from 'vue';
import { request } from '@/service/request';
import TestPlanDetailSelector from './TestPlanDetailSelector.vue';
interface TestItem {
id: number;
name: string;
selected: boolean;
}
interface TestDataItem {
key: string;
testItem: string;
standardValue: string;
allowableRange: string;
measuredValue: string | null;
verifier: string | null;
result: string;
children?: TestDataItem[];
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const props = defineProps({
testItems: {
type: Array as () => TestItem[],
required: true
},
saveAsDraft: {
type: Boolean,
required: true
}
});
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const emit = defineEmits(['update:testItems', 'update:saveAsDraft']);
// 主体框架内置的第三方方法通过window对象暴露、供外部组件使用
// 获取主题状态
const themeStore = (window as any).$themeStore;
// 获取消息提示
const message = (window as any).$message;
const dialog = (window as any).$dialog;
// 通过计算属性确定当前的主体色
const isDarkMode = computed(() => themeStore?.darkMode || false);
// 根据主题计算文字颜色
const textColor = computed(() => (isDarkMode.value ? 'rgba(255, 255, 255, 0.85)' : 'rgba(0, 0, 0, 0.85)'));
const legendTextColor = computed(() => (isDarkMode.value ? 'rgba(255, 255, 255, 0.65)' : 'rgba(0, 0, 0, 0.65)'));
const borderColor = computed(() => (isDarkMode.value ? '#303030' : '#ffffff'));
// CSS变量
const cssVars = computed(() => ({
'--text-color': textColor.value,
'--bg-color': isDarkMode.value ? '#1f1f1f' : '#f5f5f5',
'--panel-bg': isDarkMode.value ? '#262626' : '#ffffff',
'--card-bg': isDarkMode.value ? '#303030' : '#ffffff',
'--border-color': isDarkMode.value ? '#434343' : '#e8e8e8'
}));
// 表单数据
const testPlan = ref('');
const testDate = ref(null);
const sampleCode = ref('');
const testResult = ref('');
const testComment = ref('');
const showDetailSelector = ref(false);
// 状态变量
const loading = ref<boolean>(false);
const titleOptions = ref<Array<{ label: string; value: string }>>([]);
// 获取测试方案选项
async function fetchTestPlanOptions() {
try {
loading.value = true;
// 调用标准实体查询接口
const { data, error } = await request({
url: '/Restful/Kivii.Standards.Entities.Standard/Query.json',
method: 'get',
params: { Type: '比对测试' }
});
console.log('测试方案响应数据:', data);
if (error) {
console.error('获取测试方案失败:', error);
message.error('获取测试方案失败');
return;
}
// 添加数据存在性检查
if (data && data.Results) {
// 直接使用Results数组
titleOptions.value = data.Results.map((item: any) => ({
label: item.Title,
value: item.Kvid
}));
} else {
console.error('无效的数据格式:', data);
message.error('数据格式不正确');
}
} catch (error) {
message.error('获取测试方案失败');
console.error('获取测试方案失败:', error);
} finally {
loading.value = false;
}
}
// 表格列定义
const columns = [
{ title: '测试项目', key: 'testItem' },
{ title: '标准值', key: 'standardValue', align: 'center' as const },
{ title: '允许偏差范围', key: 'allowableRange', align: 'center' as const },
{
title: '实测值',
key: 'measuredValue',
align: 'center' as const,
render: (row: TestDataItem) => {
return h(resolveComponent('n-input'), {
value: row.measuredValue,
placeholder: '请输入测试值',
onUpdateValue: (value: string) => {
row.measuredValue = value;
checkTestResult(row);
}
});
}
},
{
title: '授信人',
key: 'verifier',
align: 'center' as const,
render: (row: TestDataItem) => {
return h(resolveComponent('n-input'), {
value: row.verifier,
placeholder: '请输入授信人',
onUpdateValue: (value: string) => {
row.verifier = value;
}
});
}
},
{
title: '结果',
key: 'result',
align: 'center' as const,
render: (_row: TestDataItem) => {
return h(resolveComponent('n-tag'), { type: 'default' }, { default: () => '待测试' });
}
},
{
title: '操作',
key: 'actions',
width: 200,
align: 'center',
render(row: any) {
return h('div', { style: 'display: flex; gap: 8px; justify-content: center;' }, [
h(
resolveComponent('n-button'),
{
size: 'small',
type: 'error',
onClick: () => deleteTestPlanDetail(row)
},
{ default: () => '删除' }
)
]);
}
}
];
function deleteTestPlanDetail(row: TestDataItem) {
dialog.warning({
title: '确认删除',
content: `是否确认删除测试项"${row.testItem}"?`,
positiveText: '确认',
negativeText: '取消',
onPositiveClick: () => {
// 递归查找并删除行项目
const removeItem = (items: TestDataItem[], key: string): boolean => {
for (let i = 0; i < items.length; i++) {
if (items[i].key === key) {
items.splice(i, 1);
return true;
}
if (items[i].children && items[i].children.length > 0) {
if (removeItem(items[i].children, key)) {
return true;
}
}
}
return false;
};
if (removeItem(testData.value, row.key)) {
message.success('删除成功');
}
}
});
}
// 测试数据
const testData = ref<TestDataItem[]>([]);
// 处理测试方案变化
function handlePlanChange(value: string) {
if (value) {
// 打开明细选择弹窗
showDetailSelector.value = true;
}
}
// 处理方案明细确认事件
function handlePlanDetailsConfirm(selectedItems: any[]) {
// 获取当前最大测试项索引,用于生成唯一key
let maxIndex = 0;
if (testData.value.length > 0) {
// 从现有key中提取数字部分
const keyNumbers = testData.value.map(item => {
const matches = item.key.match(/test-(\d+)/);
return matches ? Number.parseInt(matches[1], 10) : 0;
});
maxIndex = Math.max(...keyNumbers);
}
// 处理新选择的测试项,添加正确的key
const processedItems = processTestItems(selectedItems, maxIndex);
// 将处理后的数据添加到现有数据中
testData.value = [...testData.value, ...processedItems];
showDetailSelector.value = false;
message.success(`已添加${selectedItems.length}个测试项目`);
}
// 处理测试项目数据,添加必要的字段并构建树形结构
function processTestItems(items: any[], startId = 0) {
// 创建一个新的数组来存储处理后的数据
return items.map((item, index) => {
const newId = startId + index + 1;
return {
key: `test-${newId}`,
testItem: item.testItem,
standardValue: item.standardValue,
allowableRange: item.allowableRange,
measuredValue: null,
verifier: null,
result: '待测试',
// 如果原数据中有children,也需要递归处理
...(item.children
? {
children: processTestItems(item.children, startId + items.length)
}
: {})
};
});
}
// 页面加载时获取测试方案选项
onMounted(() => {
fetchTestPlanOptions();
});
// 检查测试结果
function checkTestResult(row: TestDataItem) {
// 这里可以添加逻辑来判断测试结果是否在允许范围内
// 现在只是占位,实际应用中应根据实测值和标准值+允许偏差范围判断
if (row.measuredValue) {
row.result = '待评估';
} else {
row.result = '待测试';
}
}
</script>
<template>
<div>
<div class="section-title">
<svg class="section-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<path
fill="currentColor"
d="M6,22A3,3 0 0,1 3,19C3,18.4 3.18,17.84 3.5,17.37L9,7.81V6A1,1 0 0,1 8,5V4A2,2 0 0,1 10,2H14A2,2 0 0,1 16,4V5A1,1 0 0,1 15,6V7.81L20.5,17.37C20.82,17.84 21,18.4 21,19A3,3 0 0,1 18,22H6M5,19A1,1 0 0,0 6,20H18A1,1 0 0,0 19,19C19,18.79 18.93,18.59 18.82,18.43L16.53,14.47L14,17L8.93,11.93L5.18,18.43C5.07,18.59 5,18.79 5,19M13,10A1,1 0 0,0 12,11A1,1 0 0,0 13,12A1,1 0 0,0 14,11A1,1 0 0,0 13,10Z"
></path>
</svg>
<span>测试方案选择</span>
</div>
<NFormItem label="测试方案" required>
<NSelect
v-model:value="testPlan"
:options="titleOptions"
placeholder="请选择测试方案"
style="width: 100%"
@update:value="handlePlanChange"
></NSelect>
</NFormItem>
<NFormItem label="测试日期" required>
<NDatePicker v-model:value="testDate" type="date" clearable style="width: 100%" format="yyyy/MM/dd"></NDatePicker>
</NFormItem>
<NFormItem label="测试样品编号" class="last-form-item" required>
<NInput v-model:value="sampleCode" placeholder="请输入测试样品编号"></NInput>
</NFormItem>
<NDivider></NDivider>
<div class="section-title">
<svg class="section-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<path
fill="currentColor"
d="M10,17L6,13L7.41,11.59L10,14.17L16.59,7.58L18,9M12,3A1,1 0 0,1 13,4A1,1 0 0,1 12,5A1,1 0 0,1 11,4A1,1 0 0,1 12,3M19,3H14.82C14.4,1.84 13.3,1 12,1C10.7,1 9.6,1.84 9.18,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3Z"
></path>
</svg>
<span>比对测试数据</span>
</div>
<NDataTable
:columns="columns"
:data="testData"
:bordered="false"
:max-height="300"
:loading="loading"
:row-key="row => row.key"
children-key="children"
:indent="20"
:default-expand-all="true"
striped
></NDataTable>
<div class="result-summary">
<span class="result-label">总体结果:</span>
<NTag type="warning">未完成</NTag>
</div>
<NDivider></NDivider>
<div class="section-title">
<svg class="section-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<path
fill="currentColor"
d="M6,2C4.89,2 4,2.89 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2H6M6,4H13V9H18V20H6V4M8,12V14H16V12H8M8,16V18H13V16H8Z"
></path>
</svg>
<span>测试结论</span>
</div>
<NFormItem label="测试结论">
<NRadioGroup v-model:value="testResult">
<NSpace>
<NRadio value="pass">通过</NRadio>
<NRadio value="fail">不通过</NRadio>
<NRadio value="partial">部分通过</NRadio>
</NSpace>
</NRadioGroup>
</NFormItem>
<NFormItem label="测试意见" class="last-form-item">
<NInput v-model:value="testComment" type="textarea" placeholder="请输入测试意见" :rows="4"></NInput>
</NFormItem>
<!-- 测试方案明细选择弹窗 -->
<NModal
v-model:show="showDetailSelector"
style="width: 800px"
preset="card"
:bordered="false"
:show-icon="false"
title="测试方案明细选择"
>
<TestPlanDetailSelector
:standard-kvid="testPlan"
@cancel="showDetailSelector = false"
@confirm="handlePlanDetailsConfirm"
></TestPlanDetailSelector>
</NModal>
</div>
</template>
<style scoped>
.section-title {
display: flex;
align-items: center;
font-size: 18px;
font-weight: bold;
margin-bottom: 16px;
color: #333;
}
.section-icon {
margin-right: 8px;
color: #333;
display: flex;
align-items: center;
justify-content: center;
}
.test-item {
padding: 8px 0;
}
.text-sm {
font-size: 14px;
}
.text-gray-400 {
color: #9ca3af;
}
.result-summary {
display: flex;
justify-content: flex-end;
align-items: center;
padding-top: 24px;
/* padding-bottom: 16px; */
}
.result-label {
font-size: 16px;
font-weight: bold;
margin-right: 8px;
}
:deep(.n-form-item-label) {
text-align: justify;
text-align-last: justify;
}
:deep(.last-form-item .n-form-item-feedback-wrapper) {
display: none;
}
</style>
<script setup lang="ts">
import { computed, h, ref, resolveComponent } from 'vue';
import type { UploadCustomRequestOptions } from 'naive-ui';
interface FileItem {
id: number;
name: string;
remark: string;
uploadTime?: string;
size?: string;
}
// 主体框架内置的第三方方法通过window对象暴露、供外部组件使用
// 获取消息提示
const message = (window as any).$message;
// 状态变量
const fileData = ref<FileItem[]>([
{
id: 1,
name: '2023年度财务报表.xlsx',
remark: '年度财务总结文件',
uploadTime: '2023-12-15',
size: '2.3MB'
},
{
id: 2,
name: '产品设计方案.docx',
remark: '产品设计初稿,待审核',
uploadTime: '2023-12-20',
size: '1.5MB'
},
{
id: 3,
name: '市场调研报告.pdf',
remark: '2023第四季度市场调研',
uploadTime: '2023-12-18',
size: '3.7MB'
}
]);
const loading = ref<boolean>(false);
const showPreviewModal = ref<boolean>(false);
const currentFile = ref<FileItem | null>(null);
// 表格列定义
const columns = ref([
{
title: '文件名称',
key: 'name',
width: '50%'
},
{
title: '上传日期',
key: 'uploadTime',
align: 'center' as const,
width: '30%'
},
{
title: '操作',
key: 'actions',
align: 'center' as const,
width: '20%',
render(row: FileItem) {
return h('div', { style: 'display: flex; gap: 8px; justify-content: center;' }, [
h(
resolveComponent('n-button'),
{
size: 'small',
type: 'info',
onClick: () => viewFile(row)
},
{ default: () => '预览' }
),
h(
resolveComponent('n-button'),
{
size: 'small',
type: 'error',
onClick: () => deleteFile(row)
},
{ default: () => '删除' }
)
]);
}
}
]);
// 查看文件
function viewFile(file: FileItem) {
currentFile.value = file;
showPreviewModal.value = true;
}
// 删除文件
function deleteFile(file: FileItem) {
message.success(`文件"${file.name}"已删除`);
fileData.value = fileData.value.filter(item => item.id !== file.id);
}
// 下载文件
function downloadFile(file: FileItem) {
message.success(`开始下载文件: ${file.name}`);
// 实际项目中这里应该调用后端API进行文件下载
}
// 处理文件上传
function handleUpload(options: UploadCustomRequestOptions) {
const { file } = options;
if (file) {
loading.value = true;
// 模拟上传延迟
setTimeout(() => {
const newFile: FileItem = {
id: Date.now(),
name: file.name,
remark: '新上传文件',
uploadTime: new Date().toISOString().split('T')[0],
size: formatFileSize(file.file?.size || 0)
};
fileData.value.unshift(newFile);
message.success(`文件"${file.name}"上传成功!`);
loading.value = false;
}, 1500);
}
}
// 格式化文件大小
function formatFileSize(size: number): string {
if (size < 1024) {
return `${size}B`;
} else if (size < 1024 * 1024) {
return `${(size / 1024).toFixed(1)}KB`;
}
return `${(size / (1024 * 1024)).toFixed(1)}MB`;
}
</script>
<template>
<div>
<NSpace vertical>
<NSpace justify="end" style="margin-bottom: 10px">
<NUpload action="#" :custom-request="handleUpload" :show-file-list="false">
<NButton type="primary">
<template #icon>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="18" height="18">
<path
fill="currentColor"
d="M19.35,10.04C18.67,6.59 15.64,4 12,4C9.11,4 6.6,5.64 5.35,8.04C2.34,8.36 0,10.91 0,14A6,6 0 0,0 6,20H19A5,5 0 0,0 24,15C24,12.36 21.95,10.22 19.35,10.04M19,18H6A4,4 0 0,1 2,14C2,11.95 3.53,10.24 5.56,10.03L6.63,9.92L7.13,8.97C8.08,7.14 9.94,6 12,6C14.62,6 16.88,7.86 17.39,10.43L17.69,11.93L19.22,12.04C20.78,12.14 22,13.45 22,15A3,3 0 0,1 19,18M8,13H10.55V16H13.45V13H16L12,9L8,13Z"
></path>
</svg>
</template>
上传文件
</NButton>
</NUpload>
</NSpace>
<NDataTable
:columns="columns"
:data="fileData"
:bordered="false"
:loading="loading"
max-height="500px"
></NDataTable>
</NSpace>
<!-- 文件预览对话框 -->
<NModal v-model:show="showPreviewModal" preset="card" title="文件预览" style="width: 50%" :mask-closable="true">
<div v-if="currentFile">
<NDescriptions label-placement="left" bordered>
<NDescriptionsItem label="文件名称">
{{ currentFile.name }}
</NDescriptionsItem>
<NDescriptionsItem label="上传日期">
{{ currentFile.uploadTime || '未记录' }}
</NDescriptionsItem>
</NDescriptions>
<div class="modal-footer">
<NButton type="primary" @click="downloadFile(currentFile)">下载文件</NButton>
<NButton @click="showPreviewModal = false">关闭</NButton>
</div>
</div>
</NModal>
</div>
</template>
<style scoped>
.section-title {
display: flex;
align-items: center;
font-size: 18px;
font-weight: bold;
margin-bottom: 16px;
color: #333;
}
.section-icon {
margin-right: 8px;
color: #333;
display: flex;
align-items: center;
justify-content: center;
}
.file-preview-content {
margin: 20px 0;
height: 300px;
display: flex;
align-items: center;
justify-content: center;
border: 1px dashed #ccc;
border-radius: 4px;
}
.no-preview-message {
text-align: center;
color: #999;
}
.file-icon {
font-size: 48px;
opacity: 0.6;
}
.modal-footer {
margin-top: 20px;
display: flex;
justify-content: flex-end;
gap: 12px;
}
</style>
<script setup lang="ts">
import { computed, defineEmits, defineProps, h, onMounted, ref, resolveComponent } from 'vue';
import { request } from '@/service/request';
import FileListView from './fileListView.vue';
// 主体框架内置的第三方方法通过window对象暴露、供外部组件使用
// 获取主题状态
const themeStore = (window as any).$themeStore;
// 获取消息提示
const message = (window as any).$message;
const dialog = (window as any).$dialog;
// 通过计算属性确定当前的主体色
const isDarkMode = computed(() => themeStore?.darkMode || false);
// 根据主题计算文字颜色
const textColor = computed(() => (isDarkMode.value ? 'rgba(255, 255, 255, 0.85)' : 'rgba(0, 0, 0, 0.85)'));
const legendTextColor = computed(() => (isDarkMode.value ? 'rgba(255, 255, 255, 0.65)' : 'rgba(0, 0, 0, 0.65)'));
const borderColor = computed(() => (isDarkMode.value ? '#303030' : '#ffffff'));
// CSS变量
const cssVars = computed(() => ({
'--text-color': textColor.value,
'--bg-color': isDarkMode.value ? '#1f1f1f' : '#f5f5f5',
'--panel-bg': isDarkMode.value ? '#262626' : '#ffffff',
'--card-bg': isDarkMode.value ? '#303030' : '#ffffff',
'--border-color': isDarkMode.value ? '#434343' : '#e8e8e8'
}));
// 修改FileItem接口,但保留原有字段
interface FileItem {
id?: number;
name?: string; // 对应Title
type?: string;
status?: string; // 对应StatusType
remark?: string; // 对应Description
StandardKvid: string;
Kvid: string;
Title?: string;
Code?: string;
Unit?: string;
QualifiedValue?: string;
Description?: string;
StatusType?: string;
}
// API返回的数据项接口
interface ResultItem {
id?: number;
StandardKvid: string;
Kvid: string;
Title?: string;
Code?: string;
Unit?: string;
QualifiedValue?: string;
Description?: string;
StatusType?: string;
[key: string]: any;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const props = defineProps({
fileList: {
type: Array as () => FileItem[],
required: true
},
report: {
type: Object as () => any,
required: true
}
});
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const emit = defineEmits(['update:file-list', 'update:reviewResult', 'update:reviewComment']);
// 状态变量
const titleOptions = ref<SelectOption[]>([]);
const selectedStandardKvid = ref<string>('');
const fileData = ref<FileItem[]>([]);
const loading = ref<boolean>(false);
// 控制fileListView模态框显示
const showFileListView = ref<boolean>(false);
// 根据选择的标题筛选数据
const filteredData = computed(() => {
if (!selectedStandardKvid.value) {
return [];
}
return fileData.value.filter(file => file.StandardKvid === selectedStandardKvid.value);
});
// 获取文件类型数据
async function fetchTitleOptions() {
try {
loading.value = true;
// 调用标准实体查询接口
const { data, error } = await request({
url: '/Restful/Kivii.Standards.Entities.Standard/Query.json',
method: 'get',
params: { Type: '文件审核' }
});
console.log('完整响应数据:', data);
if (error) {
console.error('获取文件类型失败:', error);
message.error('获取文件类型失败');
return;
}
// 添加数据存在性检查
if (data && data.Results) {
// 直接使用Results数组
titleOptions.value = data.Results.map((item: any) => ({
label: item.Title,
value: item.Kvid
}));
} else {
console.error('无效的数据格式:', data);
message.error('数据格式不正确');
}
} catch (error) {
message.error('获取文件类型失败');
console.error('获取文件类型失败:', error);
} finally {
loading.value = false;
}
}
// 获取文件详情数据
async function fetchFileDetails() {
if (!selectedStandardKvid.value) return;
try {
loading.value = true;
const { data, error } = await request({
url: '/Restful/Kivii.Standards.Entities.Detection/Query.json',
method: 'get',
params: { StandardKvid: selectedStandardKvid.value }
});
if (error) {
console.error('获取文件详情失败:', error);
message.error('获取文件详情失败');
return;
}
if (data && data.Results) {
createReportItems(data.Results);
} else {
message.error('获取文件数据失败');
}
} catch (error) {
message.error('获取文件详情失败');
console.error(error);
} finally {
loading.value = false;
}
}
async function createReportItems(reuslts: any) {
const { data, error } = await request({
url: '/Restful/Kivii.Lims.Entities.ReportItem/Detection.json',
method: 'post',
data: { Detections: reuslts, ReportKvid: props.report.Kvid }
});
if (error) {
console.error('项目创建失败:', error);
message.error(error.message);
return;
}
if (data && data.Results) {
// 保留完整的Results数组,同时添加必要的展示字段
fileData.value = data.Results.map((item: any) => ({
...item, // 保留所有原始字段
name: item.Title || '', // 添加展示用的name字段
remark: item.Description || item.QualifiedValue || '', // 添加展示用的remark字段
status: item.StatusType || 'pending' // 添加展示用的status字段
}));
// 向父组件更新文件列表
emit('update:file-list', fileData.value);
}
}
// 处理标题选择变化
function handleTitleChange(value: string) {
selectedStandardKvid.value = value;
fetchFileDetails();
}
// 页面加载时获取文件类型
onMounted(() => {
fetchTitleOptions();
});
// 表格列定义
const columns = ref<DataTableColumn<FileItem>[]>([
{
title: '文件名称',
key: 'name',
width: '35%'
},
{
title: '备注',
key: 'remark',
width: '20%'
},
{
title: '文件状态',
key: 'status',
width: '15%',
align: 'center',
render(row: FileItem) {
const statusMap: Record<string, { type: string; label: string }> = {
BeforeTest: { type: 'warning', label: '待上传' },
Asigning: { type: 'warning', label: '已上传' },
Testing: { type: 'success', label: '审核中' },
TestFinished: { type: 'warning', label: '不通过' },
TestCollected: { type: 'success', label: '已通过' }
};
const status = row.status || '';
const { type, label } = statusMap[status] || { type: 'default', label: status || '未知' };
return h(
resolveComponent('n-tag'),
{
type,
size: 'small'
},
{ default: () => label }
);
}
},
{
title: '操作',
key: 'actions',
width: 200,
align: 'center',
render(row: any) {
return h('div', { style: 'display: flex; gap: 8px; justify-content: center;' }, [
h(
resolveComponent('n-button'),
{
size: 'small',
onClick: () => viewFile(row)
},
{ default: () => '查看' }
),
h(
resolveComponent('n-button'),
{
size: 'small',
type: 'error',
onClick: () => deleteFileRecord(row)
},
{ default: () => '删除' }
)
]);
}
}
]);
// 查看文件
function viewFile(row: FileItem) {
// 可以根据需要将文件信息传递给fileListView组件
console.log(`查看文件: ${row.name}`);
showFileListView.value = true;
}
// 删除文件记录
function deleteFileRecord(row: FileItem) {
// 实际应该调用删除API
message.success(`文件"${row.name}"删除成功`);
// 从列表中移除
fileData.value = fileData.value.filter(item => item.Kvid !== row.Kvid);
// 向父组件更新文件列表
emit('update:file-list', fileData.value);
}
</script>
<template>
<div>
<div class="section-title">
<svg class="section-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<path
fill="currentColor"
d="M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M14,20V19C14,17.67 11.33,17 10,17C8.67,17 6,17.67 6,19V20H14M10,12A2,2 0 0,0 8,14A2,2 0 0,0 10,16A2,2 0 0,0 12,14A2,2 0 0,0 10,12Z"
></path>
</svg>
<span>审核文件提交</span>
</div>
<NFormItem label="选择文件类型">
<NSelect
v-model:value="selectedStandardKvid"
:options="titleOptions"
placeholder="选择文件类型"
@update:value="handleTitleChange"
></NSelect>
</NFormItem>
<NDataTable
:columns="columns"
:data="filteredData"
:bordered="false"
max-height="300px"
:loading="loading"
></NDataTable>
<!-- fileListView模态框 -->
<NModal v-model:show="showFileListView" preset="card" title="文件列表" style="width: 50%" :mask-closable="true">
<FileListView></FileListView>
</NModal>
</div>
</template>
<style scoped>
.section-title {
display: flex;
align-items: center;
font-size: 18px;
font-weight: bold;
margin-bottom: 16px;
color: #333;
}
.section-icon {
margin-right: 8px;
color: #333;
display: flex;
align-items: center;
justify-content: center;
}
.upload-hint {
color: #999;
font-size: 13px;
margin-top: 8px;
}
.mt-4 {
margin-top: 16px;
}
</style>
<script setup lang="ts">
import { onMounted, ref, watch } from 'vue';
// import mainSupplier from '/codes/mainSupplier.vue';
import { request } from '@/service/request';
import MainSupplier from './SampleForm.vue';
// 主体框架内置的第三方方法通过window对象暴露、供外部组件使用
// 引入消息提示
const message = (window as any).$message;
// 获取主题状态(已删除,因为使用naive ui组件不需要自定义主题计算)
const loading = ref<boolean>(false);
const searchText = ref<string>('');
const statusFilter = ref<string>('');
const timeFilter = ref<string>('');
// 控制弹窗显示状态 - 移动到使用前定义
const showMainSupplier = ref(false);
// 搜索防抖定时器
const searchTimer = ref<NodeJS.Timeout | null>(null);
// 统计数据
const statistics = ref({
totalApplications: { value: 0, change: 12.5, label: '总申请数' },
monthlyApplications: { value: 0, change: 8.3, label: '本月申请' },
passRate: { value: 0, change: 5.2, label: '认证通过率' },
pendingReview: { value: 0, label: '待审查数量', note: '30天内' }
});
// 申请列表数据类型定义
interface ApplicationItem {
Kvid: string;
ReportId: string;
SampleName: string;
StatusType: string;
SampleManufacturerName: string;
SampleManufacturerContactName: string;
SampleManufacturerContactNumber: string;
SampleManufacturerAddress: string;
DealDate: string;
CreateTime: string;
status: string;
statusColor: string;
applyDate: string;
progress: number;
actions: string[];
}
// 申请列表数据
const applicationList = ref<ApplicationItem[]>([]);
// 筛选选项
const statusOptions = [
{ label: '全部状态', value: '' },
{ label: '草稿', value: 0 },
{ label: '待审核', value: 100 },
{ label: '文件审核', value: 300 },
{ label: '现场评审', value: 400 },
{ label: '比对测试', value: 500 },
{ label: '认证签发', value: 700 },
{ label: '认证完成', value: 2147483647 }
];
const timeOptions = [
{ label: '全部时间', value: '' },
{ label: '近7天', value: '7d' },
{ label: '近30天', value: '30d' },
{ label: '近3个月', value: '3m' }
];
// 辅助函数:根据状态获取颜色
const getStatusColor = (status: string) => {
const statusColorMap: { [key: string]: string } = {
待审核: '#f56a00', // 橙色 - 待处理
文件审核: '#1890ff', // 蓝色 - 进行中
现场评审: '#722ed1', // 紫色 - 重要评审
比对测试: '#eb2f96', // 粉红色 - 测试阶段
认证签发: '#13c2c2', // 青色 - 即将完成
认证完成: '#52c41a' // 绿色 - 已完成
};
return statusColorMap[status] || '#666';
};
// 辅助函数:根据状态获取进度
const getProgressByStatus = (status: string) => {
const progressMap: { [key: string]: number } = {
草稿: 0,
待审核: 20,
文件审核: 40,
现场评审: 60,
比对测试: 80,
认证签发: 90,
认证完成: 100
};
return progressMap[status] || 0;
};
// 辅助函数:根据状态获取操作按钮
const getActionsByStatus = (status: string) => {
const actionsMap: { [key: string]: string[] } = {
草稿: ['查看详情', '提交申请', '删除'],
待审核: ['查看详情', '开始审核'],
文件审核: ['查看详情', '文件评审'],
现场评审: ['查看详情', '现场评审'],
比对测试: ['查看详情', '测试管理'],
认证签发: ['查看详情', '签发证书'],
认证完成: ['查看详情', '下载证书']
};
return actionsMap[status] || ['查看详情'];
};
// 辅助函数:将API的StatusType映射为显示状态
const mapStatusType = (statusType: string) => {
const statusMap: { [key: string]: string } = {
Unsupported: '草稿',
CommissionAccept: '待审核',
TaskAssign: '文件审核',
DataEntry: '现场评审',
ReportPreparation: '比对测试',
ReportIssue: '认证签发',
ReportCollected: '认证完成'
};
return statusMap[statusType] || '待审核';
};
// 辅助函数:格式化日期
const formatDate = (dateString: string) => {
if (!dateString) return '';
try {
const date = new Date(dateString);
return date
.toLocaleDateString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
})
.replace(/\//g, '-');
} catch (error) {
return dateString;
}
};
// 获取数据
const fetchData = async () => {
loading.value = true;
try {
const { data, error } = await request({
url: '/Restful/Kivii.Lims.Entities.Report/Query.json',
method: 'get',
params: {
// 根据实际API参数调整
...(searchText.value && {
QueryKeys:
'ReportId,SampleManufacturerName,SampleName,SampleManufacturerContactName,SampleManufacturerAddress',
QueryValues: searchText.value
}),
...(statusFilter.value && { Status: statusFilter.value }),
...(timeFilter.value && { timeRange: timeFilter.value }),
...{ OrderBy: 'Status' }
}
});
if (error) {
console.error('获取数据失败:', error);
message.error('获取数据失败,请稍后重试');
return;
}
if (data && data.Results) {
// 统计数据继续使用模拟数据,但更新总数
const totalCount = data.Total || data.Results.length;
statistics.value.totalApplications.value = totalCount;
// 更新申请列表数据
if (Array.isArray(data.Results)) {
applicationList.value = data.Results.map((item: any) => ({
// 直接使用API字段名
...item,
// 保留转换后的自定义字段
status: mapStatusType(item.StatusType),
statusColor: getStatusColor(mapStatusType(item.StatusType)),
applyDate: formatDate(item.DealDate),
progress: getProgressByStatus(mapStatusType(item.StatusType)),
actions: getActionsByStatus(mapStatusType(item.StatusType))
}));
message.success('数据获取成功');
} else {
message.error('获取数据失败:响应格式错误');
}
} else {
message.error('获取数据失败:响应格式错误');
}
} catch (error: any) {
console.error('获取数据失败:', error);
message.error('获取数据失败,请稍后重试');
} finally {
loading.value = false;
}
};
// 新增认证
const handleAddApplication = () => {
showMainSupplier.value = true;
};
// 查看详情
const handleViewDetail = (item: any) => {
message.info(`查看 ${item.SampleName} 详情`);
};
// 提交申请
const handleSubmitApplication = (item: any) => {
message.info(`提交申请 ${item.SampleName}`);
};
// 删除
const handleDelete = (item: any) => {
message.info(`删除 ${item.SampleName}`);
};
// 开始审核
const handleStartReview = (item: any) => {
message.info(`开始审核 ${item.SampleName}`);
};
// 文件评审
const handleDocumentReview = (item: any) => {
message.info(`文件评审 ${item.SampleName}`);
};
// 现场评审
const handleSiteReview = (item: any) => {
message.info(`现场评审 ${item.SampleName}`);
};
// 测试管理
const handleTestManagement = (item: any) => {
message.info(`测试管理 ${item.SampleName}`);
};
// 签发证书
const handleIssueCertificate = (item: any) => {
message.info(`签发证书给 ${item.SampleName}`);
};
// 下载证书
const handleDownloadCertificate = (item: any) => {
message.info(`下载 ${item.SampleName} 的证书`);
};
// 防抖搜索函数
const debouncedSearch = () => {
if (searchTimer.value) {
clearTimeout(searchTimer.value);
}
searchTimer.value = setTimeout(() => {
fetchData();
}, 500); // 500ms延时
};
// 监听筛选条件变化,自动重新获取数据
watch(searchText, () => {
debouncedSearch();
});
// 状态和时间筛选立即执行
watch(
[statusFilter, timeFilter],
() => {
fetchData();
},
{ deep: true }
);
onMounted(() => {
fetchData();
});
// 处理关闭并刷新数据
const handleCloseAndRefresh = () => {
console.log('关闭mainSupplier组件并刷新数据');
showMainSupplier.value = false;
fetchData();
};
</script>
<template>
<div>
<MainSupplier v-if="showMainSupplier" @close-and-refresh="handleCloseAndRefresh"></MainSupplier>
<NSpace v-if="!showMainSupplier" vertical size="large" style="padding: 20px">
<!-- 统计卡片区域 -->
<NGrid cols="4" x-gap="20" responsive="screen" :collapsed-rows="1">
<NGridItem>
<NCard>
<template #header>
<NSpace align="center" justify="space-between">
<NSpace align="center" size="small">
<NIcon size="20" color="#1890ff">
<svg viewBox="0 0 24 24">
<path
d="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20Z"
/>
</svg>
</NIcon>
<NText depth="3" style="font-size: 14px">{{ statistics.totalApplications.label }}</NText>
</NSpace>
<NText type="success" style="font-size: 12px; font-weight: 500">
{{ statistics.totalApplications.change }}%
</NText>
</NSpace>
</template>
<NStatistic :value="statistics.totalApplications.value"></NStatistic>
</NCard>
</NGridItem>
<NGridItem>
<NCard>
<template #header>
<NSpace align="center" justify="space-between">
<NSpace align="center" size="small">
<NIcon size="20" color="#722ed1">
<svg viewBox="0 0 24 24">
<path
d="M19,3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5C21,3.89 20.1,3 19,3M19,19H5V5H19V19Z"
/>
</svg>
</NIcon>
<NText depth="3" style="font-size: 14px">{{ statistics.monthlyApplications.label }}</NText>
</NSpace>
<NText type="success" style="font-size: 12px; font-weight: 500">
{{ statistics.monthlyApplications.change }}%
</NText>
</NSpace>
</template>
<NStatistic :value="statistics.monthlyApplications.value"></NStatistic>
</NCard>
</NGridItem>
<NGridItem>
<NCard>
<template #header>
<NSpace align="center" justify="space-between">
<NSpace align="center" size="small">
<NIcon size="20" color="#52c41a">
<svg viewBox="0 0 24 24">
<path d="M9,20.42L2.79,14.21L5.62,11.38L9,14.77L18.88,4.88L21.71,7.71L9,20.42Z" />
</svg>
</NIcon>
<NText depth="3" style="font-size: 14px">{{ statistics.passRate.label }}</NText>
</NSpace>
<NText type="success" style="font-size: 12px; font-weight: 500">
{{ statistics.passRate.change }}%
</NText>
</NSpace>
</template>
<NStatistic :value="statistics.passRate.value" suffix="%"></NStatistic>
</NCard>
</NGridItem>
<NGridItem>
<NCard>
<template #header>
<NSpace align="center" justify="space-between">
<NSpace align="center" size="small">
<NIcon size="20" color="#fa8c16">
<svg viewBox="0 0 24 24">
<path
d="M12,20A7,7 0 0,1 5,13A7,7 0 0,1 12,6A7,7 0 0,1 19,13A7,7 0 0,1 12,20M19.03,7.39L20.45,5.97C20,5.46 19.55,5 19.04,4.56L17.62,6C16.07,4.74 14.12,4 12,4A9,9 0 0,0 3,13A9,9 0 0,0 12,22C17,22 21,17.97 21,13C21,10.88 20.26,8.93 19.03,7.39M11,14H13V8H11M15,1H9V3H15V1Z"
/>
</svg>
</NIcon>
<NText depth="3" style="font-size: 14px">{{ statistics.pendingReview.label }}</NText>
</NSpace>
<NText depth="3" style="font-size: 12px">{{ statistics.pendingReview.note }}</NText>
</NSpace>
</template>
<NStatistic :value="statistics.pendingReview.value"></NStatistic>
</NCard>
</NGridItem>
</NGrid>
<!-- 搜索和筛选区域 -->
<NCard>
<NSpace align="center" justify="space-between">
<NInput v-model:value="searchText" placeholder="搜索实验室名称/编号/负责人" style="width: 400px" clearable>
<template #prefix>
<NIcon>
<svg viewBox="0 0 24 24">
<path
d="M9.5,3A6.5,6.5 0 0,1 16,9.5C16,11.11 15.41,12.59 14.44,13.73L14.71,14H15.5L20.5,19L19,20.5L14,15.5V14.71L13.73,14.44C12.59,15.41 11.11,16 9.5,16A6.5,6.5 0 0,1 3,9.5A6.5,6.5 0 0,1 9.5,3M9.5,5C7,5 5,7 5,9.5C5,12 7,14 9.5,14C12,14 14,12 14,9.5C14,7 12,5 9.5,5Z"
/>
</svg>
</NIcon>
</template>
</NInput>
<NSpace>
<NSelect
v-model:value="statusFilter"
:options="statusOptions"
placeholder="状态筛选"
style="width: 120px"
clearable
></NSelect>
<NSelect
v-model:value="timeFilter"
:options="timeOptions"
placeholder="时间筛选"
style="width: 120px"
clearable
></NSelect>
</NSpace>
<NButton type="primary" @click="handleAddApplication">
<template #icon>
<NIcon>
<svg viewBox="0 0 24 24">
<path d="M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z" />
</svg>
</NIcon>
</template>
新增认证
</NButton>
</NSpace>
</NCard>
<!-- 申请列表 -->
<NCard>
<template #header>
<NSpace justify="space-between">
<NText style="font-size: 16px; font-weight: 600">认证申请列表</NText>
<NText depth="3" style="font-size: 14px">{{ applicationList.length }} 条记录</NText>
</NSpace>
</template>
<NSpace vertical size="large">
<!-- 空状态显示 -->
<div v-if="applicationList.length === 0 && !loading" style="text-align: center; padding: 60px 0">
<NIcon size="64" depth="3" style="margin-bottom: 16px">
<svg viewBox="0 0 24 24">
<path d="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20Z" />
</svg>
</NIcon>
<NText depth="3" style="font-size: 16px; display: block; margin-bottom: 8px">暂无申请记录</NText>
<NText depth="3" style="font-size: 14px">请尝试调整筛选条件或添加新的认证申请</NText>
</div>
<!-- 加载状态 -->
<div v-if="loading" style="text-align: center; padding: 60px 0">
<NIcon size="32" style="animation: spin 1s linear infinite; margin-bottom: 16px">
<svg viewBox="0 0 24 24">
<path d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
</svg>
</NIcon>
<NText depth="3" style="font-size: 16px">加载中...</NText>
</div>
<NCard v-for="item in applicationList" :key="item.Kvid" embedded>
<template #header>
<NSpace justify="space-between" align="center">
<NSpace align="center">
<NText style="font-size: 18px; font-weight: 600">{{ item.SampleName }}</NText>
<NTag type="info" :color="{ color: item.statusColor, textColor: 'white' }">
{{ item.status }}
</NTag>
</NSpace>
<NSpace>
<NButton
v-if="item.actions.includes('查看详情')"
quaternary
type="primary"
@click="handleViewDetail(item)"
>
查看详情
</NButton>
<NButton
v-if="item.actions.includes('提交申请')"
type="primary"
@click="handleSubmitApplication(item)"
>
提交申请
</NButton>
<NButton v-if="item.actions.includes('删除')" type="error" @click="handleDelete(item)">删除</NButton>
<NButton v-if="item.actions.includes('开始审核')" type="primary" @click="handleStartReview(item)">
开始审核
</NButton>
<NButton v-if="item.actions.includes('文件评审')" type="primary" @click="handleDocumentReview(item)">
文件评审
</NButton>
<NButton v-if="item.actions.includes('现场评审')" type="primary" @click="handleSiteReview(item)">
现场评审
</NButton>
<NButton v-if="item.actions.includes('测试管理')" type="primary" @click="handleTestManagement(item)">
测试管理
</NButton>
<NButton
v-if="item.actions.includes('签发证书')"
type="warning"
@click="handleIssueCertificate(item)"
>
签发证书
</NButton>
<NButton
v-if="item.actions.includes('下载证书')"
type="success"
@click="handleDownloadCertificate(item)"
>
下载证书
</NButton>
</NSpace>
</NSpace>
</template>
<NSpace vertical>
<NSpace align="center">
<NText depth="3">申请编号:</NText>
<NText strong>{{ item.ReportId }}</NText>
</NSpace>
<NGrid cols="5" x-gap="16" y-gap="16" responsive="screen">
<NGridItem>
<NSpace align="center" size="small">
<NIcon size="16" depth="3">
<svg viewBox="0 0 24 24">
<path d="M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z" />
</svg>
</NIcon>
<NText depth="3">所属机构</NText>
<NText>{{ item.SampleManufacturerName || '暂无' }}</NText>
</NSpace>
</NGridItem>
<NGridItem>
<NSpace align="center" size="small">
<NIcon size="16" depth="3">
<svg viewBox="0 0 24 24">
<path
d="M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,14C16.42,14 20,15.79 20,18V20H4V18C4,15.79 7.58,14 12,14Z"
/>
</svg>
</NIcon>
<NText depth="3">负责人</NText>
<NText>{{ item.SampleManufacturerContactName || '暂无' }}</NText>
</NSpace>
</NGridItem>
<NGridItem>
<NSpace align="center" size="small">
<NIcon size="16" depth="3">
<svg viewBox="0 0 24 24">
<path
d="M6.62,10.79C8.06,13.62 10.38,15.94 13.21,17.38L15.41,15.18C15.69,14.9 16.08,14.82 16.43,14.93C17.55,15.3 18.75,15.5 20,15.5A1,1 0 0,1 21,16.5V20A1,1 0 0,1 20,21A17,17 0 0,1 3,4A1,1 0 0,1 4,3H7.5A1,1 0 0,1 8.5,4C8.5,5.25 8.7,6.45 9.07,7.57C9.18,7.92 9.1,8.31 8.82,8.59L6.62,10.79Z"
/>
</svg>
</NIcon>
<NText depth="3">联系电话</NText>
<NText>{{ item.SampleManufacturerContactNumber || '暂无' }}</NText>
</NSpace>
</NGridItem>
<NGridItem>
<NSpace align="center" size="small">
<NIcon size="16" depth="3">
<svg viewBox="0 0 24 24">
<path
d="M12,2C8.13,2 5,5.13 5,9C5,14.25 12,22 12,22S19,14.25 19,9C19,5.13 15.87,2 12,2M12,11.5A2.5,2.5 0 0,1 9.5,9A2.5,2.5 0 0,1 12,6.5A2.5,2.5 0 0,1 14.5,9A2.5,2.5 0 0,1 12,11.5Z"
/>
</svg>
</NIcon>
<NText depth="3">地址</NText>
<NText>{{ item.SampleManufacturerAddress || '暂无' }}</NText>
</NSpace>
</NGridItem>
<NGridItem>
<NSpace align="center" size="small">
<NIcon size="16" depth="3">
<svg viewBox="0 0 24 24">
<path
d="M19,19H5V8H19M16,1V3H8V1H6V3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5C21,3.89 20.1,3 19,3H18V1M17,12H12V17H17V12Z"
/>
</svg>
</NIcon>
<NText depth="3">申请日期</NText>
<NText>{{ item.applyDate }}</NText>
</NSpace>
</NGridItem>
</NGrid>
<NSpace vertical size="small">
<NSpace justify="space-between" align="center">
<NText depth="3">认证进度</NText>
<NText style="font-size: 12px; color: #1890ff">{{ item.progress }}% 完成</NText>
</NSpace>
<NProgress
type="line"
:percentage="item.progress"
:color="item.statusColor"
:show-indicator="false"
:height="8"
:border-radius="4"
></NProgress>
<NSpace justify="space-between" style="margin-top: 8px">
<NSpace
v-for="(step, index) in ['待审核', '文件审核', '现场评审', '比对测试', '认证签发', '认证完成']"
:key="index"
vertical
size="small"
align="center"
>
<div
:style="{
width: '12px',
height: '12px',
borderRadius: '50%',
backgroundColor: item.progress >= (index + 1) * 20 ? item.statusColor : '#e8e8e8',
border:
item.progress >= (index + 1) * 20 ? `2px solid ${item.statusColor}` : '2px solid #e8e8e8'
}"
></div>
<NText
:style="{
fontSize: '12px',
color: item.progress >= (index + 1) * 20 ? item.statusColor : '#999',
fontWeight: item.progress >= (index + 1) * 20 ? '500' : 'normal'
}"
>
{{ step }}
</NText>
</NSpace>
</NSpace>
</NSpace>
</NSpace>
</NCard>
</NSpace>
</NCard>
</NSpace>
</div>
</template>
<style scoped>
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>
<script setup lang="ts">
import { onMounted, ref, watch } from 'vue';
import { request } from '@/service/request';
// 主体框架内置的第三方方法通过window对象暴露、供外部组件使用
// 引入消息提示
const message = (window as any).$message;
// 获取主题状态(已删除,因为使用naive ui组件不需要自定义主题计算)
const loading = ref<boolean>(false);
const searchText = ref<string>('');
const statusFilter = ref<string>('');
const timeFilter = ref<string>('');
// 搜索防抖定时器
const searchTimer = ref<NodeJS.Timeout | null>(null);
// 统计数据
const statistics = ref({
totalApplications: { value: 0, change: 12.5, label: '总申请数' },
monthlyApplications: { value: 0, change: 8.3, label: '本月申请' },
passRate: { value: 0, change: 5.2, label: '认证通过率' },
pendingReview: { value: 0, label: '待审查数量', note: '30天内' }
});
// 申请列表数据类型定义
interface ApplicationItem {
Kvid: string;
ReportId: string;
SampleName: string;
StatusType: string;
SampleManufacturerName: string;
SampleManufacturerContactName: string;
SampleManufacturerContactNumber: string;
SampleManufacturerAddress: string;
DealDate: string;
CreateTime: string;
status: string;
statusColor: string;
applyDate: string;
progress: number;
actions: string[];
}
// 申请列表数据
const applicationList = ref<ApplicationItem[]>([]);
// 筛选选项
const statusOptions = [
{ label: '全部状态', value: '' },
{ label: '草稿', value: 0 },
{ label: '待审核', value: 100 },
{ label: '文件审核', value: 300 },
{ label: '现场评审', value: 400 },
{ label: '比对测试', value: 500 },
{ label: '认证签发', value: 700 },
{ label: '认证完成', value: 2147483647 }
];
const timeOptions = [
{ label: '全部时间', value: '' },
{ label: '近7天', value: '7d' },
{ label: '近30天', value: '30d' },
{ label: '近3个月', value: '3m' }
];
// 辅助函数:根据状态获取颜色
const getStatusColor = (status: string) => {
const statusColorMap: { [key: string]: string } = {
待审核: '#f56a00', // 橙色 - 待处理
文件审核: '#1890ff', // 蓝色 - 进行中
现场评审: '#722ed1', // 紫色 - 重要评审
比对测试: '#eb2f96', // 粉红色 - 测试阶段
认证签发: '#13c2c2', // 青色 - 即将完成
认证完成: '#52c41a' // 绿色 - 已完成
};
return statusColorMap[status] || '#666';
};
// 辅助函数:根据状态获取进度
const getProgressByStatus = (status: string) => {
const progressMap: { [key: string]: number } = {
草稿: 0,
待审核: 20,
文件审核: 40,
现场评审: 60,
比对测试: 80,
认证签发: 90,
认证完成: 100
};
return progressMap[status] || 0;
};
// 辅助函数:根据状态获取操作按钮
const getActionsByStatus = (status: string) => {
const actionsMap: { [key: string]: string[] } = {
草稿: ['查看详情', '提交申请', '删除'],
待审核: ['查看详情', '开始审核'],
文件审核: ['查看详情', '文件评审'],
现场评审: ['查看详情', '现场评审'],
比对测试: ['查看详情', '测试管理'],
认证签发: ['查看详情', '签发证书'],
认证完成: ['查看详情', '下载证书']
};
return actionsMap[status] || ['查看详情'];
};
// 辅助函数:将API的StatusType映射为显示状态
const mapStatusType = (statusType: string) => {
const statusMap: { [key: string]: string } = {
Unsupported: '草稿',
CommissionAccept: '待审核',
TaskAssign: '文件审核',
DataEntry: '现场评审',
ReportPreparation: '比对测试',
ReportIssue: '认证签发',
ReportCollected: '认证完成'
};
return statusMap[statusType] || '待审核';
};
// 辅助函数:格式化日期
const formatDate = (dateString: string) => {
if (!dateString) return '';
try {
const date = new Date(dateString);
return date
.toLocaleDateString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
})
.replace(/\//g, '-');
} catch (error) {
return dateString;
}
};
// 获取数据
const fetchData = async () => {
loading.value = true;
try {
const { data, error } = await request({
url: '/Restful/Kivii.Lims.Entities.Report/Query.json',
method: 'get',
params: {
// 根据实际API参数调整
...(searchText.value && {
QueryKeys:
'ReportId,SampleManufacturerName,SampleName,SampleManufacturerContactName,SampleManufacturerAddress',
QueryValues: searchText.value
}),
...(statusFilter.value && { Status: statusFilter.value }),
...(timeFilter.value && { timeRange: timeFilter.value }),
...{ OrderBy: 'Status' }
}
});
if (error) {
console.error('获取数据失败:', error);
message.error('获取数据失败,请稍后重试');
return;
}
if (data && data.Results) {
// 统计数据继续使用模拟数据,但更新总数
const totalCount = data.Total || data.Results.length;
statistics.value.totalApplications.value = totalCount;
// 更新申请列表数据
if (Array.isArray(data.Results)) {
applicationList.value = data.Results.map((item: any) => ({
// 直接使用API字段名
...item,
// 保留转换后的自定义字段
status: mapStatusType(item.StatusType),
statusColor: getStatusColor(mapStatusType(item.StatusType)),
applyDate: formatDate(item.DealDate),
progress: getProgressByStatus(mapStatusType(item.StatusType)),
actions: getActionsByStatus(mapStatusType(item.StatusType))
}));
}
message.success('数据获取成功');
} else {
message.error('获取数据失败:响应格式错误');
}
} catch (error: any) {
console.error('获取数据失败:', error);
message.error('获取数据失败,请稍后重试');
} finally {
loading.value = false;
}
};
// 新增认证
const handleAddApplication = () => {
message.info('跳转到新增认证页面');
};
// 查看详情
const handleViewDetail = (item: any) => {
message.info(`查看 ${item.SampleName} 详情`);
};
// 开始审核
const handleStartReview = (item: any) => {
message.info(`开始审核 ${item.SampleName}`);
};
// 文件评审
const handleDocumentReview = (item: any) => {
message.info(`文件评审 ${item.SampleName}`);
};
// 现场评审
const handleSiteReview = (item: any) => {
message.info(`现场评审 ${item.SampleName}`);
};
// 测试管理
const handleTestManagement = (item: any) => {
message.info(`测试管理 ${item.SampleName}`);
};
// 签发证书
const handleIssueCertificate = (item: any) => {
message.info(`签发证书给 ${item.SampleName}`);
};
// 下载证书
const handleDownloadCertificate = (item: any) => {
message.info(`下载 ${item.SampleName} 的证书`);
};
// 防抖搜索函数
const debouncedSearch = () => {
if (searchTimer.value) {
clearTimeout(searchTimer.value);
}
searchTimer.value = setTimeout(() => {
fetchData();
}, 500); // 500ms延时
};
// 监听筛选条件变化,自动重新获取数据
watch(searchText, () => {
debouncedSearch();
});
// 状态和时间筛选立即执行
watch(
[statusFilter, timeFilter],
() => {
fetchData();
},
{ deep: true }
);
onMounted(() => {
fetchData();
});
</script>
<template>
<NSpace vertical size="large" style="padding: 20px">
<!-- 统计卡片区域 -->
<NGrid cols="4" x-gap="20" responsive="screen" :collapsed-rows="1">
<NGridItem>
<NCard>
<template #header>
<NSpace align="center" justify="space-between">
<NSpace align="center" size="small">
<NIcon size="20" color="#1890ff">
<svg viewBox="0 0 24 24">
<path d="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20Z" />
</svg>
</NIcon>
<NText depth="3" style="font-size: 14px">{{ statistics.totalApplications.label }}</NText>
</NSpace>
<NText type="success" style="font-size: 12px; font-weight: 500">
{{ statistics.totalApplications.change }}%
</NText>
</NSpace>
</template>
<NStatistic :value="statistics.totalApplications.value" />
</NCard>
</NGridItem>
<NGridItem>
<NCard>
<template #header>
<NSpace align="center" justify="space-between">
<NSpace align="center" size="small">
<NIcon size="20" color="#722ed1">
<svg viewBox="0 0 24 24">
<path
d="M19,3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5C21,3.89 20.1,3 19,3M19,19H5V5H19V19Z"
/>
</svg>
</NIcon>
<NText depth="3" style="font-size: 14px">{{ statistics.monthlyApplications.label }}</NText>
</NSpace>
<NText type="success" style="font-size: 12px; font-weight: 500">
{{ statistics.monthlyApplications.change }}%
</NText>
</NSpace>
</template>
<NStatistic :value="statistics.monthlyApplications.value" />
</NCard>
</NGridItem>
<NGridItem>
<NCard>
<template #header>
<NSpace align="center" justify="space-between">
<NSpace align="center" size="small">
<NIcon size="20" color="#52c41a">
<svg viewBox="0 0 24 24">
<path d="M9,20.42L2.79,14.21L5.62,11.38L9,14.77L18.88,4.88L21.71,7.71L9,20.42Z" />
</svg>
</NIcon>
<NText depth="3" style="font-size: 14px">{{ statistics.passRate.label }}</NText>
</NSpace>
<NText type="success" style="font-size: 12px; font-weight: 500">{{ statistics.passRate.change }}%</NText>
</NSpace>
</template>
<NStatistic :value="statistics.passRate.value" suffix="%" />
</NCard>
</NGridItem>
<NGridItem>
<NCard>
<template #header>
<NSpace align="center" justify="space-between">
<NSpace align="center" size="small">
<NIcon size="20" color="#fa8c16">
<svg viewBox="0 0 24 24">
<path
d="M12,20A7,7 0 0,1 5,13A7,7 0 0,1 12,6A7,7 0 0,1 19,13A7,7 0 0,1 12,20M19.03,7.39L20.45,5.97C20,5.46 19.55,5 19.04,4.56L17.62,6C16.07,4.74 14.12,4 12,4A9,9 0 0,0 3,13A9,9 0 0,0 12,22C17,22 21,17.97 21,13C21,10.88 20.26,8.93 19.03,7.39M11,14H13V8H11M15,1H9V3H15V1Z"
/>
</svg>
</NIcon>
<NText depth="3" style="font-size: 14px">{{ statistics.pendingReview.label }}</NText>
</NSpace>
<NText depth="3" style="font-size: 12px">{{ statistics.pendingReview.note }}</NText>
</NSpace>
</template>
<NStatistic :value="statistics.pendingReview.value" />
</NCard>
</NGridItem>
</NGrid>
<!-- 搜索和筛选区域 -->
<NCard>
<NSpace align="center" justify="space-between">
<NInput v-model:value="searchText" placeholder="搜索实验室名称/编号/负责人" style="width: 400px" clearable>
<template #prefix>
<NIcon>
<svg viewBox="0 0 24 24">
<path
d="M9.5,3A6.5,6.5 0 0,1 16,9.5C16,11.11 15.41,12.59 14.44,13.73L14.71,14H15.5L20.5,19L19,20.5L14,15.5V14.71L13.73,14.44C12.59,15.41 11.11,16 9.5,16A6.5,6.5 0 0,1 3,9.5A6.5,6.5 0 0,1 9.5,3M9.5,5C7,5 5,7 5,9.5C5,12 7,14 9.5,14C12,14 14,12 14,9.5C14,7 12,5 9.5,5Z"
/>
</svg>
</NIcon>
</template>
</NInput>
<NSpace>
<NSelect
v-model:value="statusFilter"
:options="statusOptions"
placeholder="状态筛选"
style="width: 120px"
clearable
/>
<NSelect
v-model:value="timeFilter"
:options="timeOptions"
placeholder="时间筛选"
style="width: 120px"
clearable
/>
</NSpace>
<NButton type="primary" @click="handleAddApplication">
<template #icon>
<NIcon>
<svg viewBox="0 0 24 24">
<path d="M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z" />
</svg>
</NIcon>
</template>
新增认证
</NButton>
</NSpace>
</NCard>
<!-- 申请列表 -->
<NCard>
<template #header>
<NSpace justify="space-between">
<NText style="font-size: 16px; font-weight: 600">认证申请列表</NText>
<NText depth="3" style="font-size: 14px">{{ applicationList.length }} 条记录</NText>
</NSpace>
</template>
<NSpace vertical size="large">
<!-- 空状态显示 -->
<div v-if="applicationList.length === 0 && !loading" style="text-align: center; padding: 60px 0">
<NIcon size="64" depth="3" style="margin-bottom: 16px">
<svg viewBox="0 0 24 24">
<path d="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20Z" />
</svg>
</NIcon>
<NText depth="3" style="font-size: 16px; display: block; margin-bottom: 8px">暂无申请记录</NText>
<NText depth="3" style="font-size: 14px">请尝试调整筛选条件或添加新的认证申请</NText>
</div>
<!-- 加载状态 -->
<div v-if="loading" style="text-align: center; padding: 60px 0">
<NIcon size="32" style="animation: spin 1s linear infinite; margin-bottom: 16px">
<svg viewBox="0 0 24 24">
<path d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
</svg>
</NIcon>
<NText depth="3" style="font-size: 16px">加载中...</NText>
</div>
<NCard v-for="item in applicationList" :key="item.Kvid" embedded>
<template #header>
<NSpace justify="space-between" align="center">
<NSpace align="center">
<NText style="font-size: 18px; font-weight: 600">{{ item.SampleName }}</NText>
<NTag type="info" :color="{ color: item.statusColor, textColor: 'white' }">
{{ item.status }}
</NTag>
</NSpace>
<NSpace>
<NButton
v-if="item.actions.includes('查看详情')"
quaternary
type="primary"
@click="handleViewDetail(item)"
>
查看详情
</NButton>
<NButton v-if="item.actions.includes('开始审核')" type="primary" @click="handleStartReview(item)">
开始审核
</NButton>
<NButton v-if="item.actions.includes('文件评审')" type="primary" @click="handleDocumentReview(item)">
文件评审
</NButton>
<NButton v-if="item.actions.includes('现场评审')" type="primary" @click="handleSiteReview(item)">
现场评审
</NButton>
<NButton v-if="item.actions.includes('测试管理')" type="primary" @click="handleTestManagement(item)">
测试管理
</NButton>
<NButton v-if="item.actions.includes('签发证书')" type="warning" @click="handleIssueCertificate(item)">
签发证书
</NButton>
<NButton
v-if="item.actions.includes('下载证书')"
type="success"
@click="handleDownloadCertificate(item)"
>
下载证书
</NButton>
</NSpace>
</NSpace>
</template>
<NSpace vertical>
<NSpace align="center">
<NText depth="3">申请编号:</NText>
<NText strong>{{ item.ReportId }}</NText>
</NSpace>
<NGrid cols="5" x-gap="16" y-gap="16" responsive="screen">
<NGridItem>
<NSpace align="center" size="small">
<NIcon size="16" depth="3">
<svg viewBox="0 0 24 24">
<path d="M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z" />
</svg>
</NIcon>
<NText depth="3">所属机构</NText>
<NText>{{ item.SampleManufacturerName || '暂无' }}</NText>
</NSpace>
</NGridItem>
<NGridItem>
<NSpace align="center" size="small">
<NIcon size="16" depth="3">
<svg viewBox="0 0 24 24">
<path
d="M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,14C16.42,14 20,15.79 20,18V20H4V18C4,15.79 7.58,14 12,14Z"
/>
</svg>
</NIcon>
<NText depth="3">负责人</NText>
<NText>{{ item.SampleManufacturerContactName || '暂无' }}</NText>
</NSpace>
</NGridItem>
<NGridItem>
<NSpace align="center" size="small">
<NIcon size="16" depth="3">
<svg viewBox="0 0 24 24">
<path
d="M6.62,10.79C8.06,13.62 10.38,15.94 13.21,17.38L15.41,15.18C15.69,14.9 16.08,14.82 16.43,14.93C17.55,15.3 18.75,15.5 20,15.5A1,1 0 0,1 21,16.5V20A1,1 0 0,1 20,21A17,17 0 0,1 3,4A1,1 0 0,1 4,3H7.5A1,1 0 0,1 8.5,4C8.5,5.25 8.7,6.45 9.07,7.57C9.18,7.92 9.1,8.31 8.82,8.59L6.62,10.79Z"
/>
</svg>
</NIcon>
<NText depth="3">联系电话</NText>
<NText>{{ item.SampleManufacturerContactNumber || '暂无' }}</NText>
</NSpace>
</NGridItem>
<NGridItem>
<NSpace align="center" size="small">
<NIcon size="16" depth="3">
<svg viewBox="0 0 24 24">
<path
d="M12,2C8.13,2 5,5.13 5,9C5,14.25 12,22 12,22S19,14.25 19,9C19,5.13 15.87,2 12,2M12,11.5A2.5,2.5 0 0,1 9.5,9A2.5,2.5 0 0,1 12,6.5A2.5,2.5 0 0,1 14.5,9A2.5,2.5 0 0,1 12,11.5Z"
/>
</svg>
</NIcon>
<NText depth="3">地址</NText>
<NText>{{ item.SampleManufacturerAddress || '暂无' }}</NText>
</NSpace>
</NGridItem>
<NGridItem>
<NSpace align="center" size="small">
<NIcon size="16" depth="3">
<svg viewBox="0 0 24 24">
<path
d="M19,19H5V8H19M16,1V3H8V1H6V3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5C21,3.89 20.1,3 19,3H18V1M17,12H12V17H17V12Z"
/>
</svg>
</NIcon>
<NText depth="3">申请日期</NText>
<NText>{{ item.applyDate }}</NText>
</NSpace>
</NGridItem>
</NGrid>
<NSpace vertical size="small">
<NSpace justify="space-between" align="center">
<NText depth="3">认证进度</NText>
<NText style="font-size: 12px; color: #1890ff">{{ item.progress }}% 完成</NText>
</NSpace>
<NProgress
type="line"
:percentage="item.progress"
:color="item.statusColor"
:show-indicator="false"
:height="8"
:border-radius="4"
/>
<NSpace justify="space-between" style="margin-top: 8px">
<NSpace
v-for="(step, index) in ['待审核', '文件审核', '现场评审', '比对测试', '认证签发', '认证完成']"
:key="index"
vertical
size="small"
align="center"
>
<div
:style="{
width: '12px',
height: '12px',
borderRadius: '50%',
backgroundColor: item.progress >= (index + 1) * 20 ? item.statusColor : '#e8e8e8',
border: item.progress >= (index + 1) * 20 ? `2px solid ${item.statusColor}` : '2px solid #e8e8e8'
}"
></div>
<NText
:style="{
fontSize: '12px',
color: item.progress >= (index + 1) * 20 ? item.statusColor : '#999',
fontWeight: item.progress >= (index + 1) * 20 ? '500' : 'normal'
}"
>
{{ step }}
</NText>
</NSpace>
</NSpace>
</NSpace>
</NSpace>
</NCard>
</NSpace>
</NCard>
</NSpace>
</template>
<style scoped>
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment