Browse Source

feat:代码提交

main
上官小雨 5 months ago
committed by masiyuan
parent
commit
4c79fa27b2
  1. 2
      zdxt-web-client/zdxt-admin-nos-plus-ui/.env.development
  2. 2
      zdxt-web-client/zdxt-admin-nos-plus-ui/src/api/nos/type.ts
  3. 2
      zdxt-web-client/zdxt-admin-nos-plus-ui/src/utils/request.ts
  4. 147
      zdxt-web-client/zdxt-admin-nos-plus-ui/src/views/nos/detail.vue
  5. 27
      zdxt-web-client/zdxt-admin-nos-plus-ui/src/views/nos/legalTech.vue
  6. 21
      zdxt-web-client/zdxt-admin-nos-plus-ui/src/views/nos/legalTechEdit.vue
  7. 64
      zdxt-web-client/zdxt-efcode-nos-enforcement-app/src/components/enforcement/nos/AttachmentTaskSource.vue
  8. 85
      zdxt-web-client/zdxt-efcode-nos-enforcement-app/src/components/enforcement/nos/EnforcementInfoSection.vue
  9. 1
      zdxt-web-client/zdxt-efcode-nos-enforcement-app/src/http/http.js
  10. 2
      zdxt-web-client/zdxt-efcode-nos-enforcement-app/src/main.js
  11. 2
      zdxt-web-client/zdxt-efcode-nos-enforcement-app/src/router/nos/nos-base-router.js
  12. 1
      zdxt-web-client/zdxt-efcode-nos-enforcement-app/src/util/fileUpload.js
  13. 13
      zdxt-web-client/zdxt-efcode-nos-enforcement-app/src/util/fileUrl.js
  14. 389
      zdxt-web-client/zdxt-efcode-nos-enforcement-app/src/views/enforcement/nos/components/NosDetailMaterialsTab.vue
  15. 208
      zdxt-web-client/zdxt-efcode-nos-enforcement-app/src/views/enforcement/nos/components/NosDetailTaskTab.vue
  16. 12
      zdxt-web-client/zdxt-efcode-nos-enforcement-app/src/views/enforcement/nos/components/NosListPanel.vue
  17. 21
      zdxt-web-client/zdxt-efcode-nos-enforcement-app/src/views/enforcement/nos/nosAdd.vue
  18. 1
      zdxt-web-client/zdxt-efcode-nos-enforcement-app/src/views/enforcement/nos/nosCloudRecord.vue
  19. 11
      zdxt-web-client/zdxt-efcode-nos-enforcement-app/src/views/enforcement/nos/nosDetail.vue
  20. 282
      zdxt-web-client/zdxt-efcode-nos-enforcement-app/src/views/enforcement/nos/nosEvaluateList.vue
  21. 21
      zdxt-web-client/zdxt-efcode-nos-enforcement-app/src/views/enforcement/nos/nosList.vue
  22. 2
      zdxt-web-client/zdxt-efcode-nos-enforcement-app/vite-env/.env.enforcement-dev
  23. 2
      zdxt-web-client/zdxt-efcode-nos-enterprise-app/src/api/enterprise/index.js
  24. 4
      zdxt-web-client/zdxt-efcode-nos-enterprise-app/src/api/nos/enfDemo.js
  25. 1
      zdxt-web-client/zdxt-efcode-nos-enterprise-app/src/http/http.js
  26. 2
      zdxt-web-client/zdxt-efcode-nos-enterprise-app/src/router/nos/nos-base-router.js
  27. 69
      zdxt-web-client/zdxt-efcode-nos-enterprise-app/src/views/enterprise/nos/components/NosDetailMaterialsTab.vue
  28. 176
      zdxt-web-client/zdxt-efcode-nos-enterprise-app/src/views/enterprise/nos/components/NosDetailTaskTab.vue
  29. 12
      zdxt-web-client/zdxt-efcode-nos-enterprise-app/src/views/enterprise/nos/components/NosListPanel.vue
  30. 2
      zdxt-web-client/zdxt-efcode-nos-enterprise-app/src/views/enterprise/nos/message/nosEvaluationList.vue
  31. 2
      zdxt-web-client/zdxt-efcode-nos-enterprise-app/src/views/enterprise/nos/nosCloudRecord.vue
  32. 21
      zdxt-web-client/zdxt-efcode-nos-enterprise-app/src/views/enterprise/nos/nosDetail.vue
  33. 20
      zdxt-web-client/zdxt-efcode-nos-enterprise-app/src/views/enterprise/nos/nosList.vue
  34. 2
      zdxt-web-client/zdxt-efcode-nos-enterprise-app/vite-env/.env.enterprise-dev

2
zdxt-web-client/zdxt-admin-nos-plus-ui/.env.development

@ -1,5 +1,5 @@
# VITE_APP_PROXY_TARGET = 'http://139.186.148.21:8080'
VITE_APP_PROXY_TARGET = 'http://192.168.1.11:28080'
VITE_APP_PROXY_TARGET = 'http://192.168.1.9:28080'
# 页面标题
VITE_APP_TITLE = zdxt-Vue-Plus多租户管理系统
VITE_APP_LOGO_TITLE = 行政执法监督码

2
zdxt-web-client/zdxt-admin-nos-plus-ui/src/api/nos/type.ts

@ -333,7 +333,7 @@ export interface NosLawEnforcementTaskItem {
organId?: string;
params?: MapObject;
/**
* 是否通过(0-有异议,1-通过)
* 是否通过(0-不通过,1-通过)
*/
pass?: number;
/**

2
zdxt-web-client/zdxt-admin-nos-plus-ui/src/utils/request.ts

@ -97,6 +97,8 @@ service.interceptors.request.use(
if (config.data instanceof FormData) {
delete config.headers['Content-Type'];
}
console.log(111,config);
return config;
},
(error: any) => {

147
zdxt-web-client/zdxt-admin-nos-plus-ui/src/views/nos/detail.vue

@ -43,11 +43,11 @@
<div class="section">
<div class="section-title">检查对象</div>
<div class="detail-row">
<span class="detail-label">{{detailData?.taskTypeId != 5 ? '执法单位' : '巡查单位'}}</span>
<span class="detail-label">{{detailData?.taskTypeId != 5 || detailData?.taskOriginalTypeId != 5 ? '执法单位' : '巡查单位'}}</span>
<span class="detail-value">{{ detailData?.lawEnforcementAgency || '--' }}</span>
</div>
<div class="detail-row">
<span class="detail-label">{{detailData?.taskTypeId != 5 ? '执法人员' : '参与人员'}}</span>
<span class="detail-label">{{detailData?.taskTypeId != 5 || detailData?.taskOriginalTypeId != 5 ? '执法人员' : '参与人员'}}</span>
<div class="detail-value">
<template v-if="(detailData?.lawEnforcer || []).length">
<p v-for="(person, index) in detailData?.lawEnforcer || []" :key="index" class="detail-text">
@ -63,7 +63,7 @@
<!-- 执法信息 -->
<div class="detail-info-right">
<div class="section">
<div class="section-title">{{detailData?.taskTypeId != 5 ? '执法信息' : '巡查信息'}}</div>
<div class="section-title">{{detailData?.taskTypeId != 5 || detailData?.taskOriginalTypeId != 5 ? '执法信息' : '巡查信息'}}</div>
<div class="detail-row">
<span class="detail-label">企业名称</span>
<div class="detail-value">
@ -77,8 +77,8 @@
<span class="detail-label">类  别</span>
<span class="detail-value">{{ detailData?.taskTypeName || '--' }}</span>
</div>
<div class="detail-row" v-if="detailData?.taskTypeId == 5 || detailData?.taskTypeId == 1">
<span class="detail-label">{{detailData?.taskTypeId != 5 ? '执法事项' : '巡查事项'}}</span>
<div class="detail-row" v-if="detailData?.taskTypeId == 5 || detailData?.taskTypeId == 1 || detailData?.taskOriginalTypeId == 5 || detailData?.taskOriginalTypeId == 1">
<span class="detail-label">{{detailData?.taskTypeId != 5 || detailData?.taskOriginalTypeId != 5 ? '执法事项' : '巡查事项'}}</span>
<span class="detail-value">{{ detailData?.lawEnforcementMatters || '--' }}</span>
</div>
<div v-if="requirementLabel" class="detail-row">
@ -94,7 +94,7 @@
</div>
</div>
<div class="detail-row">
<span class="detail-label">{{detailData?.taskTypeId != 5 ? '执法依据' : '依  据'}}</span>
<span class="detail-label">{{detailData?.taskTypeId != 5 || detailData?.taskOriginalTypeId != 5 ? '执法依据' : '依  据'}}</span>
<div class="detail-value">
<template v-if="(detailData?.legalBasis || []).length">
<p v-for="(basis, index) in detailData?.legalBasis || []" :key="index" class="detail-text">
@ -119,6 +119,26 @@
</div>
</div>
</div>
<template v-if="isCompletedTask">
<el-divider />
<!-- 调查信息 -->
<div class="section">
<div class="section-title title-with-btn">
<span>调查信息</span>
<el-button size="small" @click="copyInvestigationInfo">复制</el-button>
</div>
<div class="investigation-content">
<p>
<template v-if="noticeFileName"><span class="text-nowrap">依据</span>“{{ noticeFileName }}”通知/公告,</template>{{ investigationParagraph1Body }}
</p>
<p>{{ investigationParagraph2 }}</p>
<p>
{{ investigationParagraph3 }}<br />
检查时间{{ formattedCheckTimeCn }}
</p>
</div>
</div>
</template>
<el-divider />
<!-- 企业材料 -->
<div class="section">
@ -184,7 +204,7 @@
</div>
<div class="detail-row check-result-row">
<span class="detail-label">检查时间:</span>
<div class="detail-value">{{ detailData?.checkTime || '--' }}</div>
<div class="detail-value">{{detailData?.taskStatus == 5 ? detailData?.deadline : detailData?.checkTime || '--' }}</div>
</div>
</div>
@ -296,6 +316,18 @@ const requirementLabel = computed(() => {
return CATEGORY_REQUIREMENT_LABEL_MAP[typeName] || '';
});
const isCompletedTask = computed(() => detailData.value?.taskStatus === 4);
const noticeFileName = computed(() => detailData.value?.proclamation?.[0]?.fileName || '');
const enforcementPersonNames = computed(() => {
return detailData.value?.lawEnforcer?.map((person) => person.lawEnforcerName).filter(Boolean).join('、') || '';
});
const requirementItems = computed(() => {
return detailData.value?.taskItemList?.map((item) => item.itemName).filter(Boolean).join('、') || '';
});
const onBack = () => {
router.back();
};
@ -305,6 +337,73 @@ const formatDateTime = (time?: string) => {
return parseTime(time, '{y}-{m}-{d} {h}:{i}:{s}')?.replace(/:\d{2}$/, '') || '';
};
const formatDateTimeCn = (time?: string) => {
if (!time) return '';
const parsed = parseTime(time, '{y}年{m}月{d}日{h}时{i}分{s}秒');
return parsed || time;
};
const formattedCreateTimeCn = computed(() => formatDateTimeCn(detailData.value?.createTime));
const formattedCheckTimeCn = computed(() => formatDateTimeCn(detailData.value?.checkTime));
const investigationParagraph1Body = computed(() => {
const detail = detailData.value;
if (!detail) return '';
const personType = detail.taskTypeId === 5 ? '巡查人员' : '执法人员';
const enforcementUnit = detail.lawEnforcementAgency || '深圳市执法局';
return `在${formattedCreateTimeCn.value}${enforcementUnit},${personType}${enforcementPersonNames.value},对${detail.enterpriseName || ''}进行了${detail.taskTypeName || ''}。`;
});
const investigationParagraph2 = computed(() => {
const detail = detailData.value;
if (!detail) return '';
let text = '';
if (detail.taskTypeId === 1 || detail.taskTypeId === 5) {
const matterType = detail.taskTypeId === 5 ? '巡查事项' : '执法事项';
text += `${matterType},${detail.lawEnforcementMatters || ''};`;
}
text += `${requirementLabel.value}有:${requirementItems.value}。`;
return text;
});
const investigationParagraph3 = computed(() => {
return `企业已提交材料,经核验,企业通过本次${detailData.value?.taskTypeName || ''}。`;
});
const investigationText = computed(() => {
return `${noticeFileName.value ? `依据“${noticeFileName.value}”通知/公告,` : ''}${investigationParagraph1Body.value}\n\n${investigationParagraph2.value}\n\n${investigationParagraph3.value}\n检查时间${formattedCheckTimeCn.value}`;
});
const copyInvestigationInfo = async () => {
try {
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(investigationText.value);
ElMessage.success('复制成功');
return;
}
const textArea = document.createElement('textarea');
textArea.value = investigationText.value;
textArea.style.position = 'absolute';
textArea.style.left = '-999999px';
document.body.prepend(textArea);
textArea.select();
try {
document.execCommand('copy');
ElMessage.success('复制成功');
} catch {
ElMessage.error('复制失败,请手动复制');
} finally {
textArea.remove();
}
} catch {
ElMessage.error('复制失败,请手动复制');
}
};
const formatLawEnforcerName = (person: LawEnforcer) => {
return `${person.lawEnforcerName || ''}${person.lawEnforcerPhone ? ` ${person.lawEnforcerPhone}` : ''}`.trim() || '--';
};
@ -318,7 +417,8 @@ const STATUS_OPTIONS = [
{ text: "退回企业补正", value: 2 },
{ text: "待审核", value: 3 },
{ text: "已办结", value: 4 },
{ text: "已办结(超期)", value: 5 }
{ text: "已办结(超期)", value: 5 },
{ text: "转为待核实处置案件", value: 6 },
]
// 使用上面的类型
const getCheckResultText = (taskStatus?: number) => {
@ -328,7 +428,7 @@ const getCheckResultText = (taskStatus?: number) => {
const getItemAuditText = (pass?: number) => {
if (pass === 1) return '通过';
if (pass === 0) return '有异议';
if (pass === 0) return '不通过';
return '待审核';
};
@ -558,6 +658,13 @@ watch(
color: #303133;
}
.title-with-btn {
display: flex;
align-items: center;
gap: 20px;
/* justify-content: space-between; */
}
.detail-row {
display: flex;
align-items: flex-start;
@ -594,6 +701,28 @@ watch(
word-break: break-all;
}
.investigation-content {
padding: 12px;
background: #f8f9fa;
border-radius: 6px;
font-size: 14px;
color: #303133;
line-height: 1.6;
word-break: break-all;
}
.investigation-content p {
margin: 0 0 12px 0;
}
.investigation-content p:last-child {
margin-bottom: 0;
}
.text-nowrap {
white-space: nowrap;
}
.detail-value--notice {
display: flex;
align-items: center;

27
zdxt-web-client/zdxt-admin-nos-plus-ui/src/views/nos/legalTech.vue

@ -17,13 +17,7 @@
</el-form-item>
<el-form-item label="执法单位:">
<el-input
v-model="searchForm.lawEnforcementAgency"
placeholder="请输入关键字"
class="search-input"
clearable
@keyup.enter="handleSearch"
/>
<el-input v-model="searchForm.lawEnforcementAgency" placeholder="请输入关键字" class="search-input" clearable @keyup.enter="handleSearch" />
</el-form-item>
<el-form-item>
@ -34,21 +28,12 @@
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</div>
<!-- 操作区域 -->
<div class="action-panel">
<el-button type="primary" @click="handleAdd">新增</el-button>
</div>
<!-- 数据表格 -->
<div class="table-container">
<el-table
v-loading="loading"
:data="tableData"
border
:header-cell-style="{ backgroundColor: '#f5f7fa', fontWeight: 'bold' }"
>
<el-table v-loading="loading" :data="tableData" border :header-cell-style="{ backgroundColor: '#f5f7fa', fontWeight: 'bold' }">
<el-table-column prop="lawEnforcementAgency" label="执法单位" min-width="160" show-overflow-tooltip />
<el-table-column label="法制材料" min-width="200">
@ -109,10 +94,7 @@ import { useRouter } from 'vue-router';
import { ElMessage } from 'element-plus';
import { Paperclip } from '@element-plus/icons-vue';
import { getLegalTechRecordsPage } from '@/api/nos/index';
import type {
NosLawEnforcementFilingPageVO,
LegalTechRecordsPageResponse
} from '@/api/nos/type';
import type { NosLawEnforcementFilingPageVO, LegalTechRecordsPageResponse } from '@/api/nos/type';
import { parseTime } from '@/utils/ruoyi';
const router = useRouter();
@ -216,7 +198,7 @@ const buildQueryParams = () => {
const getList = async () => {
loading.value = true;
try {
const res = (await getLegalTechRecordsPage(buildQueryParams()) as unknown) as LegalTechRecordsPageResponse;
const res = (await getLegalTechRecordsPage(buildQueryParams())) as unknown as LegalTechRecordsPageResponse;
tableData.value = res.rows || [];
total.value = res.total || 0;
} catch (error) {
@ -278,6 +260,7 @@ onMounted(() => {
}
.search-panel {
display: flex;
background: #fff;
padding: 16px 20px;
border-radius: 8px;

21
zdxt-web-client/zdxt-admin-nos-plus-ui/src/views/nos/legalTechEdit.vue

@ -32,6 +32,7 @@
:show-file-list="true"
:file-list="legalFileList"
:on-remove="(file) => handleFileRemove(file, 'legal')"
:before-upload="beforeUpload"
accept=".pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx"
:limit="1"
:on-exceed="handleExceed"
@ -39,7 +40,7 @@
<div class="upload-box">
<el-icon class="upload-icon"><FolderOpened /></el-icon>
<p>点击或将文件拖拽到这里上传</p>
<p class="upload-tip">文件格式:word、excel、ppt、PDF等文档</p>
<p class="upload-tip">文件格式:word、excel、ppt、PDF等文档,大小不超过200M</p>
</div>
</el-upload>
</div>
@ -54,6 +55,7 @@
:show-file-list="true"
:file-list="technicalFileList"
:on-remove="(file) => handleFileRemove(file, 'technical')"
:before-upload="beforeUpload"
accept=".pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx"
:limit="1"
:on-exceed="handleExceed"
@ -61,7 +63,7 @@
<div class="upload-box">
<el-icon class="upload-icon"><FolderOpened /></el-icon>
<p>点击或将文件拖拽到这里上传</p>
<p class="upload-tip">文件格式:word、excel、ppt、PDF等文档</p>
<p class="upload-tip">文件格式:word、excel、ppt、PDF等文档,大小不超过200M</p>
</div>
</el-upload>
</div>
@ -131,6 +133,21 @@ const loadDeptOptions = async () => {
}
};
const MAX_FILE_SIZE = 200 * 1024 * 1024;
const ALLOWED_FILE_EXT_RE = /\.(pdf|doc|docx|xls|xlsx|ppt|pptx)$/i;
const beforeUpload = (file: File) => {
if (!ALLOWED_FILE_EXT_RE.test(file.name)) {
ElMessage.warning('仅支持上传 word、excel、ppt、PDF 格式文件');
return false;
}
if (file.size > MAX_FILE_SIZE) {
ElMessage.warning('文件大小不能超过 200M');
return false;
}
return true;
};
const handleExceed = () => {
ElMessage.warning('只能上传一个文件,请先删除已上传的文件');
};

64
zdxt-web-client/zdxt-efcode-nos-enforcement-app/src/components/enforcement/nos/AttachmentTaskSource.vue

@ -6,7 +6,17 @@
<div class="file-upload-row label-required-placeholder">
<div class="file-upload-label">附  件 <span class="form-text">(仅限于PDF文档,50M以下,最多五个文件)</span></div>
<div class="uploader-wrapper">
<van-uploader v-model="formData.proclamation" accept=".pdf" multiple :max-count="5" :after-read="onAfterReadImage" />
<van-uploader
v-model="formData.proclamation"
capture="false"
accept=".pdf"
multiple
:max-count="5"
:max-size="50 * 1024 * 1024"
@oversize="onOversize"
:before-read="onBeforeRead"
:after-read="onAfterReadImage"
/>
</div>
</div>
@ -31,7 +41,8 @@
import { computed } from 'vue';
import { uploadFile } from '@/util/fileUpload';
import { toFullFileUrl } from '@/util/fileUrl';
import { showToast } from 'vant';
import 'vant/es/toast/style';
const props = defineProps({
formData: {
type: Object,
@ -50,16 +61,51 @@ const taskSourceName = computed(() => {
const emit = defineEmits(['showTaskSourcePicker', 'open-picker']);
const showUploadToast = (message) => {
showToast({
message,
className: 'attachment-upload-toast'
});
};
// 附件上传前校验
const onBeforeRead = (file) => {
const files = Array.isArray(file) ? file : [file];
let isValid = true;
for (const item of files) {
const isPdf = item.name.toLowerCase().endsWith('.pdf');
const isTypeMatch = item.type === 'application/pdf';
if (!isPdf || !isTypeMatch) {
showUploadToast('仅支持上传 PDF 格式文件');
isValid = false;
break;
}
}
return isValid;
};
// 文件大小超出限制
const onOversize = () => {
showUploadToast('文件大小不能超过 50MB');
};
// 附件上传处理
const onAfterReadImage = (file) => {
const files = Array.isArray(file) ? file : [file];
files.forEach((f) => {
f.status = 'uploading';
f.message = '上传中...';
uploadFile(f, (res) => {
const data = res && res.data ? res.data : res;
const filePath = typeof data === 'string' ? data : (data && (data.url || data.fileUrl || data.path)) || '';
const filePath = res.url;
if (filePath) {
f.url = toFullFileUrl(filePath);
f.name = f.name || f.file?.name || '';
f.status = 'done';
f.message = '上传成功';
} else {
f.status = 'failed';
f.message = '上传失败';
}
});
});
@ -129,4 +175,14 @@ const onAfterReadImage = (file) => {
padding-left: 8px; // 按需调整
box-sizing: border-box;
}
:global(.attachment-upload-toast) {
color: #fff !important;
background: rgba(0, 0, 0, 0.75) !important;
}
:global(.attachment-upload-toast .van-toast__text) {
color: #fff !important;
}
</style>

85
zdxt-web-client/zdxt-efcode-nos-enforcement-app/src/components/enforcement/nos/EnforcementInfoSection.vue

@ -30,14 +30,15 @@
<van-icon name="add-o" class="add-circle-icon" @click="$emit('addPerson')" />
</div>
</div>
<!-- 隐藏字段用于验证执法人员 -->
<van-field
v-if="formData.taskTypeId != 5"
v-model="hasPerson"
name="enforcementPerson"
label=" "
:rules="[{ required: true, message: '请选择执法人员' }]"
class="error-only-field" />
</div>
<!-- 隐藏字段用于验证执法人员 -->
<van-field
v-if="formData.taskTypeId != 5"
v-model="hasPerson"
name="enforcementPerson"
:rules="[{ required: true, message: '请选择执法人员' }]"
style="display: none" />
<!-- 执法类别 -->
<van-field
@ -84,18 +85,32 @@
<van-icon v-else name="close" class="remove-icon" @click.stop="removeMaterialRow(index)" />
</div>
</div>
<van-field
v-if="formData.taskTypeId != 5"
v-model="hasMaterialRequirement"
name="checkItemList"
label=" "
:rules="[{ required: true, message: `请输入${requirementLabel}` }]"
class="error-only-field" />
</div>
</template>
<!-- 执法依据 / 依据 -->
<van-field
v-if="requirementLabel && formData.taskTypeId != 5"
v-model="hasMaterialRequirement"
name="checkItemList"
:rules="[{ required: true, message: `请填写${requirementLabel}` }]"
style="display: none" />
v-if="formData.taskTypeId == 5"
v-model="formData.legalBasis[0]"
name="legalBasis"
label="依  据"
placeholder="请输入依据"
type="textarea"
class="usci-code-field"
rows="2"
autosize
maxlength="1000"
show-word-limit />
<!-- 执法依据 -->
<div class="form-row required">
<div class="form-label"><span class="required-mark" :class="{ 'required-mark--placeholder': formData.taskTypeId == 5 }">*</span>执法依据</div>
<div v-else class="form-row required">
<div class="form-label"><span class="required-mark">*</span>执法依据</div>
<div class="form-value basis-select">
<div v-for="(basis, index) in formData.legalBasis" :key="index" class="basis-item-add">
<span @click="$emit('openBasisPicker', index)" :class="{ placeholder: !basis }">
@ -105,6 +120,13 @@
<van-icon v-else name="close" class="remove-icon" @click.stop="$emit('removeBasis', index)" />
</div>
</div>
<van-field
v-if="formData.taskTypeId != 5"
v-model="hasLegalBasis"
name="legalBasis"
label=" "
:rules="[{ required: true, message: '请选择执法依据' }]"
class="error-only-field" />
</div>
<!-- 原因 -->
@ -174,7 +196,7 @@ const requirementLabel = computed(() => {
// 计算属性:是否有执法人员
const hasPerson = computed(() => {
return props.defaultPerson?.lawEnforcerName || props.additionalPersons.some((p) => p.lawEnforcerName)
return (props.defaultPerson?.lawEnforcerName || props.additionalPersons.some((p) => p.lawEnforcerName)) ? "已填写" : ""
})
// 是否已填写检查项
@ -182,6 +204,11 @@ const hasMaterialRequirement = computed(() => {
return props.formData.checkItemList?.some((item) => item?.trim()) ? "已填写" : ""
})
// 是否已填写执法依据
const hasLegalBasis = computed(() => {
return props.formData.legalBasis?.some((item) => item?.trim()) ? "已填写" : ""
})
const addMaterialRow = () => {
props.formData.checkItemList.push("")
}
@ -219,6 +246,7 @@ const removeMaterialRow = (index) => {
// align-items: center;
padding: 10px;
border-bottom: 1px solid #f0f0f0;
flex-wrap: wrap;
&:last-child {
border-bottom: none;
@ -489,4 +517,29 @@ const removeMaterialRow = (index) => {
padding-left: 8px; // 按需调整
box-sizing: border-box;
}
.error-only-field {
width: 100%;
padding-top: 0;
padding-bottom: 0;
margin-top: -5px;
margin-bottom: -10px;
background: transparent;
&::after {
display: none;
}
:deep(.van-field__label) {
display: none;
}
:deep(.van-field__value) {
padding-left: 2.4rem;
}
:deep(.van-field__value .van-field__body) {
display: none;
}
:deep(.van-field__error-message) {
padding-bottom: 10px;
text-align: left;
}
}
</style>

1
zdxt-web-client/zdxt-efcode-nos-enforcement-app/src/http/http.js

@ -48,6 +48,7 @@ function setupInterceptors(http) {
// if(show){
// return response;
// }
router.push({ path: "/enforcement/login" });
if (response.data.message) {
// Message.error(response.data.message);
}

2
zdxt-web-client/zdxt-efcode-nos-enforcement-app/src/main.js

@ -1,2 +1,4 @@
import App from "@/App"; // 挂载文件
import 'vant/es/toast/style';
createApp(App).use($globalReady).mount("#app");

2
zdxt-web-client/zdxt-efcode-nos-enforcement-app/src/router/nos/nos-base-router.js

@ -6,7 +6,7 @@ const routes = [
name: "nosList",
component: () => import("@/views/enforcement/nos/nosList.vue"),
meta: {
isKeepAlive: false,
isKeepAlive: true,
},
},
// 非现场添加

1
zdxt-web-client/zdxt-efcode-nos-enforcement-app/src/util/fileUpload.js

@ -22,6 +22,7 @@ const uploadFile = (files, callback) => {
files.message = '上传中...';
const form = new FormData();
form.append('file', files.file);
console.log(files.file);
http.post('/app-enf/common/file/upload', form)
.then((res) => {

13
zdxt-web-client/zdxt-efcode-nos-enforcement-app/src/util/fileUrl.js

@ -1,17 +1,14 @@
import { baseUrl } from "@/config/env"
const ABSOLUTE_URL_RE = /^https?:\/\//i
const isAbsoluteUrl = (path) =>
ABSOLUTE_URL_RE.test(path) || path.startsWith("blob:") || path.startsWith("data:")
const isAbsoluteUrl = (path) => typeof path === "string" && /^https?:\/\//i.test(path)
const getApiBase = () => (baseUrl || "").replace(/\/+$/, "")
/**
* ȥ�� VITE_BASE_URL ����ǰ׺��֧���ظ�ǰ׺�����õ�������·������ /profile/2026/...
* 去掉 VITE_BASE_URL 多余前缀,支持重复前缀,得到相对路径,如 /profile/2026/...
*/
export const toRelativeFilePath = (path) => {
if (!path) return ""
if (typeof path !== "string" || !path) return ""
if (isAbsoluteUrl(path)) return path
const prefix = getApiBase()
@ -29,10 +26,10 @@ export const toRelativeFilePath = (path) => {
}
/**
* ƴ��Ϊǰ�˿ɷ���·�����ݵȣ����ظ����ã����� uploader ���ԣ�
* 拼接为前端可访问路径,如 /profile/2026/06/12/xxx.pdf
*/
export const toFullFileUrl = (path) => {
if (!path) return ""
if (typeof path !== "string" || !path) return ""
if (isAbsoluteUrl(path)) return path
const prefix = getApiBase()

389
zdxt-web-client/zdxt-efcode-nos-enforcement-app/src/views/enforcement/nos/components/NosDetailMaterialsTab.vue

@ -7,7 +7,7 @@
<div class="empty-text-sub">等待企业上传材料</div>
</div>
</template>
<template v-else-if="Number(taskStatus) === 5">
<template v-else-if="Number(taskStatus) === 5 && !taskItemList.some((item) => (item.itemValue || []).length > 0)">
<div class="empty-state">
<van-image :src="noDataImg" width="151" height="120" />
<div class="empty-text-main">暂无资料上传</div>
@ -20,62 +20,32 @@
</div>
</template>
<template v-else>
<div
v-for="(item, index) in taskItemList"
:key="index"
class="material-card"
>
<div class="material-title">
<span class="required-mark">*</span>{{ item.itemName }}
</div>
<div v-for="(item, index) in taskItemList" :key="index" class="material-card">
<div class="material-title"><span class="required-mark">*</span>{{ item.itemName }}</div>
<div class="material-media">
<template v-for="(url, fileIndex) in item.itemValue" :key="fileIndex">
<div
v-if="isVideo(url)"
class="media-item media-item--video"
@click="onPreview(url, item.itemValue)"
>
<video
:src="resolveMediaUrl(url)"
muted
preload="metadata"
class="video-thumb"
></video>
<div v-if="isVideo(url)" class="media-item media-item--video" @click="onPreview(url, item.itemValue)">
<video :src="resolveMediaUrl(url)" muted preload="metadata" class="video-thumb"></video>
<van-icon name="play-circle-o" class="play-icon" />
</div>
<div
v-else-if="isPdf(url)"
class="media-item media-item--pdf"
@click="onPreview(url, item.itemValue)"
>
<div v-else-if="isPdf(url)" class="media-item media-item--pdf" @click="onPreview(url, item.itemValue)">
<div class="pdf-icon-wrapper">
<van-icon name="description" class="pdf-icon" />
<span class="pdf-text">PDF</span>
</div>
</div>
<van-image
v-else
:src="resolveMediaUrl(url)"
fit="cover"
class="media-item"
@click="onPreview(url, item.itemValue)"
/>
<van-image v-else :src="resolveMediaUrl(url)" fit="cover" class="media-item" @click="onPreview(url, item.itemValue)" />
</template>
</div>
<van-field
v-if="Number(taskStatus) >= 2"
class="audit-field"
label="审核"
:required="Number(taskStatus) === 3"
>
<van-field v-if="Number(taskStatus) >= 2" class="audit-field" label="审核" :required="Number(taskStatus) === 3">
<template #input>
<van-radio-group v-if="Number(taskStatus) === 3" v-model="item.pass" direction="horizontal" @change="onAuditChange(item)">
<van-radio :name="1" icon-size="16px">通过</van-radio>
<van-radio :name="0" icon-size="16px">有异议</van-radio>
<van-radio :name="0" icon-size="16px">不通过</van-radio>
</van-radio-group>
<span v-else class="readonly-text">{{ item.pass === 1 ? '通过' : (item.pass === 0 ? '有异议' : '') }}</span>
<span v-else class="readonly-text">{{ item.pass === 1 ? '通过' : item.pass === 0 ? '不通过' : '' }}</span>
</template>
</van-field>
@ -86,7 +56,7 @@
type="textarea"
rows="3"
maxlength="1000"
show-word-limit
:show-word-limit="Number(taskStatus) === 3"
placeholder="请输入"
label="原因"
:readonly="Number(taskStatus) !== 3"
@ -95,150 +65,113 @@
/>
</div>
<div class="result-card" v-if="Number(taskStatus) >= 2">
<div class="result-title">检查结果</div>
<template v-if="Number(taskStatus) === 3">
<template v-if="taskItemList.length > 0 && taskItemList.every(item => item.pass === 1)">
<div class="readonly-result-text" style="padding-top: 4px;">检查结果:通过</div>
<div class="result-card" v-if="Number(taskStatus) >= 2">
<div class="result-title">检查结果</div>
<template v-if="Number(taskStatus) === 3">
<template v-if="taskItemList.length > 0 && taskItemList.every((item) => item.pass === 1)">
<div class="readonly-result-text" style="padding-top: 4px">检查结果:通过</div>
</template>
<template v-else>
<van-radio-group v-model="inspectResult.checkResult" class="result-options" @change="onResultTypeChange">
<van-radio :name="1" icon-size="16px" v-if="Number(taskTypeId) != 5">转为待核实处置案件</van-radio>
<van-radio :name="2" icon-size="16px">退回企业补正材料</van-radio>
</van-radio-group>
<van-field
v-if="inspectResult.checkResult === 2"
v-model="inspectResult.deadline"
class="deadline-field"
label="补正时限"
placeholder="请选择"
readonly
is-link
required
:rules="[{ required: true, message: '请选择补正时限' }]"
@click="showDatePicker = true"
/>
</template>
</template>
<template v-else>
<van-radio-group v-model="inspectResult.checkResult" class="result-options" @change="onResultTypeChange">
<van-radio :name="1" icon-size="16px">转为待核实处置案件</van-radio>
<van-radio :name="2" icon-size="16px">退回企业补正材料</van-radio>
</van-radio-group>
<van-field
v-if="inspectResult.checkResult === 2"
v-model="inspectResult.deadline"
class="deadline-field"
label="补正时限"
placeholder="请选择"
readonly
is-link
required
:rules="[{ required: true, message: '请选择补正时限' }]"
@click="showDatePicker = true"
/>
<div class="readonly-result-text" style="padding-top: 4px">检查结果:{{ TASK_STATUS_TEXT[Number(taskStatus)] }}</div>
<div class="readonly-result-text" v-if="Number(taskStatus) !== 3 && inspectResult.checkResult === 2 && inspectResult.deadline">
补正时限:{{ inspectResult.deadline }}
</div>
<div class="readonly-result-text" v-else>检查时间:{{ Number(taskStatus) === 4 ? inspectResult.checkTime : overdueCheckTime }}</div>
</template>
</template>
<template v-else>
<div class="readonly-result-text" style="padding-top: 4px;">
检查结果:{{ inspectResult.checkResult === 0 ? '通过' : (inspectResult.checkResult === 1 ? '转为待核实处置案件' : (inspectResult.checkResult === 2 ? '退回企业补正材料' : '')) }}
</div>
</template>
<div class="readonly-result-text" v-if="Number(taskStatus) !== 3 && inspectResult.checkTime">
检查时间:{{ inspectResult.checkTime }}
</div>
<div class="readonly-result-text" v-if="Number(taskStatus) !== 3 && inspectResult.checkResult === 2 && inspectResult.deadline">
补正时限:{{ inspectResult.deadline }}
</div>
</div>
<div class="cloud-record-card" v-if="Number(taskStatus) === 4 && Number(digitalVoiceLoggingStatus) !== 0">
<div class="cloud-record-row">
<span class="cloud-record-label">云笔录</span>
<van-button
class="btn-view"
size="small"
plain
type="primary"
@click="onViewCloudRecord"
>
查看
</van-button>
<div class="cloud-record-card" v-if="Number(taskStatus) === 4 && Number(digitalVoiceLoggingStatus) !== 0">
<div class="cloud-record-row">
<span class="cloud-record-label">云笔录</span>
<van-button class="btn-view" size="small" plain type="primary" @click="onViewCloudRecord"> 查看 </van-button>
</div>
</div>
</div>
<div class="evaluate-card" v-if="Number(taskStatus) === 4 && evaluateData">
<div class="card-title">评价</div>
<!-- 表情选择 -->
<div class="emoji-section">
<div class="emoji-item"
:class="{ active: evaluateForm.satisfactionLevel === 3, disabled: true }">
<img class="emoji-img"
:src="evaluateForm.satisfactionLevel === 3 ? veryGoodImg : veryGoodDisImg" alt="满意" />
<span class="emoji-label">满意</span>
<div class="evaluate-card" v-if="Number(taskStatus) === 4 && evaluateData">
<div class="card-title">评价</div>
<!-- 表情选择 -->
<div class="emoji-section">
<div class="emoji-item" :class="{ active: evaluateForm.satisfactionLevel === 3, disabled: true }">
<img class="emoji-img" :src="evaluateForm.satisfactionLevel === 3 ? veryGoodImg : veryGoodDisImg" alt="满意" />
<span class="emoji-label">满意</span>
</div>
<div class="emoji-item"
:class="{ active: evaluateForm.satisfactionLevel === 2, disabled: true }">
<img class="emoji-img" :src="evaluateForm.satisfactionLevel === 2 ? goodImg : goodDisImg"
alt="一般" />
<span class="emoji-label">一般</span>
<div class="emoji-item" :class="{ active: evaluateForm.satisfactionLevel === 2, disabled: true }">
<img class="emoji-img" :src="evaluateForm.satisfactionLevel === 2 ? goodImg : goodDisImg" alt="一般" />
<span class="emoji-label">一般</span>
</div>
<div class="emoji-item"
:class="{ active: evaluateForm.satisfactionLevel === 1, disabled: true }">
<img class="emoji-img" :src="evaluateForm.satisfactionLevel === 1 ? badImg : badDisImg"
alt="不满意" />
<span class="emoji-label">不满意</span>
<div class="emoji-item" :class="{ active: evaluateForm.satisfactionLevel === 1, disabled: true }">
<img class="emoji-img" :src="evaluateForm.satisfactionLevel === 1 ? badImg : badDisImg" alt="不满意" />
<span class="emoji-label">不满意</span>
</div>
</div>
</div>
<!-- 评价原因 -->
<div class="reason-section" v-if="evaluateForm.satisfactionLevel === 1 || evaluateForm.satisfactionLevel === 2">
<div class="reason-row">
<span class="reason-label"><span class="required-mark">*</span>原因:</span>
<div class="reason-input-wrap">
<van-field
v-model="evaluateForm.commentReason"
type="textarea"
rows="3"
maxlength="300"
show-word-limit
placeholder="请输入"
readonly
class="reason-field"
/>
<!-- 评价原因 -->
<div class="reason-section" v-if="evaluateForm.satisfactionLevel === 1 || evaluateForm.satisfactionLevel === 2">
<div class="reason-row">
<span class="reason-label"><span class="required-mark">*</span>原因:</span>
<div class="reason-input-wrap">
<van-field
v-model="evaluateForm.commentReason"
type="textarea"
rows="3"
maxlength="300"
show-word-limit
placeholder="请输入"
readonly
class="reason-field"
/>
</div>
</div>
</div>
</div>
</div>
</template>
<van-popup v-model:show="showDatePicker" round position="bottom" teleport="body">
<van-date-picker
v-model="pickerDate"
title="选择补正时限"
:min-date="minDate"
@confirm="onDateConfirm"
@cancel="showDatePicker = false"
/>
<van-date-picker v-model="pickerDate" title="选择补正时限" :min-date="minDate" @confirm="onDateConfirm" @cancel="showDatePicker = false" />
</van-popup>
<!-- 视频预览弹窗(审核播放) -->
<van-popup
v-model:show="showVideoPreview"
class="video-preview-popup"
teleport="body"
closeable
@closed="previewVideoUrl = ''"
>
<video
v-if="previewVideoUrl"
:src="previewVideoUrl"
controls
autoplay
playsinline
webkit-playsinline
class="preview-video"
></video>
<van-popup v-model:show="showVideoPreview" class="video-preview-popup" teleport="body" closeable @closed="previewVideoUrl = ''">
<video v-if="previewVideoUrl" :src="previewVideoUrl" controls autoplay playsinline webkit-playsinline class="preview-video"></video>
</van-popup>
</div>
</template>
<script setup name="NosDetailMaterialsTab">
import { showImagePreview, showToast } from 'vant';
import { useRoute } from 'vue-router';
import noDataImg from '@/assets/images/nos/no-data.png';
import { toFullFileUrl } from '@/util/fileUrl';
import veryGoodImg from '@/assets/images/eval-verygood.png';
import veryGoodDisImg from '@/assets/images/eval-verygood-dis.png';
import goodImg from '@/assets/images/eval-good.png';
import goodDisImg from '@/assets/images/eval-good-dis.png';
import badImg from '@/assets/images/eval-bad.png';
import badDisImg from '@/assets/images/eval-bad-dis.png';
import { showImagePreview, showToast } from 'vant'
import { useRoute } from 'vue-router'
import noDataImg from '@/assets/images/nos/no-data.png'
import { toFullFileUrl } from '@/util/fileUrl'
import veryGoodImg from '@/assets/images/eval-verygood.png'
import veryGoodDisImg from '@/assets/images/eval-verygood-dis.png'
import goodImg from '@/assets/images/eval-good.png'
import goodDisImg from '@/assets/images/eval-good-dis.png'
import badImg from '@/assets/images/eval-bad.png'
import badDisImg from '@/assets/images/eval-bad-dis.png'
const props = defineProps({
taskStatus: {
@ -247,110 +180,119 @@ const props = defineProps({
},
taskDeadline: {
type: String,
default: '',
default: ''
},
taskTypeId: {
type: [Number, String],
default: ''
},
evaluateData: {
type: Object,
default: null,
default: null
}
});
})
const emit = defineEmits(['view-cloud-record']);
const emit = defineEmits(['view-cloud-record'])
const onViewCloudRecord = () => {
emit('view-cloud-record');
};
emit('view-cloud-record')
}
const overdueCheckTime = computed(() => props.taskDeadline || '');
const overdueCheckTime = computed(() => props.taskDeadline || '')
const TASK_STATUS_TEXT = {
1: '企业处理中',
2: '退回企业补正',
3: '待审核',
4: '已办结',
5: '已办结(超期)',
6: '转为待核实处置案件'
}
const evaluateForm = ref({
satisfactionLevel: 3, // 3: 满意, 2: 一般, 1: 不满意
commentReason: "",
});
watch(() => props.evaluateData, (val) => {
if (val && val.satisfaction !== undefined) {
const satMap = { 0: 3, 1: 2, 2: 1 };
evaluateForm.value.satisfactionLevel = satMap[val.satisfaction] !== undefined ? satMap[val.satisfaction] : 3;
if (val.commentReason) {
evaluateForm.value.commentReason = val.commentReason;
commentReason: ''
})
watch(
() => props.evaluateData,
(val) => {
if (val && val.satisfaction !== undefined) {
const satMap = { 0: 3, 1: 2, 2: 1 }
evaluateForm.value.satisfactionLevel = satMap[val.satisfaction] !== undefined ? satMap[val.satisfaction] : 3
if (val.commentReason) {
evaluateForm.value.commentReason = val.commentReason
}
}
}
}, { immediate: true, deep: true });
},
{ immediate: true, deep: true }
)
const taskItemList = defineModel('taskItemList', { type: Array, default: () => [] });
const digitalVoiceLoggingStatus = defineModel('digitalVoiceLoggingStatus', { type: [Number, String], default: '' });
const taskItemList = defineModel('taskItemList', { type: Array, default: () => [] })
const digitalVoiceLoggingStatus = defineModel('digitalVoiceLoggingStatus', { type: [Number, String], default: '' })
const inspectResult = defineModel('inspectResult', {
type: Object,
default: () => ({
checkResult: '',
deadline: '',
}),
});
deadline: ''
})
})
const showDatePicker = ref(false);
const showVideoPreview = ref(false);
const previewVideoUrl = ref('');
const minDate = new Date();
const showDatePicker = ref(false)
const showVideoPreview = ref(false)
const previewVideoUrl = ref('')
const minDate = new Date()
const resolveMediaUrl = (url) => toFullFileUrl(url);
const resolveMediaUrl = (url) => toFullFileUrl(url)
const pickerDate = ref([
String(minDate.getFullYear()),
String(minDate.getMonth() + 1).padStart(2, '0'),
String(minDate.getDate()).padStart(2, '0'),
]);
const pickerDate = ref([String(minDate.getFullYear()), String(minDate.getMonth() + 1).padStart(2, '0'), String(minDate.getDate()).padStart(2, '0')])
const onAuditChange = (item) => {
if (item.pass !== 0) {
item.reason = '';
item.reason = ''
}
};
}
const onResultTypeChange = () => {
if (inspectResult.value.checkResult !== 2) {
inspectResult.value.deadline = '';
inspectResult.value.deadline = ''
}
};
}
const onDateConfirm = ({ selectedValues }) => {
inspectResult.value.deadline = selectedValues.join('-');
showDatePicker.value = false;
};
inspectResult.value.deadline = selectedValues.join('-')
showDatePicker.value = false
}
const isVideo = (url) => {
if (!url) return false;
return /\.(mp4|webm|ogg|mov)$/i.test(url);
};
if (!url) return false
return /\.(mp4|webm|ogg|mov)$/i.test(url)
}
const isPdf = (url) => {
if (!url) return false;
return /\.pdf$/i.test(url);
};
if (!url) return false
return /\.pdf$/i.test(url)
}
const onPreview = (url, itemValue) => {
if (!url) return;
const fullUrl = resolveMediaUrl(url);
if (!url) return
const fullUrl = resolveMediaUrl(url)
if (isVideo(url)) {
previewVideoUrl.value = fullUrl;
showVideoPreview.value = true;
return;
previewVideoUrl.value = fullUrl
showVideoPreview.value = true
return
}
if (isPdf(url)) {
window.open(fullUrl, '_blank');
return;
window.open(fullUrl, '_blank')
return
}
const images = itemValue
.filter((u) => !isVideo(u) && !isPdf(u))
.map((u) => resolveMediaUrl(u));
const start = images.indexOf(fullUrl);
const images = itemValue.filter((u) => !isVideo(u) && !isPdf(u)).map((u) => resolveMediaUrl(u))
const start = images.indexOf(fullUrl)
showImagePreview({
images,
startPosition: start >= 0 ? start : 0,
});
};
startPosition: start >= 0 ? start : 0
})
}
</script>
<style lang="less" scoped>
@ -550,6 +492,7 @@ const onPreview = (url, itemValue) => {
.video-preview-popup {
width: 100vw;
height: 100vh;
max-width: none !important;
background-color: #000;
display: flex;
align-items: center;

208
zdxt-web-client/zdxt-efcode-nos-enforcement-app/src/views/enforcement/nos/components/NosDetailTaskTab.vue

@ -10,20 +10,23 @@
<div class="detail-row">
<span class="detail-label">附  件</span>
<div class="detail-value">
<div v-for="(proc, index) in detail.proclamations" :key="index" class="detail-value--notice">
<span class="notice-name">{{ proc.fileName }}</span>
<van-button
v-if="proc.filePath"
class="btn-download"
size="small"
plain
type="primary"
icon="down"
@click="onDownloadNotice(proc.filePath)"
>
下载
</van-button>
</div>
<template v-if="detail.proclamations?.length">
<div v-for="(proc, index) in detail.proclamations" :key="index" class="detail-value--notice">
<span class="notice-name">{{ proc.fileName }}</span>
<van-button
v-if="proc.filePath"
class="btn-download"
size="small"
plain
type="primary"
icon="down"
@click="onDownloadNotice(proc.filePath)"
>
下载
</van-button>
</div>
</template>
<span v-else>--</span>
</div>
</div>
<div class="detail-row">
@ -35,11 +38,11 @@
<div class="section">
<div class="section-title">检查对象</div>
<div class="detail-row">
<span class="detail-label">{{ detail.taskTypeId != 5 ? '执法单位' : '巡查单位' }}</span>
<span class="detail-label">{{ detail.taskTypeId != 5 || detail.taskOriginalTypeId != 5 ? '执法单位' : '巡查单位' }}</span>
<span class="detail-value">{{ detail.enforcementUnit }}</span>
</div>
<div class="detail-row">
<span class="detail-label">{{ detail.taskTypeId != 5 ? '执法人员' : '参与人员' }}</span>
<span class="detail-label">{{ detail.taskTypeId != 5 || detail.taskOriginalTypeId != 5 ? '执法人员' : '参与人员' }}</span>
<div class="detail-value">
<p v-for="(person, index) in detail.enforcementPerson" :key="index" class="detail-text">
{{ person }}
@ -49,7 +52,7 @@
</div>
<div class="section">
<div class="section-title">{{ detail.taskTypeId != 5 ? '执法信息' : '巡查信息' }}</div>
<div class="section-title">{{ detail.taskTypeId != 5 || detail.taskOriginalTypeId != 5 ? '执法信息' : '巡查信息' }}</div>
<div class="detail-row">
<span class="detail-label">企业名称</span>
<div class="detail-value">
@ -61,10 +64,10 @@
</div>
<div class="detail-row">
<span class="detail-label">类  别</span>
<span class="detail-value">{{ detail.category }}</span>
<span class="detail-value">{{ detail.taskStatus == 6 ? detail.taskOriginalTypeName : detail.category }}</span>
</div>
<div class="detail-row" v-if="detail.taskTypeId == 1 || detail.taskTypeId == 5">
<span class="detail-label">{{ detail.taskTypeId == 1 ? '执法事项' : '巡查事项' }}</span>
<div class="detail-row" v-if="detail.taskTypeId == 1 || detail.taskTypeId == 5 || detail.taskOriginalTypeId == 1 || detail.taskOriginalTypeId == 5">
<span class="detail-label">{{ detail.taskTypeId == 1 || detail.taskOriginalTypeId == 1 ? '执法事项' : '巡查事项' }}</span>
<span class="detail-value">{{ detail.enforcementMatter }}</span>
</div>
<div v-if="requirementLabel" class="detail-row">
@ -79,7 +82,7 @@
</div>
</div>
<div class="detail-row">
<span class="detail-label">执法依据</span>
<span class="detail-label">{{ detail.taskTypeId != 5 || detail.taskOriginalTypeId != 5 ? '执法依据' : '依  据' }}</span>
<div class="detail-value">
<p v-for="(basis, index) in detail.enforcementBases" :key="index" class="detail-text">
{{ basis }}
@ -95,32 +98,143 @@
<span class="detail-value">{{ detail.deadline }}</span>
</div>
</div>
<!-- 生成报告-->
<div class="section" v-if="detail.taskStatus == 4">
<div class="section-title title-with-btn">
<span>调查信息</span>
<van-button size="small" plain class="btn-copy" @click="copyInvestigationInfo">复制</van-button>
</div>
<div class="investigation-content">
<p>
<template v-if="noticeFileName"><span class="text-nowrap">依据</span>“{{ noticeFileName }}”通知/公告,</template>{{ investigationParagraph1Body }}
</p>
<p>{{ investigationParagraph2 }}</p>
<p>
{{ investigationParagraph3 }}<br />
检查时间: {{ formattedCheckTime }}
</p>
</div>
</div>
</div>
</template>
<script setup name="NosDetailTaskTab">
import { showToast } from 'vant'
const CATEGORY_REQUIREMENT_LABEL_MAP = {
行政检查: '检查要求',
材料备案: '备案文件',
材料验证: '验证文件'
};
材料验证: '验证文件',
巡查: '巡查要求'
}
const props = defineProps({
detail: {
type: Object,
required: true
}
});
})
const emit = defineEmits(['download-notice']);
const emit = defineEmits(['download-notice'])
const requirementLabel = computed(() => {
return CATEGORY_REQUIREMENT_LABEL_MAP[props.detail.category] || '';
});
return CATEGORY_REQUIREMENT_LABEL_MAP[props.detail.category] || ''
})
const onDownloadNotice = (url) => {
emit('download-notice', url);
};
emit('download-notice', url)
}
const enforcementPersons = computed(() => {
return props.detail.enforcementPersonNames?.join('、') || ''
})
const requirementItems = computed(() => {
return props.detail.materialRequirements?.map((item) => item.name).join('、') || ''
})
const formatDateTimeCn = (dateTime) => {
if (!dateTime) return ''
const date = new Date(dateTime)
if (Number.isNaN(date.getTime())) return dateTime
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hour = String(date.getHours()).padStart(2, '0')
const minute = String(date.getMinutes()).padStart(2, '0')
const second = String(date.getSeconds()).padStart(2, '0')
return `${year}年${month}月${day}日${hour}时${minute}分${second}秒`
}
const formattedCreateTime = computed(() => formatDateTimeCn(props.detail.createTime))
const formattedCheckTime = computed(() => formatDateTimeCn(props.detail.checkTime))
const noticeFileName = computed(() => props.detail.proclamations?.[0]?.fileName || '')
const noticeText = computed(() => {
return noticeFileName.value ? `依据“${noticeFileName.value}”通知/公告,` : ''
})
const investigationParagraph1Body = computed(() => {
const { detail } = props
const personType = detail.taskTypeId == 5 ? '巡查人员' : '执法人员'
return `在${formattedCreateTime.value}深圳市执法局,${personType}${enforcementPersons.value},对${detail.enterpriseName || ''}进行了${detail.category || ''}。`
})
const investigationParagraph1 = computed(() => {
return `${noticeText.value}${investigationParagraph1Body.value}`
})
const investigationParagraph2 = computed(() => {
const { detail } = props
let text = ''
if (detail.taskTypeId == 1 || detail.taskTypeId == 5) {
const matterType = detail.taskTypeId == 5 ? '巡查事项' : '执法事项'
text += `${matterType},${detail.enforcementMatter || ''};`
}
text += `${requirementLabel.value}有:${requirementItems.value}。`
return text
})
const investigationParagraph3 = computed(() => {
return `企业已提交材料,经核验,企业通过本次${props.detail.category || ''}。`
})
const getInvestigationText = () => {
return `${investigationParagraph1.value}\n\n${investigationParagraph2.value}\n\n${investigationParagraph3.value}\n检查时间${formattedCheckTime.value}`
}
const copyInvestigationInfo = async () => {
try {
const text = getInvestigationText()
// 兼容处理剪贴板
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(text)
showToast('复制成功')
} else {
const textArea = document.createElement('textarea')
textArea.value = text
textArea.style.position = 'absolute'
textArea.style.left = '-999999px'
document.body.prepend(textArea)
textArea.select()
try {
document.execCommand('copy')
showToast('复制成功')
} catch (error) {
showToast('复制失败,请手动复制')
} finally {
textArea.remove()
}
}
} catch (err) {
showToast('复制失败,请手动复制')
}
}
</script>
<style lang="less" scoped>
@ -165,6 +279,42 @@ const onDownloadNotice = (url) => {
color: #323233;
}
.title-with-btn {
display: flex;
align-items: center;
justify-content: space-between;
}
.btn-copy {
height: 24px;
padding: 0 10px;
font-size: 12px;
border-radius: 4px;
border-color: #dcdee0;
color: #646566;
}
.investigation-content {
padding: 12px;
background: #f8f9fa;
border-radius: 6px;
font-size: 14px;
color: #323233;
line-height: 1.6;
p {
margin: 0 0 12px 0;
&:last-child {
margin-bottom: 0;
}
}
}
.text-nowrap {
white-space: nowrap;
}
.detail-row {
display: flex;
align-items: flex-start;

12
zdxt-web-client/zdxt-efcode-nos-enforcement-app/src/views/enforcement/nos/components/NosListPanel.vue

@ -94,6 +94,8 @@ const state = reactive({
error: false
})
let listRequestId = 0
const resetList = () => {
state.pageNum = 1
state.list = []
@ -103,6 +105,7 @@ const resetList = () => {
}
const fetchList = async () => {
const requestId = listRequestId
try {
const params = {
pageNo: state.pageNum,
@ -112,6 +115,8 @@ const fetchList = async () => {
keywords: keyword.value.trim()
}
const res = await nosListPageApi(params)
if (requestId !== listRequestId) return
const rows = res?.rows || []
const total = res?.total || 0
@ -127,6 +132,7 @@ const fetchList = async () => {
state.pageNum += 1
}
} catch (err) {
if (requestId !== listRequestId) return
console.error("NosListPanel fetch error:", err)
if (state.pageNum === 1 && state.list.length === 0) {
state.total = 1
@ -136,7 +142,9 @@ const fetchList = async () => {
showFailToast("加载失败")
}
} finally {
state.loading = false
if (requestId === listRequestId) {
state.loading = false
}
}
}
@ -146,6 +154,7 @@ const onLoad = () => {
}
const onRefresh = () => {
listRequestId += 1
// 下拉刷新时不清空 list,避免出现两个加载中
state.finished = false
state.pageNum = 1
@ -155,6 +164,7 @@ const onRefresh = () => {
}
const reloadList = () => {
listRequestId += 1
resetList()
state.loading = true
fetchList()

21
zdxt-web-client/zdxt-efcode-nos-enforcement-app/src/views/enforcement/nos/nosAdd.vue

@ -178,11 +178,11 @@ const createDefaultFormData = () => ({
});
/** 默认任务截止时间:次日,格式 YYYY-MM-DD */
const getDefaultEnforcementTime = () => {
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
return `${tomorrow.getFullYear()}-${String(tomorrow.getMonth() + 1).padStart(2, '0')}-${String(tomorrow.getDate()).padStart(2, '0')}`;
};
// const getDefaultEnforcementTime = () => {
// const tomorrow = new Date();
// tomorrow.setDate(tomorrow.getDate() + 1);
// return `${tomorrow.getFullYear()}-${String(tomorrow.getMonth() + 1).padStart(2, '0')}-${String(tomorrow.getDate()).padStart(2, '0')}`;
// };
/** uploader 展示用(幂等,可多次调用) */
const serializeProclamation = (proclamation = []) =>
@ -233,7 +233,8 @@ const applyFormData = (data) => {
/** 重置表单、企业及执法人员等全部页面状态 */
const resetFormState = () => {
applyFormData(createDefaultFormData());
formData.deadline = getDefaultEnforcementTime();
// formData.deadline = getDefaultEnforcementTime();
formData.deadline = '';
enterpriseData.value = createDefaultEnterpriseData();
additionalLawEnforcers.value = [];
categoryDisabled.value = false;
@ -336,9 +337,10 @@ const handlePageEnter = () => {
}
applyTabCategoryFromRoute();
if (!formData.deadline) {
formData.deadline = getDefaultEnforcementTime();
}
// 取消默认时间
// if (!formData.deadline) {
// formData.deadline = getDefaultEnforcementTime();
// }
};
const taskSourceOptions = ref([]);
@ -537,6 +539,7 @@ const handleCancel = () => {
/** 校验附件上传状态后,二次确认并调用新增任务接口 */
const onSubmit = () => {
// 有未上传完的附件则拦截
const uploadingFile = (formData.proclamation || []).find((img) => img && img.status === 'uploading');
if (uploadingFile) {

1
zdxt-web-client/zdxt-efcode-nos-enforcement-app/src/views/enforcement/nos/nosCloudRecord.vue

@ -361,6 +361,7 @@ onMounted(() => {
.video-preview-popup {
width: 100vw;
height: 100vh;
max-width: none !important;
background-color: #000;
display: flex;
align-items: center;

11
zdxt-web-client/zdxt-efcode-nos-enforcement-app/src/views/enforcement/nos/nosDetail.vue

@ -33,6 +33,7 @@
v-model:inspect-result="inspectResult"
:task-status="taskDetail.taskStatus"
:task-deadline="taskDetail.deadline"
:task-type-id="taskDetail.taskTypeId"
:digital-voice-logging-status="taskDetail.digitalVoiceLoggingStatus"
:evaluate-data="evaluateData"
@view-cloud-record="onViewCloudRecord"
@ -78,7 +79,8 @@ const evaluateData = ref(null)
const inspectResult = ref({
checkResult: '',
deadline: ''
deadline: '',
checkTime: ''
})
const applyDetailData = (data) => {
@ -92,6 +94,7 @@ const applyDetailData = (data) => {
taskSource: data.taskSourceName || '',
enforcementUnit: data.lawEnforcementAgency || '',
enforcementPerson: (data.lawEnforcer || []).map((e) => `${e.lawEnforcerName || ''} ${e.lawEnforcerPhone || ''}`.trim()).filter(Boolean),
enforcementPersonNames: (data.lawEnforcer || []).map((e) => e.lawEnforcerName || '').filter(Boolean),
enterpriseName: data.enterpriseName || '',
enterpriseNumber: data.usciCode || '',
category: data.taskTypeName || '',
@ -101,7 +104,11 @@ const applyDetailData = (data) => {
startTime: data.createTime || '',
deadline: data.deadline || '',
taskTypeId: data.taskTypeId || '',
digitalVoiceLoggingStatus: data.digitalVoiceLoggingStatus || ''
digitalVoiceLoggingStatus: data.digitalVoiceLoggingStatus || '',
checkTime: data.checkTime || '',
createTime: data.createTime || '',
taskOriginalTypeName: data.taskOriginalTypeName || '',
taskOriginalTypeId: data.taskOriginalTypeId || '',
}
taskItemList.value = (data.taskItemList || []).map((item) => ({
...item,

282
zdxt-web-client/zdxt-efcode-nos-enforcement-app/src/views/enforcement/nos/nosEvaluateList.vue

@ -1,162 +1,164 @@
<template>
<div class="evaluate-list-page">
<!-- 顶部导航栏 -->
<van-nav-bar title="执法评价" left-text="返回" left-arrow @click-left="onClickLeft">
</van-nav-bar>
<!-- 列表区域 -->
<div class="list-container">
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
<van-list v-model:loading="loading" :finished="finished" finished-text="已全部加载" @load="onLoad"
:offset="50">
<van-empty v-if="finished && list.length === 0" description="暂无执法评价" />
<div class="evaluate-card" v-for="item in list" :key="item.id" @click="toDetail(item)">
<!-- 执法时间 -->
<div class="card-time">
办结时间:{{ item.endTime }}
</div>
<!-- 标题 -->
<div class="card-title">{{ item.title || '执法检查' }}</div>
<!-- 内容 -->
<div class="card-content">
<div class="info-row">
<span class="label">执法单位</span>
<span class="value">{{ item.lawEnforcementAgency || '--' }}</span>
</div>
<div class="info-row">
<span class="label">执法人员</span>
<div>
<div v-for="(enforcer, index) in item.lawEnforcer" :key="index"
class="enforcer-item">
{{ enforcer.lawEnforcerName }} {{ enforcer.lawEnforcerIdCard || enforcer.lawEnforcerPhone }}
</div>
</div>
</div>
<div class="info-row">
<span class="label">企业名称</span>
<span class="value">{{ item.enterpriseName }}</span>
</div>
</div>
<!-- 底部状态 -->
<div class="card-footer">
<span class="status">状态:<span :class="getStatusClass(item.commentStatus)">{{
getStatusText(item.commentStatus) }}</span></span>
<span class="evaluate-time">评价时间:{{ item.commentTime }}</span>
</div>
<!-- 顶部导航栏 -->
<van-nav-bar title="执法评价" left-text="返回" left-arrow @click-left="onClickLeft"> </van-nav-bar>
<!-- 列表区域 -->
<div class="list-container">
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
<van-list v-model:loading="loading" :finished="finished" finished-text="已全部加载" @load="onLoad" :offset="50">
<van-empty v-if="finished && list.length === 0" description="暂无执法评价" />
<div class="evaluate-card" v-for="item in list" :key="item.id" @click="toDetail(item)">
<!-- 执法时间 -->
<div class="card-time">办结时间:{{ item.endTime }}</div>
<!-- 标题 -->
<div class="card-title">{{ item.title || '执法检查' }}</div>
<!-- 内容 -->
<div class="card-content">
<div class="info-row">
<span class="label">执法单位</span>
<span class="value">{{ item.lawEnforcementAgency || '--' }}</span>
</div>
<div class="info-row">
<span class="label">执法人员</span>
<div>
<div v-for="(enforcer, index) in item.lawEnforcer" :key="index" class="enforcer-item">
{{ enforcer.lawEnforcerName }} {{ enforcer.lawEnforcerIdCard || enforcer.lawEnforcerPhone }}
</div>
</van-list>
</van-pull-refresh>
</div>
</div>
</div>
<div class="info-row">
<span class="label">企业名称</span>
<span class="value">{{ item.enterpriseName }}</span>
</div>
</div>
<!-- 底部状态 -->
<div class="card-footer">
<span class="status"
>状态:<span :class="getStatusClass(item.commentStatus)">{{ getStatusText(item.commentStatus) }}</span></span
>
<span class="evaluate-time">评价时间:{{ item.commentTime }}</span>
</div>
</div>
</van-list>
</van-pull-refresh>
</div>
</div>
</template>
<script setup name="enforcementEvaluateList">
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { showToast } from 'vant';
import 'vant/es/toast/style';
import { nosTaskHasCommentPageApi } from '@/api/nos/enfDemo';
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { showToast } from 'vant'
import 'vant/es/toast/style'
import { nosTaskHasCommentPageApi } from '@/api/nos/enfDemo'
const router = useRouter();
const router = useRouter()
// ========== 列表状态 ==========
const list = ref([]);
const loading = ref(false);
const finished = ref(false);
const refreshing = ref(false);
const pageNum = ref(1);
const pageSize = ref(10);
const list = ref([])
const loading = ref(false)
const finished = ref(false)
const refreshing = ref(false)
const pageNum = ref(1)
const pageSize = ref(10)
// ========== 获取状态文本 ==========
const getStatusText = (status) => {
const statusMap = {
'0': '未评价',
'1': '已评价',
'2': '已退回',
};
return statusMap[status] || '未知';
};
'0': '未评价',
'1': '已评价',
'2': '已退回'
}
return statusMap[status] || '未知'
}
// ========== 获取状态样式类 ==========
const getStatusClass = (status) => {
const classMap = {
'0': 'status-pending',
'1': 'status-evaluated',
'2': 'status-rejected',
};
return classMap[status] || 'status-default';
};
'0': 'status-pending',
'1': 'status-evaluated',
'2': 'status-rejected'
}
return classMap[status] || 'status-default'
}
// ========== 加载数据 ==========
const fetchList = () => {
const params = {
pageNum: pageNum.value,
pageSize: pageSize.value,
};
nosTaskHasCommentPageApi(params, (res) => {
pageNum: pageNum.value,
pageSize: pageSize.value
}
nosTaskHasCommentPageApi(
params,
(res) => {
if (res.code === 200) {
const rows = res.rows || [];
const total = res.total ?? 0;
if (refreshing.value) {
list.value = Array.isArray(rows) ? rows : [];
refreshing.value = false;
} else {
list.value = [...list.value, ...(Array.isArray(rows) ? rows : [])];
}
if (!Array.isArray(rows) || rows.length === 0 || list.value.length >= total) {
finished.value = true;
} else {
pageNum.value++;
}
loading.value = false;
const rows = res.rows || []
const total = res.total ?? 0
if (refreshing.value) {
list.value = Array.isArray(rows) ? rows : []
refreshing.value = false
} else {
list.value = [...list.value, ...(Array.isArray(rows) ? rows : [])]
}
if (!Array.isArray(rows) || rows.length === 0 || list.value.length >= total) {
finished.value = true
} else {
pageNum.value++
}
loading.value = false
} else {
finished.value = true;
loading.value = false;
refreshing.value = false;
showToast(res.msg || '加载失败');
finished.value = true
loading.value = false
refreshing.value = false
showToast(res.msg || '加载失败')
}
}, () => {
finished.value = true;
loading.value = false;
refreshing.value = false;
showToast('网络请求失败');
});
};
},
() => {
finished.value = true
loading.value = false
refreshing.value = false
showToast('网络请求失败')
}
)
}
// ========== 触底加载 ==========
const onLoad = () => {
if (refreshing.value) return;
fetchList();
};
if (refreshing.value) return
fetchList()
}
// ========== 下拉刷新 ==========
const onRefresh = () => {
pageNum.value = 1;
list.value = [];
finished.value = false;
loading.value = true;
fetchList();
};
pageNum.value = 1
list.value = []
finished.value = false
loading.value = true
fetchList()
}
// ========== 跳转详情页面 ==========
const toDetail = (item) => {
router.push({
path: '/enforcement/nos/detail',
query: { id: item.id, tab: item.taskStatus }
});
};
path: '/enforcement/nos/detail',
query: { id: item.id, tab: item.taskStatus }
})
}
// ========== 返回上一页 ==========
const onClickLeft = () => {
router.back();
};
// 跳转至执法检查列表页面
router.push({
path: '/enforcement/nos'
})
}
</script>
<style lang="less" scoped>
@ -229,18 +231,18 @@ const onClickLeft = () => {
font-size: 14px;
.label {
color: #999;
min-width: 80px;
color: #999;
min-width: 80px;
}
.enforcer-item {
padding: 0px 0px 6px 0px;
padding: 0px 0px 6px 0px;
}
.value {
flex: 1;
color: #333;
word-break: break-all;
flex: 1;
color: #333;
word-break: break-all;
}
}
@ -255,25 +257,25 @@ const onClickLeft = () => {
color: #666;
.status {
.status-pending {
color: #faad14;
}
.status-pending {
color: #faad14;
}
.status-evaluated {
color: #52c41a;
}
.status-evaluated {
color: #52c41a;
}
.status-rejected {
color: #f5222d;
}
.status-rejected {
color: #f5222d;
}
.status-default {
color: #999;
}
.status-default {
color: #999;
}
}
.evaluate-time {
color: #999;
color: #999;
}
}

21
zdxt-web-client/zdxt-efcode-nos-enforcement-app/src/views/enforcement/nos/nosList.vue

@ -14,7 +14,7 @@
<van-tab v-for="tab in TAB_LIST" :key="tab.key" :title="tab.title" :name="tab.key" />
</van-tabs>
<NosListPanel :tab-key="activeTab" @add="onAdd" @item-click="onItemClick" />
<NosListPanel ref="listPanelRef" :tab-key="activeTab" @add="onAdd" @item-click="onItemClick" />
</div>
<FooterBar active="offsite" />
@ -49,7 +49,7 @@
</template>
<script setup name="nosList">
import { onMounted } from 'vue';
import { onMounted, onActivated } from 'vue';
import FooterBar from '@/components/FooterBar.vue';
import NosListPanel from './components/NosListPanel.vue';
import { isFilingApi } from '@/api/nos/enfDemo';
@ -71,6 +71,14 @@ const TAB_LIST = isLaw ? [
];
const activeTab = ref(1);
const listPanelRef = ref(null);
/** keep-alive 首次激活由 van-list 自行加载,跳过后续返回时再刷新 */
let isFirstActivated = true;
const refreshCurrentTab = () => {
listPanelRef.value?.reloadList();
};
const onBack = () => router.back();
@ -127,6 +135,15 @@ const checkFilingStatus = async () => {
onMounted(() => {
checkFilingStatus();
});
/** 从详情/新增等子页返回时,保持 tab 不变并刷新当前列表 */
onActivated(() => {
if (isFirstActivated) {
isFirstActivated = false;
return;
}
refreshCurrentTab();
});
</script>
<style lang="less" scoped>

2
zdxt-web-client/zdxt-efcode-nos-enforcement-app/vite-env/.env.enforcement-dev

@ -9,7 +9,7 @@ VITE_NOS_BASE_URL = '/nosapi'
# 仅 dev server 代理目标(生产由 nginx 转发,不读这两项)
VITE_PROXY_TARGET = 'http://139.186.148.21:8080'
VITE_NOS_PROXY_TARGET = 'http://192.168.1.11:28080'
VITE_NOS_PROXY_TARGET = 'http://192.168.1.9:28080'
# 附件/图片等静态资源前缀(与现场执法同源)
VITE_FILE_URL = '/api'

2
zdxt-web-client/zdxt-efcode-nos-enterprise-app/src/api/enterprise/index.js

@ -4,7 +4,7 @@ import { _get, _post } from "../../http/index";
let enterpriseAppBaseUrl = "/app-ent";
// 企业端用户登录
export const enterpriseLogin = (params, success, error) => _post(enterpriseAppBaseUrl + "/enterprise/login/siqLogin", params, success, error);
export const enterpriseLogin = (params, success, error) => _post(enterpriseAppBaseUrl + "/enterprise/login/siqLoginMock", params, success, error);
// 获取用户关联的企业信息(登录后扩展使用,已在 login.vue 注释保留)
export const getUserRefEnterpriseInfo = (params, success, error) => _get(enterpriseAppBaseUrl + "/enterpriseInfo/getUserRefEnterpriseInfo", params, success, error);

4
zdxt-web-client/zdxt-efcode-nos-enterprise-app/src/api/nos/enfDemo.js

@ -15,9 +15,7 @@ export const nosTaskCommentAPi = (params, success, error) =>
// 根据评价状态查询当前企业任务
export const nosTaskListByCommentStatusAPi = (params, success, error) =>
_get("/app-ent/nos-law-enforcement-task/query/by-comment-status", params, success, error);
// 分页查询非现场执法任务未读消息
export const nosTaskListUnReadAPi = (params, success, error) =>
_post("/app-ent/nos-law-enforcement-task/msg/un-read/page", params, success, error);
// ----------------------------- 非现场执法任务通知 -----------------------------
// 分页查询非现场执法未读任务通知
export const nosTaskListNoticeAPi = (params, success, error) =>

1
zdxt-web-client/zdxt-efcode-nos-enterprise-app/src/http/http.js

@ -45,6 +45,7 @@ function setupInterceptors(http) {
// if(show){
// return response;
// }
router.push({ path: "/enterprise/login" });
if (response.data.message) {
// Message.error(response.data.message);
}

2
zdxt-web-client/zdxt-efcode-nos-enterprise-app/src/router/nos/nos-base-router.js

@ -16,7 +16,7 @@ const routes = [
name: "nosList",
component: () => import("@/views/enterprise/nos/nosList.vue"),
meta: {
isKeepAlive: false,
isKeepAlive: true,
},
},
// 非现场详情

69
zdxt-web-client/zdxt-efcode-nos-enterprise-app/src/views/enterprise/nos/components/NosDetailMaterialsTab.vue

@ -1,11 +1,23 @@
<template>
<div class="detail-materials-tab">
<template v-if="taskItemList.length === 0">
<!-- <template v-if="taskItemList.length === 0">
<div class="empty-state">
<van-image :src="noDataImg" width="151" height="120" />
<div class="empty-text-main">暂无资料上传</div>
<div class="empty-text-sub">暂无材料要求</div>
</div>
</template> -->
<template v-if="Number(taskStatus) === 5 && !taskItemList.some(item => (item.itemValue || []).length > 0)">
<div class="empty-state">
<van-image :src="noDataImg" width="151" height="120" />
<div class="empty-text-main">暂无资料上传</div>
<div class="empty-text-sub"></div>
</div>
<div class="result-card">
<div class="result-title">检查结果</div>
<div class="readonly-result-text">检查结果:已办结(<span class="overdue-text">超期</span>)</div>
<div class="readonly-result-text" >检查时间:{{ inspectResult.checkTime }}</div>
</div>
</template>
<template v-else>
<div v-for="(item, index) in taskItemList" :key="index" class="material-card">
@ -13,7 +25,7 @@
<van-icon v-if="Number(item.pass) === 0" name="info-o" class="required-mark" />
<span v-else class="required-mark">*</span>
{{ item.itemName }}
<span class="material-desc">(小于20M/个,最多30个文件)</span>
<span class="material-desc">(小于200M/个,最多30个文件)</span>
</div>
<div class="material-media" v-if="!isEdit">
@ -36,10 +48,11 @@
<van-uploader
v-model="item.fileList"
multiple
:max-count="30"
:max-size="20 * 1024 * 1024"
:max-count="20"
:max-size="200 * 1024 * 1024"
accept="image/*,video/*,.pdf"
:after-read="onAfterReadImage"
:before-read="onBeforeRead"
@oversize="onOversize"
>
<template #default>
@ -53,7 +66,7 @@
<div class="objection-card" v-if="Number(item.pass) === 0">
<div class="objection-row">
<span class="objection-label">审核:</span>
<span class="objection-value">有异议</span>
<span class="objection-value">不通过</span>
</div>
<div class="objection-row" v-if="item.reason">
<span class="objection-label">原因:</span>
@ -66,16 +79,10 @@
<div class="result-title">检查结果</div>
<div class="readonly-result-text" style="padding-top: 4px">
检查结果:{{
inspectResult.checkResult === 0
? '通过'
: inspectResult.checkResult === 1
? '转为待核实处置案件'
: inspectResult.checkResult === 2
? '退回企业补正材料'
: '待审核'
TASK_STATUS_TEXT[Number(taskStatus)]
}}
</div>
<div class="readonly-result-text" v-if="inspectResult.checkTime">检查时间:{{ inspectResult.checkTime }}</div>
<div class="readonly-result-text">检查时间:{{ inspectResult.checkTime }}</div>
<div class="readonly-result-text" v-if="inspectResult.checkResult === 2 && inspectResult.deadline">
补正时限:{{ inspectResult.deadline }}
</div>
@ -138,7 +145,7 @@
type="textarea"
rows="3"
maxlength="300"
show-word-limit
:show-word-limit="!readonly"
placeholder="请输入"
:readonly="readonly"
class="reason-field"
@ -158,7 +165,7 @@
</van-popup>
<!-- 视频预览弹窗 -->
<van-popup v-model:show="showVideoPreview" class="video-preview-popup" teleport="body" closeable @closed="previewVideoUrl = ''">
<van-popup v-model:show="showVideoPreview" class="video-preview-popup" teleport="body" closeable @closed="previewVideoUrl = ''">
<video v-if="previewVideoUrl" :src="previewVideoUrl" controls autoplay playsinline webkit-playsinline class="preview-video"></video>
</van-popup>
</div>
@ -171,7 +178,7 @@ import noDataImg from '@/assets/images/nos/no-data.png'
import { uploadFile } from '@/util/fileUpload'
import { toFullFileUrl } from '@/util/fileUrl'
import { nosTaskCommentAPi } from '@/api/nos/enfDemo'
import 'vant/es/toast/style';
import veryGoodImg from '@/assets/images/eval-verygood.png'
import veryGoodDisImg from '@/assets/images/eval-verygood-dis.png'
import goodImg from '@/assets/images/eval-good.png'
@ -291,7 +298,7 @@ const previewVideoUrl = ref('')
const pickerDate = ref([String(minDate.getFullYear()), String(minDate.getMonth() + 1).padStart(2, '0'), String(minDate.getDate()).padStart(2, '0')])
const onOversize = () => {
showToast('文件大小不能超过 20MB')
showToast('文件大小不能超过 200MB')
}
const isVideo = (url) => {
@ -304,6 +311,31 @@ const isPdf = (url) => {
return /\.pdf$/i.test(url)
}
const IMAGE_EXT_RE = /\.(png|jpe?g|gif|webp|bmp)$/i
const VIDEO_EXT_RE = /\.(mp4|webm|ogg|mov)$/i
const PDF_EXT_RE = /\.pdf$/i
const isAllowedUploadFile = (file) => {
const name = (file.name || '').toLowerCase()
const type = (file.type || '').toLowerCase()
if (type.startsWith('image/') || IMAGE_EXT_RE.test(name)) return true
if (type.startsWith('video/') || VIDEO_EXT_RE.test(name)) return true
if (type === 'application/pdf' || PDF_EXT_RE.test(name)) return true
return false
}
// 附件上传前校验,限制图片、视频、PDF文件
const onBeforeRead = (file) => {
const files = Array.isArray(file) ? file : [file]
for (const item of files) {
if (!isAllowedUploadFile(item)) {
showToast('仅支持上传图片、视频、PDF 格式文件')
return false
}
}
return true
}
const onAfterReadImage = (file) => {
const files = Array.isArray(file) ? file : [file]
files.forEach((f) => {
@ -609,6 +641,8 @@ const onPreview = (url, itemValue) => {
.video-preview-popup {
width: 100vw;
height: 100vh;
max-width: none !important;
margin: 0 !important;
background-color: #000;
display: flex;
align-items: center;
@ -617,6 +651,7 @@ const onPreview = (url, itemValue) => {
.preview-video {
width: 100%;
max-height: 100%;
object-fit: cover;
outline: none;
}

176
zdxt-web-client/zdxt-efcode-nos-enterprise-app/src/views/enterprise/nos/components/NosDetailTaskTab.vue

@ -10,20 +10,23 @@
<div class="detail-row">
<span class="detail-label">附  件</span>
<div class="detail-value">
<div v-for="(proc, index) in detail.proclamations" :key="index" class="detail-value--notice">
<span class="notice-name">{{ proc.fileName }}</span>
<van-button
v-if="proc.filePath"
class="btn-download"
size="small"
plain
type="primary"
icon="down"
@click="onDownloadNotice(proc.filePath)"
>
下载
</van-button>
</div>
<template v-if="detail.proclamations?.length">
<div v-for="(proc, index) in detail.proclamations" :key="index" class="detail-value--notice">
<span class="notice-name">{{ proc.fileName }}</span>
<van-button
v-if="proc.filePath"
class="btn-download"
size="small"
plain
type="primary"
icon="down"
@click="onDownloadNotice(proc.filePath)"
>
下载
</van-button>
</div>
</template>
<span v-else>--</span>
</div>
</div>
<div class="detail-row">
@ -77,7 +80,7 @@
</div>
</div>
<div class="detail-row">
<span class="detail-label">执法依据</span>
<span class="detail-label">{{detail.taskTypeId != 5 ? '执法依据' : '依  据'}}</span>
<div class="detail-value">
<p v-for="(basis, index) in detail.enforcementBases" :key="index" class="detail-text">
{{ basis }}
@ -93,10 +96,28 @@
<span class="detail-value">{{ detail.deadline }}</span>
</div>
</div>
<div class="section" v-if="detail.taskStatus == 4">
<div class="section-title title-with-btn">
<span>调查信息</span>
<van-button size="small" plain class="btn-copy" @click="copyInvestigationInfo">复制</van-button>
</div>
<div class="investigation-content">
<p>
<template v-if="noticeFileName"><span class="text-nowrap">依据</span>“{{ noticeFileName }}”通知/公告,</template>{{ investigationParagraph1Body }}
</p>
<p>{{ investigationParagraph2 }}</p>
<p>
{{ investigationParagraph3 }}<br />
检查时间: {{ formattedCheckTime }}
</p>
</div>
</div>
</div>
</template>
<script setup name="NosDetailTaskTab">
import { showToast } from 'vant';
const CATEGORY_REQUIREMENT_LABEL_MAP = {
行政检查: '检查要求',
材料备案: '备案文件',
@ -117,9 +138,99 @@ const requirementLabel = computed(() => {
return CATEGORY_REQUIREMENT_LABEL_MAP[props.detail.category] || '';
});
const enforcementPersons = computed(() => {
return props.detail.enforcementPersonNames?.join('、') || ''
})
const requirementItems = computed(() => {
return props.detail.materialRequirements?.map((item) => item.name).join('、') || ''
})
const formatDateTimeCn = (dateTime) => {
if (!dateTime) return ''
const date = new Date(dateTime)
if (Number.isNaN(date.getTime())) return dateTime
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hour = String(date.getHours()).padStart(2, '0')
const minute = String(date.getMinutes()).padStart(2, '0')
const second = String(date.getSeconds()).padStart(2, '0')
return `${year}年${month}月${day}日${hour}时${minute}分${second}秒`
}
const formattedCreateTime = computed(() => formatDateTimeCn(props.detail.createTime))
const formattedCheckTime = computed(() => formatDateTimeCn(props.detail.checkTime))
const onDownloadNotice = (url) => {
emit('download-notice', url);
};
const noticeFileName = computed(() => props.detail.proclamations?.[0]?.fileName || '')
const noticeText = computed(() => {
return noticeFileName.value ? `依据“${noticeFileName.value}”通知/公告,` : ''
})
const investigationParagraph1Body = computed(() => {
const { detail } = props
const personType = detail.taskTypeId == 5 ? '巡查人员' : '执法人员'
return `在${formattedCreateTime.value}深圳市执法局,${personType}${enforcementPersons.value},对${detail.enterpriseName || ''}进行了${detail.category || ''}。`
})
const investigationParagraph1 = computed(() => {
return `${noticeText.value}${investigationParagraph1Body.value}`
})
const investigationParagraph2 = computed(() => {
const { detail } = props
let text = ''
if (detail.taskTypeId == 1 || detail.taskTypeId == 5) {
const matterType = detail.taskTypeId == 5 ? '巡查事项' : '执法事项'
text += `${matterType},${detail.enforcementMatter || ''};`
}
text += `${requirementLabel.value}有:${requirementItems.value}。`
return text
})
const investigationParagraph3 = computed(() => {
return `企业已提交材料,经核验,企业通过本次${props.detail.category || ''}。`
})
const getInvestigationText = () => {
return `${investigationParagraph1.value}\n\n${investigationParagraph2.value}\n\n${investigationParagraph3.value}\n检查时间${formattedCheckTime.value}`
}
const copyInvestigationInfo = async () => {
try {
const text = getInvestigationText()
// 兼容处理剪贴板
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(text)
showToast('复制成功')
} else {
const textArea = document.createElement('textarea')
textArea.value = text
textArea.style.position = 'absolute'
textArea.style.left = '-999999px'
document.body.prepend(textArea)
textArea.select()
try {
document.execCommand('copy')
showToast('复制成功')
} catch (error) {
showToast('复制失败,请手动复制')
} finally {
textArea.remove()
}
}
} catch (err) {
showToast('复制失败,请手动复制')
}
}
</script>
<style lang="less" scoped>
@ -251,4 +362,39 @@ const onDownloadNotice = (url) => {
font-size: 16px;
color: #2b7de9;
}
.title-with-btn {
display: flex;
align-items: center;
justify-content: space-between;
}
.btn-copy {
height: 24px;
padding: 0 10px;
font-size: 12px;
border-radius: 4px;
border-color: #dcdee0;
color: #646566;
}
.investigation-content {
padding: 12px;
background: #f8f9fa;
border-radius: 6px;
font-size: 14px;
color: #323233;
line-height: 1.6;
p {
margin: 0 0 12px 0;
&:last-child {
margin-bottom: 0;
}
}
}
.text-nowrap {
white-space: nowrap;
}
</style>

12
zdxt-web-client/zdxt-efcode-nos-enterprise-app/src/views/enterprise/nos/components/NosListPanel.vue

@ -83,6 +83,8 @@ const state = reactive({
error: false
})
let listRequestId = 0
const resetList = () => {
state.pageNum = 1
state.list = []
@ -92,6 +94,7 @@ const resetList = () => {
}
const fetchList = async () => {
const requestId = listRequestId
try {
const params = {
pageNo: state.pageNum,
@ -101,6 +104,8 @@ const fetchList = async () => {
keywords: keyword.value.trim()
}
const res = await nosTaskPageApi(params)
if (requestId !== listRequestId) return
const rows = res?.rows || []
const total = res?.total || 0
@ -116,6 +121,7 @@ const fetchList = async () => {
state.pageNum += 1
}
} catch (err) {
if (requestId !== listRequestId) return
console.error("NosListPanel fetch error:", err)
if (state.pageNum === 1 && state.list.length === 0) {
state.total = 1
@ -125,7 +131,9 @@ const fetchList = async () => {
showFailToast("加载失败")
}
} finally {
state.loading = false
if (requestId === listRequestId) {
state.loading = false
}
}
}
@ -135,6 +143,7 @@ const onLoad = () => {
}
const onRefresh = () => {
listRequestId += 1
// 下拉刷新时不清空 list,避免出现两个加载中
state.finished = false
state.pageNum = 1
@ -144,6 +153,7 @@ const onRefresh = () => {
}
const reloadList = () => {
listRequestId += 1
resetList()
state.loading = true
fetchList()

2
zdxt-web-client/zdxt-efcode-nos-enterprise-app/src/views/enterprise/nos/message/nosEvaluationList.vue

@ -193,7 +193,7 @@ const fetchPendingCount = async () => {
const res = await nosTaskListByCommentStatusAPi({
commentStatus: 0,
pageNo: 1,
pageSize: 1,
pageSize: 10,
});
pendingTotal.value = res?.total ?? 0;
} catch {

2
zdxt-web-client/zdxt-efcode-nos-enterprise-app/src/views/enterprise/nos/nosCloudRecord.vue

@ -551,6 +551,7 @@ onMounted(() => {
.video-preview-popup {
width: 100vw;
height: 100vh;
max-width: none !important;
background-color: #000;
display: flex;
align-items: center;
@ -559,6 +560,7 @@ onMounted(() => {
.preview-video {
width: 100%;
max-height: 100%;
object-fit: cover;
outline: none;
}

21
zdxt-web-client/zdxt-efcode-nos-enterprise-app/src/views/enterprise/nos/nosDetail.vue

@ -41,7 +41,10 @@
</van-tab>
</van-tabs>
<div v-if="activeTab === 'materials' && (Number(taskDetail.taskStatus) === 1 || (Number(taskDetail.taskStatus) === 2 && isEdit))" class="detail-footer">
<div
v-if="activeTab === 'materials' && (Number(taskDetail.taskStatus) === 1 || (Number(taskDetail.taskStatus) === 2 && isEdit))"
class="detail-footer"
>
<van-button class="btn-cancel" @click="onCancel">取消</van-button>
<van-button class="btn-submit" type="primary" native-type="submit">提交</van-button>
</div>
@ -49,7 +52,10 @@
<van-button class="btn-submit" type="primary" @click="isEdit = true">修改</van-button>
</div>
<!-- 云笔录 -->
<div v-if="activeTab === 'materials' && Number(taskDetail.taskStatus) === 3 && Number(taskDetail.digitalVoiceLoggingStatus) === 1" class="detail-footer">
<div
v-if="activeTab === 'materials' && Number(taskDetail.taskStatus) === 3 && Number(taskDetail.digitalVoiceLoggingStatus) === 1"
class="detail-footer"
>
<van-button class="btn-cancel" @click="onCloudRecord">云笔录</van-button>
</div>
</van-form>
@ -94,6 +100,7 @@ const applyDetailData = (data) => {
taskSource: data.taskSourceName || '',
enforcementUnit: data.lawEnforcementAgency || '',
enforcementPerson: (data.lawEnforcer || []).map(e => `${e.lawEnforcerName || ''} ${e.lawEnforcerPhone || ''}`.trim()).filter(Boolean),
enforcementPersonNames: (data.lawEnforcer || []).map(e => e.lawEnforcerName || '').filter(Boolean),
enterpriseName: data.enterpriseName || '',
enterpriseNumber: data.usciCode || '',
category: data.taskTypeName || '',
@ -104,6 +111,7 @@ const applyDetailData = (data) => {
deadline: data.deadline || '',
taskTypeId: data.taskTypeId || '',
digitalVoiceLoggingStatus: data.digitalVoiceLoggingStatus || '',
checkTime: data.checkTime || '',
};
taskItemList.value = (data.taskItemList || []).map((item) => {
const { fileList, ...rest } = item
@ -115,12 +123,8 @@ const applyDetailData = (data) => {
})
// 回显检查结果及检查时间
if (data.checkResult !== undefined && data.checkResult !== null) {
inspectResult.value.checkResult = data.checkResult;
}
if (data.checkTime) {
inspectResult.value.checkTime = data.checkTime;
}
inspectResult.value.checkResult = data.checkResult || data.taskStatusName;
inspectResult.value.checkTime = data.checkTime || data.deadline;
// 评价回显
if (data.satisfaction !== undefined && data.satisfaction !== null) {
@ -299,7 +303,6 @@ const onSubmit = async () => {
:deep(.van-tab__panel) {
height: 100%;
}
}
.detail-scroll {

20
zdxt-web-client/zdxt-efcode-nos-enterprise-app/src/views/enterprise/nos/nosList.vue

@ -19,7 +19,7 @@
<van-tab v-for="tab in TAB_LIST" :key="tab.key" :title="tab.title" :name="tab.key" />
</van-tabs>
<NosListPanel :tab-key="activeTab" @item-click="onItemClick" />
<NosListPanel ref="listPanelRef" :tab-key="activeTab" @item-click="onItemClick" />
</div>
<FooterBar active="offsite" />
@ -27,6 +27,7 @@
</template>
<script setup name="nosList">
import { onActivated } from 'vue';
import FooterBar from '@/components/FooterBar.vue';
import NosListPanel from './components/NosListPanel.vue';
@ -40,6 +41,14 @@ const TAB_LIST = [
];
const activeTab = ref(1);
const listPanelRef = ref(null);
/** keep-alive 首次激活由 van-list 自行加载,跳过后续返回时再刷新 */
let isFirstActivated = true;
const refreshCurrentTab = () => {
listPanelRef.value?.reloadList();
};
const onBack = () => router.back();
@ -54,6 +63,15 @@ const onItemClick = (item) => {
query: { id: item.id, tab: activeTab.value, code: item.code },
});
};
/** 从详情等子页返回时,保持 tab 不变并刷新当前列表 */
onActivated(() => {
if (isFirstActivated) {
isFirstActivated = false;
return;
}
refreshCurrentTab();
});
</script>
<style lang="less" scoped>

2
zdxt-web-client/zdxt-efcode-nos-enterprise-app/vite-env/.env.enterprise-dev

@ -10,4 +10,4 @@ VITE_APP_ENV = 'dev'
VITE_NOS_BASE_URL = '/nosapi'
# 仅 dev server 代理目标(生产由 nginx 转发,不读这两项)
VITE_PROXY_TARGET = 'http://139.186.148.21:8080'
VITE_NOS_PROXY_TARGET = 'http://192.168.1.11:28080'
VITE_NOS_PROXY_TARGET = 'http://192.168.1.9:28080'

Loading…
Cancel
Save