Browse Source

合并非现场

dev-v1.0.2
庄锐波 5 months ago
parent
commit
9716de9278
  1. 11
      zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/controller/app/LoginController.java
  2. 11
      zdxt-web-client/zdxt-efcode-enforcement-app/src/api/nos/common.js
  3. 35
      zdxt-web-client/zdxt-efcode-enforcement-app/src/api/nos/index.js
  4. 2
      zdxt-web-client/zdxt-efcode-enforcement-app/src/common/globalExpandRouter.js
  5. 189
      zdxt-web-client/zdxt-efcode-enforcement-app/src/components/enforcement/nos/AttachmentTaskSource.vue
  6. 539
      zdxt-web-client/zdxt-efcode-enforcement-app/src/components/enforcement/nos/EnforcementInfoSection.vue
  7. 85
      zdxt-web-client/zdxt-efcode-enforcement-app/src/components/enforcement/nos/EnterpriseInfoSection.vue
  8. 619
      zdxt-web-client/zdxt-efcode-enforcement-app/src/components/enforcement/nos/RegisterPopups.vue
  9. 39
      zdxt-web-client/zdxt-efcode-enforcement-app/src/util/fileUrl.js
  10. 1
      zdxt-web-client/zdxt-efcode-enforcement-app/vite-env/.env.enforcement-dev-zrb
  11. 1
      zdxt-web-client/zdxt-efcode-enforcement-app/vite-env/.env.enforcement-prod

11
zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/controller/app/LoginController.java

@ -78,8 +78,15 @@ public abstract class LoginController {
loginUser.setNickname(nickName);
loginUser.setUserType(userType);
loginUser.setDeptId(deptId);
loginUser.setPlatformType(PlatformTypeEnums.ENTERPRISE.getKey());
if(deviceType.equals("supervision_app")){
loginUser.setPlatformType(PlatformTypeEnums.SUPERVISION.getKey());
}
else if(deviceType.equals("enforcement_app")){
loginUser.setPlatformType(PlatformTypeEnums.ENFORCEMENT.getKey());
}
else if(deviceType.equals("enterprise_app")){
loginUser.setPlatformType(PlatformTypeEnums.ENTERPRISE.getKey());
}
SaLoginParameter model = new SaLoginParameter();
model.setDeviceType(deviceType);
model.setExtra("clientid", clientId);

11
zdxt-web-client/zdxt-efcode-enforcement-app/src/api/nos/common.js

@ -0,0 +1,11 @@
import { _get, _post } from "@/http/index";
const baseUrl = "/app-enf";
// 公共api
// 执法端-查询企业信息以及二维码地址
export const queryListAndQrInfo = (params, success, error) => _post(baseUrl + "/enterpriseInfo/queryListAndQrInfo", params, success, error);
// 执法端-分页查询用户
export const queryUsersByPage = (params, success, error) => _get(baseUrl + "/system/queryUsersByPage", params, success, error);
// 查询执依据告列表
export const queryAccordingList = (params, success, error) => _get(baseUrl + "/according/list", params, success, error);

35
zdxt-web-client/zdxt-efcode-enforcement-app/src/api/nos/index.js

@ -0,0 +1,35 @@
import { _get, _post } from "@/http/index";
const baseUrl = "/app-enf";
// enf 测试单 Demo:GET /nosapi/enforcementcode/enfDemo/list -> TableDataInfo { rows, total }
export const listEnfDemo = (params) =>
_get("/enforcementcode/enfDemo/list", params);
// 非现场api
// 新增非现场执法任务
export const nosAddApi = (params, success, error) => _post("/app-enf/nos-law-enforcement-task/add", params, success, error);
// 分页查询非现场执法任务
export const nosListPageApi = (params, success, error) => _post("/app-enf/nos-law-enforcement-task/page", params, success, error);
// 获取非现场执法任务详情
export const nosInfoApi = (params, success, error) => _get("/app-enf/nos-law-enforcement-task/detail", params, success, error);
// 审核非现场执法任务
export const nosApproveApi = (params, success, error) => _post("/app-enf/nos-law-enforcement-task/approve", params, success, error);
// 查询非现场执法项目任务类别
export const nosTypeApi = (params, success, error) => _post("/app-enf/nos-law-enforcement-task/type", params, success, error);
//查询非现场执法项目任务来源列表
export const nosTaskSourceListApi = (params, success, error) => _post("/app-enf/task-source/list", params, success, error);
//查询已评价的任务
export const nosTaskHasCommentPageApi = (params, success, error) => _post("/app-enf/nos-law-enforcement-task/has-comment/page", params, success, error);
//获取当前用户所属执法单位备案
export const isFilingApi = (params, success, error) => _get("/enf-app/nos-law-enforcement-filing/detail", params, success, error);
//开启云笔录
export const cloudRecordStartApi = (params, success, error) => _get("/app-enf/nos-law-enforcement-task/start/digital-voice-logging", params, success, error);
//查询云笔录详情
export const cloudRecordDetailApi = (params, success, error) => _get("/app-enf/nos-law-enforcement-task/digital-voice-logging/detail", params, success, error);
//确认云笔录材料
export const cloudRecordConfirmApi = (params, success, error) => _post("/app-enf/nos-law-enforcement-task/digital-voice-logging/confirm", params, success, error);

2
zdxt-web-client/zdxt-efcode-enforcement-app/src/common/globalExpandRouter.js

@ -4,7 +4,7 @@ const $globalRouterModules = (routes) => {
let pathArr = [];
let nameArr = [];
const modulesRouter = import.meta.glob(
["../router/enterprise-app/ent-main-router.js", "../router/demo/demo-main-router.js", "../router/enforcement-app/enf-main-router.js", "../router/supervision-app/sup-main-router.js"]
["../router/enterprise-app/ent-main-router.js", "../router/nos/nos-base-router.js", "../router/demo/demo-main-router.js", "../router/enforcement-app/enf-main-router.js", "../router/supervision-app/sup-main-router.js"]
,{
eager: true,
});

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

@ -0,0 +1,189 @@
<template>
<div class="section">
<div class="section-title">通知/公告</div>
<!-- 附件 -->
<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="proclamation"
capture="false"
accept=".pdf"
multiple
:max-count="5"
:max-size="50 * 1024 * 1024"
@oversize="onOversize"
:before-read="onBeforeRead"
:after-read="onAfterReadImage"
/>
</div>
</div>
<!-- 任务来源 -->
<van-field
:class="taskTypeId == 5 ? 'usci-code-field' : ''"
:model-value="taskSourceName"
is-link
readonly
name="taskSourceId"
label="任务来源"
placeholder="请选择"
@click="$emit('showTaskSourcePicker')"
clearable
:rules="[{ required: taskTypeId != 5, message: '请选择任务来源' }]"
:required="taskTypeId != 5"
/>
</div>
</template>
<script setup name="AttachmentTaskSource">
import { computed } from 'vue';
import { uploadFile } from '@/util/fileUpload';
import { toFullFileUrl } from '@/util/fileUrl';
import { showToast } from 'vant';
import 'vant/es/toast/style';
const proclamation = defineModel('proclamation', { default: () => [] });
const taskSourceId = defineModel('taskSourceId', { default: '' });
const taskTypeId = defineModel('taskTypeId', { default: null });
const props = defineProps({
taskSourceOptions: {
type: Array,
default: () => []
}
});
const taskSourceName = computed(() => {
const option = props.taskSourceOptions.find((item) => item.value === taskSourceId.value);
return option?.text || '';
});
defineEmits(['showTaskSourcePicker']);
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 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 = '上传失败';
}
});
});
};
</script>
<style lang="less" scoped>
.section {
background: #fff;
// border-radius: 0.16rem;
margin-bottom: 0.24rem;
overflow: hidden;
:deep(.van-field__label) {
width: 64px;
margin-right: 22px;
color: #000;
font-size: 14px;
}
}
.section-title {
padding: 0.28rem 0.32rem;
font-size: 16px;
font-weight: 600;
color: #000;
}
.file-upload-row {
padding: 0px 20px 20px 20px;
.file-upload-label {
font-size: 14px;
color: #333;
margin-bottom: 0.16rem;
.form-text {
font-weight: 400;
font-size: 12px;
color: #b9b9b9;
text-align: left;
font-style: normal;
}
}
}
.uploader-wrapper {
:deep(.van-uploader__upload) {
background: #f5f7fa;
border-radius: 0.1rem;
}
}
:deep(.van-cell) {
padding-left: 10px;
padding-right: 10px;
}
.label-required-placeholder .form-label {
position: relative;
padding-left: 0.1rem;
&::before {
content: '*';
color: transparent;
font-size: 0.32rem;
margin-right: 0.08rem;
font-weight: bold;
}
}
:deep(.usci-code-field .van-field__label) {
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>

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

@ -0,0 +1,539 @@
<template>
<div class="section">
<div class="section-title">{{ formData.taskTypeId == 1 ? '执法信息' : '巡查信息' }}</div>
<!-- 执法单位 -->
<van-field
v-model="formData.lawEnforcementAgency"
name="lawEnforcementAgency"
:label="formData.taskTypeId == 1 ?'执法单位' : '巡查单位'"
:placeholder="formData.taskTypeId == 1 ? '请输入执法单位' : '请输入巡查单位'"
:class="formData.taskTypeId == 5 ? 'usci-code-field' : ''"
:required="formData.taskTypeId != 5"
readonly />
<!-- 执法人员 -->
<div class="form-row required">
<div class="form-label"><span class="required-mark" :class="{ 'required-mark--placeholder': formData.taskTypeId == 5 }">*</span>{{ formData.taskTypeId == 1 ? '执法人员' : '参与人员' }}</div>
<div class="form-value person-select">
<div class="person-item default-person">
<span>{{ defaultPerson?.lawEnforcerName }} {{ defaultPerson?.lawEnforcerIdCard ?? defaultPerson?.lawEnforcerPhone }}</span>
</div>
<div v-for="(person, index) in additionalPersons" :key="index" class="person-item-add">
<span @click="$emit('openPersonPicker', index)" :class="{ placeholder: !person.lawEnforcerName }">
{{ person.lawEnforcerName || "请选择" }} {{ person.lawEnforcerIdCard ?? person.lawEnforcerPhone }}
</span>
<van-icon name="close" class="remove-icon" @click="$emit('removePerson', index)" />
</div>
<div class="person-add">
<span @click="$emit('openPersonPicker', -1)">请选择</span>
<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
:model-value="category"
:is-link="!categoryDisabled"
readonly
name="taskTypeId"
class="usci-code-field"
label="类  别"
placeholder="请选择"
@click="onCategoryClick"
:clearable="!categoryDisabled"
/>
<!-- 执法事项 -->
<van-field
v-model="formData.lawEnforcementMatters"
name="lawEnforcementMatters"
v-if="formData.taskTypeId == 1 || formData.taskTypeId == 5"
:label="formData.taskTypeId == 1 ? '执法事项' : '巡查事项'"
:placeholder="formData.taskTypeId == 1 ? '请输入执法事项' : '请输入巡查事项'"
type="textarea"
class="usci-code-field"
rows="2"
autosize
maxlength="1000"
show-word-limit />
<!-- 检查要求 / 备案文件 / 验证文件 -->
<template v-if="requirementLabel">
<div class="form-row required material-header-row">
<div class="form-label"><span class="required-mark" :class="{ 'required-mark--placeholder': formData.taskTypeId == 5 }">*</span>{{ requirementLabel }}</div>
<div class="form-value">
<span class="material-hint">指定企业要上传的材料名称(例如:营业执照)</span>
</div>
</div>
<div class="form-row material-input-row">
<div class="form-label"></div>
<div class="form-value material-select">
<div v-for="(item, index) in formData.checkItemList" :key="index" class="material-item">
<input v-model="formData.checkItemList[index]" class="material-input" type="text" placeholder="请输入" maxlength="100" />
<van-icon v-if="index === formData.checkItemList.length - 1" name="add-o" class="add-circle-icon" @click.stop="addMaterialRow" />
<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="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 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 }">
{{ basis || "请选择" }}
</span>
<van-icon v-if="index === formData.legalBasis.length - 1" name="add-o" class="add-circle-icon" @click.stop="$emit('addBasis')" />
<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>
<!-- 原因 -->
<!-- <van-field v-model="formData.reason" name="reason" label="原因" placeholder="请输入原因" readonly /> -->
<!-- 执法时间 -->
<van-field
v-model="formData.deadline"
name="deadline"
:class="formData.taskTypeId == 5 ? 'usci-code-field' : ''"
label="截止时间"
placeholder="请选择截止时间"
is-link
readonly
:rules="[{ required: formData.taskTypeId != 5, message: '请选择截止时间' }]"
@click="$emit('showTimePicker')"
:required="formData.taskTypeId != 5" />
<!-- 联合执法 -->
</div>
</template>
<script setup name="EnforcementInfoSection">
const formData = defineModel('formData', { required: true });
const additionalPersons = defineModel('additionalPersons', { default: () => [] });
const props = defineProps({
defaultPerson: {
type: Object,
default: null
},
categoryDisabled: {
type: Boolean,
default: false
},
category: {
type: String,
default: ''
}
});
const emit = defineEmits(['openPersonPicker', 'removePerson', 'addPerson', 'showCategoryPicker', 'showTaskSourcePicker', 'showTimePicker', 'openBasisPicker', 'addBasis', 'removeBasis']);
const onCategoryClick = () => {
if (!props.categoryDisabled) {
emit('showCategoryPicker');
}
};
const CATEGORY_REQUIREMENT_LABEL_MAP = {
行政检查: '检查要求',
材料备案: '备案文件',
材料验证: '验证文件',
巡查: '巡查要求'
};
// 根据类别动态显示 label(独立 category prop,避免 keep-alive 下嵌套对象追踪失效)
const requirementLabel = computed(() => {
return CATEGORY_REQUIREMENT_LABEL_MAP[props.category] || '';
});
// 计算属性:是否有执法人员
const hasPerson = computed(() => {
return (props.defaultPerson?.lawEnforcerName || additionalPersons.value.some((p) => p.lawEnforcerName)) ? '已填写' : '';
});
// 是否已填写检查项
const hasMaterialRequirement = computed(() => {
return formData.value.checkItemList?.some((item) => item?.trim()) ? '已填写' : '';
});
// 是否已填写执法依据
const hasLegalBasis = computed(() => {
return formData.value.legalBasis?.some((item) => item?.trim()) ? '已填写' : '';
});
const addMaterialRow = () => {
formData.value.checkItemList.push('');
};
const removeMaterialRow = (index) => {
if (formData.value.checkItemList.length > 1) {
formData.value.checkItemList.splice(index, 1);
}
};
</script>
<style lang="less" scoped>
.section {
background: #fff;
// border-radius: 0.16rem;
margin-bottom: 0.24rem;
overflow: hidden;
:deep(.van-field__label) {
margin-right: 6px;
color: #000;
}
}
.section-title {
padding: 0.28rem 0.32rem;
font-size: 16px;
font-weight: 600;
color: #000;
}
.form-row {
display: flex;
justify-content: space-between;
// align-items: center;
padding: 10px;
border-bottom: 1px solid #f0f0f0;
flex-wrap: wrap;
&:last-child {
border-bottom: none;
}
&.clickable {
&:active {
background: #f9f9f9;
}
}
&.required .form-label {
position: relative;
}
&.radio-row {
padding-top: 0.24rem;
padding-bottom: 0.24rem;
}
}
.attachment-row {
}
.required-mark {
color: #ee0a24;
font-size: 0.32rem;
margin-right: 0.08rem;
font-weight: bold;
&--placeholder {
visibility: hidden;
}
}
.form-label {
font-size: 14px;
color: #333;
flex: none;
width: 2.4rem;
font-weight: 500;
}
.form-value {
flex: 1;
font-size: 14px;
color: #333;
display: flex;
align-items: center;
.placeholder {
color: #c8c9cc;
}
}
.person-select {
// margin-left: 30px;
display: block;
// flex-direction: column;
// align-items: flex-end;
// gap: 0.16rem;
}
.person-item {
display: flex;
align-items: center;
font-size: 14px;
&.default-person span {
color: #333;
font-size: 14px;
}
}
.person-item-add {
padding: 10px 10px 0px 0px;
display: flex;
justify-content: space-between;
align-items: center;
.remove-icon {
font-size: 16px;
}
}
.person-add {
display: flex;
align-items: center;
justify-content: space-between;
align-items: center;
color: #c8c9cc;
font-size: 14px;
padding: 10px 10px 0px 0px;
.add-circle-icon {
font-size: 16px;
color: #333;
}
}
.basis-select {
display: block;
}
.basis-item-add {
padding: 10px 10px 0 0;
display: flex;
justify-content: space-between;
align-items: center;
&:first-child {
padding-top: 0;
}
.remove-icon,
.add-circle-icon {
font-size: 16px;
color: #333;
}
}
.material-header-row {
padding-bottom: 0.12rem;
border-bottom: none;
}
.material-hint {
font-size: 12px;
color: #999;
line-height: 1.4;
}
.material-input-row {
padding-top: 0;
align-items: flex-start;
}
.material-select {
display: block;
}
.material-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 10px 10px 0 0;
&:first-child {
padding-top: 0;
}
:deep(.van-checkbox) {
flex: none;
}
.material-input {
flex: 1;
min-width: 0;
max-width: 100%;
border: none;
outline: none;
font-size: 14px;
color: #333;
background: transparent;
padding-right: 0;
// text-align: right;
&::placeholder {
color: #c8c9cc;
}
}
.remove-icon,
.add-circle-icon {
flex: none;
font-size: 16px;
color: #333;
}
}
.form-section {
margin-bottom: 10px;
:deep(.van-field__value) {
display: flex;
justify-content: flex-end;
color: #000;
}
}
.form-entrusted {
height: 200px;
background-color: #fff;
.entrusted-enforcement {
:deep(.van-field__value) {
display: flex;
justify-content: flex-end;
color: #000;
}
}
}
.radio-option {
padding: 0.12rem 0.48rem;
border: 1px solid #dcdee0;
border-radius: 0.06rem;
font-size: 0.3rem;
color: #666;
transition: all 0.2s;
&:active {
transform: scale(0.95);
}
&.active {
border-color: #1367fe;
color: #1367fe;
background: rgba(19, 103, 254, 0.05);
}
}
.form-hint {
font-size: 12px;
color: #999;
padding: 0.12rem 0.32rem 0.24rem;
text-align: right;
}
.attachment-wrapper {
flex-direction: column;
align-items: flex-end;
gap: 0.16rem;
}
.attachment-hint {
font-size: 0.26rem;
color: #999;
}
.upload-icon {
font-size: 0.72rem;
color: #c8c9cc;
}
.file-upload-row {
padding: 0px 20px 20px 20px;
.file-upload-label {
font-size: 14px;
color: #333;
margin-bottom: 0.16rem;
}
}
.uploader-wrapper {
:deep(.van-uploader__upload) {
background: #f5f7fa;
border-radius: 0.1rem;
}
}
:deep(.van-cell){
padding-left: 10px;
padding-right: 10px;
}
:deep(.usci-code-field .van-field__label) {
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>

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

@ -0,0 +1,85 @@
<template>
<div class="section">
<div class="section-title">检查对象</div>
<!-- 企业名称 -->
<van-field
v-model="enterpriseData.enterpriseName"
:is-link="isSignMode"
:readonly="true"
name="enterpriseName"
label="企业名称"
placeholder="请选择"
@click="handleClick"
clearable
:rules="isSignMode ? [{ required: true, message: '请选择企业名称' }] : []"
:required="isSignMode" />
<!-- 统一社会信用代码 -->
<van-field
class="usci-code-field"
v-model="enterpriseData.usciCode"
:is-link="isSignMode"
:readonly="true"
name="usciCode"
label="统一社会信用代码"
placeholder="请选择"
@click="handleClick"
clearable
:rules="isSignMode ? [{ required: true, message: '请选择统一社会信用代码' }] : []" />
</div>
</template>
<script setup name="EnterpriseInfoSection">
const enterpriseData = defineModel({
default: () => ({
enterpriseName: '',
usciCode: '',
enterpriseId: ''
})
});
defineProps({
isSignMode: {
type: Boolean,
default: false
}
});
const emit = defineEmits(['open-picker']);
const handleClick = () => {
emit('open-picker');
};
</script>
<style lang="less" scoped>
.section {
background: #fff;
// border-radius: 0.16rem;
margin-bottom: 0.24rem;
overflow: hidden;
:deep(.van-field__label) {
width: 64px;
margin-right: 22px;
color: #000;
// font-size: 14px;
}
}
.section-title {
padding: 0.28rem 0.32rem;
font-size: 16px;
font-weight: 600;
color: #000;
}
:deep(.van-cell) {
padding-left: 10px;
padding-right: 10px;
}
:deep(.usci-code-field .van-field__label) {
padding-left: 8px; // 按需调整
box-sizing: border-box;
}
</style>

619
zdxt-web-client/zdxt-efcode-enforcement-app/src/components/enforcement/nos/RegisterPopups.vue

@ -0,0 +1,619 @@
<template>
<div class="register-popups">
<!-- 企业选择弹窗 -->
<van-popup v-model:show="localEnterprisePopup" position="bottom" :style="{ height: '80%' }" round>
<div class="enterprise-search-popup">
<div class="popup-header">
<span class="popup-title">选择企业</span>
<van-icon name="cross" class="close-icon" @click="localEnterprisePopup = false" />
</div>
<div class="search-bar">
<van-search v-model="enterpriseSearchKeyword" placeholder="请输入企业名称/统一信用代码搜索" @search="handleEnterpriseSearch" @clear="clearEnterpriseSearch" />
<van-button class="search-btn" type="primary" @click="handleEnterpriseSearch">搜索</van-button>
</div>
<van-list v-model:loading="enterpriseLoading" :finished="enterpriseFinished" :immediate-check="false"
finished-text="没有更多了" @load="loadEnterpriseList" class="enterprise-list">
<div v-for="(item, index) in enterpriseList" :key="index" class="enterprise-list-item" @click="$emit('selectEnterprise', item)">
<div class="enterprise-name">{{ item.enterpriseName }}</div>
<div class="enterprise-number">统一社会信用代码:{{ item.usciCode }}</div>
</div>
<van-empty v-if="enterpriseList.length === 0 && !enterpriseLoading" description="暂无数据" />
</van-list>
</div>
</van-popup>
<!-- 类别选择弹窗 -->
<van-popup v-model:show="localCategoryPopup" round position="bottom">
<van-picker :columns="options.categories" @cancel="localCategoryPopup = false" @confirm="$emit('confirmCategory', $event)" />
</van-popup>
<!-- 任务来源弹窗 -->
<van-popup v-model:show="localTaskSourcePopup" round position="bottom">
<van-picker :columns="options.taskSources" @cancel="localTaskSourcePopup = false" @confirm="$emit('confirmTaskSource', $event)" />
</van-popup>
<!-- 人员查询弹窗 -->
<van-popup v-model:show="localPersonPopup" position="bottom" :style="{ height: '80%' }">
<div class="person-search-popup">
<div class="popup-header">
<span class="popup-title">人员查询</span>
<van-icon name="cross" class="close-icon" @click="localPersonPopup = false" />
</div>
<div class="search-bar">
<van-search v-model="personSearchKeyword" placeholder="请输入姓名/执法证件号搜索" @search="handlePersonSearch"
@clear="clearPersonSearch" />
<van-button class="search-btn" type="primary" @click="handlePersonSearch">搜索</van-button>
</div>
<van-list v-model:loading="personLoading" :finished="personFinished" :immediate-check="false"
finished-text="没有更多了" @load="loadPersonList" class="person-list">
<div v-for="(person, index) in personList" :key="index" class="person-list-item"
@click="handleSelectPerson(person)">
<div class="person-name">{{ person.userName }}</div>
<div class="person-id">执法证件号:{{ person.lawEnforcerIdCard ?? person.lawEnforcerPhone }}</div>
</div>
<van-empty v-if="personList.length === 0 && !personLoading" description="暂无数据" />
</van-list>
</div>
</van-popup>
<!-- 执法依据弹窗 -->
<van-popup v-model:show="localBasisPopup" position="bottom" :style="{ height: '80%' }">
<div class="enforcement-basis-popup">
<div class="popup-header">
<span class="popup-title">执法依据</span>
<van-icon name="cross" class="close-icon" @click="localBasisPopup = false" />
</div>
<div class="search-bar">
<van-search v-model="basisSearchKeyword" placeholder="请输入关键字搜索" @search="handleBasisSearch" @clear="clearBasisSearch" />
<van-button class="search-btn" type="primary" @click="handleBasisSearch">搜索</van-button>
</div>
<van-list v-model:loading="basisLoading" :finished="basisFinished" :immediate-check="false"
finished-text="没有更多了" @load="loadBasisList" class="enforcement-basis-list">
<div v-for="(item, index) in enforcementBasisList" :key="index" class="basis-list-item" @click="$emit('selectBasis', item)">
<div class="basis-name">{{ item.name }}</div>
</div>
<van-empty v-if="enforcementBasisList.length === 0 && !basisLoading" description="暂无数据" />
</van-list>
</div>
</van-popup>
<!-- 执法时间弹窗(仅年月日,不可选今天及以前) -->
<van-popup v-model:show="localTimePopup" destroy-on-close round position="bottom">
<van-date-picker v-model="timePickerDate" title="选择执法时间" :min-date="minDate" @confirm="handleTimeConfirm" @cancel="localTimePopup = false" />
</van-popup>
</div>
</template>
<script setup name="RegisterPopups">
import { ref, reactive, watch, computed } from "vue"
import { queryListAndQrInfo, queryUsersByPage, queryAccordingList } from "@/api/nos/common"
const props = defineProps({
visible: {
type: Object,
required: true
},
options: {
type: Object,
default: () => ({
categories: [],
taskSources: []
})
},
/** 当前截止时间,用于弹窗回显,格式:YYYY-MM-DD */
deadline: {
type: String,
default: ""
}
})
const emit = defineEmits(["close-popup", "selectEnterprise", "confirmCategory", "confirmTaskSource", "selectPerson", "selectBasis", "selectTime"])
const timePickerDate = ref([])
const pad2 = (n) => String(n).padStart(2, "0")
/** 最小可选日期:明天(今天之后) */
const createMinDate = () => {
const d = new Date()
d.setHours(0, 0, 0, 0)
d.setDate(d.getDate() + 1)
return d
}
const minDate = ref(createMinDate())
const dateToPickerValues = (date) => [String(date.getFullYear()), pad2(date.getMonth() + 1), pad2(date.getDate())]
const getDefaultPickerDate = () => dateToPickerValues(createMinDate())
const pickerValuesToDate = ([y, m, d]) => new Date(Number(y), Number(m) - 1, Number(d))
const parseDeadline = (str) => {
if (!str?.trim()) {
return getDefaultPickerDate()
}
const datePart = str.trim().split(" ")[0]
const [y, m, d] = datePart.split("-")
if (!y || !m || !d) {
return getDefaultPickerDate()
}
const selected = pickerValuesToDate([y, m, d])
if (selected < minDate.value) {
return getDefaultPickerDate()
}
return [y, pad2(m), pad2(d)]
}
const formatDeadline = (dateValues) => {
const [y, m, d] = dateValues
return `${y}-${pad2(m)}-${pad2(d)}`
}
const initTimePicker = () => {
minDate.value = createMinDate()
timePickerDate.value = parseDeadline(props.deadline)
}
const handleTimeConfirm = ({ selectedValues }) => {
emit("selectTime", formatDeadline(selectedValues))
}
// 使用 computed 创建可写的计算属性(自动同步 props,保证响应式)
const localEnterprisePopup = computed({
get: () => props.visible.enterprise,
set: () => emit("close-popup", "enterprise")
})
const localNoticePopup = computed({
get: () => props.visible.notice,
set: () => emit("close-popup", "notice")
})
const localCategoryPopup = computed({
get: () => props.visible.category,
set: () => emit("close-popup", "category")
})
const localTaskSourcePopup = computed({
get: () => props.visible.taskSource,
set: () => emit("close-popup", "taskSource")
})
const localPersonPopup = computed({
get: () => props.visible.person,
set: () => emit("close-popup", "person")
})
const localBasisPopup = computed({
get: () => props.visible.basis,
set: () => emit("close-popup", "basis")
})
const localTimePopup = computed({
get: () => props.visible.time,
set: () => emit("close-popup", "time")
})
// ========== 企业选择相关 ==========
const enterpriseSearchKeyword = ref("")
const enterpriseList = ref([])
const enterpriseLoading = ref(false)
const enterpriseFinished = ref(false)
const enterprisePageNum = ref(1)
const enterprisePageSize = ref(10)
const loadEnterpriseList = () => {
if (enterpriseFinished.value) return
enterpriseLoading.value = true
queryListAndQrInfo(
{
pageSize: enterprisePageSize.value,
pageNum: enterprisePageNum.value,
enterpriseName: enterpriseSearchKeyword.value || undefined,
},
(res) => {
console.log(res)
if (res.code === 200) {
const data = res.rows.map(item => ({
enterpriseName: item.enterpriseName,
usciCode: item.enterpriseNumber,
enterpriseId: item.id
}))
const total = res.total || 0
if (enterprisePageNum.value === 1) {
enterpriseList.value = data
} else {
enterpriseList.value = [...enterpriseList.value, ...data]
}
if (total <= enterprisePageSize.value * enterprisePageNum.value) {
enterpriseFinished.value = true
} else {
enterprisePageNum.value++
}
} else {
enterpriseFinished.value = true
}
enterpriseLoading.value = false
},
(err) => {
console.log(err)
enterpriseLoading.value = false
enterpriseFinished.value = true
}
)
}
const initEnterpriseList = () => {
enterpriseFinished.value = false
enterprisePageNum.value = 1
enterpriseList.value = []
enterpriseLoading.value = false
loadEnterpriseList()
}
const handleEnterpriseSearch = () => {
initEnterpriseList()
}
const clearEnterpriseSearch = () => {
enterpriseSearchKeyword.value = ""
initEnterpriseList()
}
watch(localEnterprisePopup, (val) => {
if (val) {
enterpriseSearchKeyword.value = ""
initEnterpriseList()
}
})
// ========== 人员查询相关 ==========
const personSearchKeyword = ref("")
const personList = ref([])
const personLoading = ref(false)
const personFinished = ref(false)
const personPageNum = ref(1)
const personPageSize = ref(10)
const loadPersonList = () => {
if (personFinished.value) return
personLoading.value = true
queryUsersByPage(
{
pageSize: personPageSize.value,
pageNum: personPageNum.value,
lawEnforcerName: personSearchKeyword.value || undefined,
},
(res) => {
console.log(res)
if (res.code === 200) {
const data = res.rows.map(item => ({
userId: item.userId,
userName: item.userName,
phonenumber: item.phonenumber,
lawEnforcerIdCard: item.lawCertificateNum,
lawEnforcerId: item.userId,
lawEnforcerName: item.userName,
lawEnforcerPhone: item.phonenumber,
}))
const total = res.total || res.count || 0
if (personPageNum.value === 1) {
personList.value = data
} else {
personList.value = [...personList.value, ...data]
}
if (total <= personPageSize.value * personPageNum.value) {
personFinished.value = true
} else {
personPageNum.value++
}
} else {
personFinished.value = true
}
personLoading.value = false
},
(err) => {
console.log(err)
personLoading.value = false
personFinished.value = true
}
)
}
const initPersonList = () => {
personFinished.value = false
personPageNum.value = 1
personList.value = []
personLoading.value = false
loadPersonList()
}
const handleSelectPerson = (person) => {
emit('selectPerson', person)
}
const handlePersonSearch = () => {
initPersonList()
}
const clearPersonSearch = () => {
personSearchKeyword.value = ""
initPersonList()
}
watch(localPersonPopup, (val) => {
if (val) {
personSearchKeyword.value = ""
initPersonList()
}
})
// ========== 执法依据相关 ==========
const basisSearchKeyword = ref("")
const enforcementBasisList = ref([])
const basisLoading = ref(false)
const basisFinished = ref(false)
const basisPageNum = ref(1)
const basisPageSize = ref(10)
const loadBasisList = () => {
if (basisFinished.value) return
basisLoading.value = true
queryAccordingList(
{
pageSize: basisPageSize.value,
pageNum: basisPageNum.value,
name: basisSearchKeyword.value || undefined,
},
(res) => {
console.log(res)
if (res.code === 200) {
const data = res.rows.map(item => ({
id: item.id,
name: `《${item.lawName}》`,
}))
const total = res.total || res.count || 0
if (basisPageNum.value === 1) {
enforcementBasisList.value = data
} else {
enforcementBasisList.value = [...enforcementBasisList.value, ...data]
}
if (total <= basisPageSize.value * basisPageNum.value) {
basisFinished.value = true
} else {
basisPageNum.value++
}
} else {
basisFinished.value = true
}
basisLoading.value = false
},
(err) => {
console.log(err)
basisLoading.value = false
basisFinished.value = true
}
)
}
const initBasisList = () => {
basisFinished.value = false
basisPageNum.value = 1
enforcementBasisList.value = []
basisLoading.value = false
loadBasisList()
}
const handleBasisSearch = () => {
initBasisList()
}
const clearBasisSearch = () => {
basisSearchKeyword.value = ""
initBasisList()
}
watch(localBasisPopup, (val) => {
if (val) {
basisSearchKeyword.value = ""
initBasisList()
}
})
watch(localTimePopup, (val) => {
if (val) {
initTimePicker()
}
})
// ========== 预告查询相关 ==========
const noticeList = ref([])
const noticePage = ref(1)
const loading = ref(false)
const finished = ref(false)
const loadNotices = () => {
if (loading.value || finished.value) return
loading.value = true
setTimeout(() => {
const mockData = [
{ id: 1, matter: "例行检查", time: "2026-05-20 10:00" },
{ id: 2, matter: "专项检查", time: "2026-05-19 14:30" }
]
if (noticePage.value === 1) {
noticeList.value = mockData
} else {
noticeList.value = [...noticeList.value, ...mockData]
}
loading.value = false
if (noticePage.value >= 1) {
finished.value = true
}
noticePage.value++
}, 500)
}
const onNoticeScroll = () => {}
watch(localNoticePopup, (newVal) => {
if (newVal) {
noticePage.value = 1
noticeList.value = []
loading.value = false
finished.value = false
loadNotices()
}
})
</script>
<style lang="less" scoped>
.register-popups {
// 容器样式(可选)
}
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.3rem;
border-bottom: 1px solid #f5f5f5;
.popup-title {
font-size: 0.4rem;
font-weight: 600;
color: #333;
}
.close-icon {
font-size: 0.45rem;
color: #999;
}
}
.search-bar {
display: flex;
align-items: center;
margin: 0.3rem;
// padding: 0.3rem;
:deep(.van-search) {
padding-right: 0px;
flex: 1;
height: 34px;
}
.search-btn {
flex: none;
height: 34px;
}
}
.enterprise-search-popup,
.person-search-popup,
.enforcement-basis-popup {
height: 100%;
display: flex;
flex-direction: column;
}
.enterprise-list,
.person-list,
.enforcement-basis-list {
flex: 1;
overflow-y: auto;
padding: 0 0.3rem;
}
.enterprise-list-item,
.person-list-item,
.basis-list-item {
padding: 20px 20px 14px 14px;
border-bottom: 1px solid #f5f5f5;
&:active {
background: #f9f9f9;
}
}
.enterprise-name,
.person-name,
.basis-name {
font-size: 16px;
color: #333;
margin-bottom: 10px;
}
.enterprise-number,
.person-id {
font-size: 14px;
color: #999;
}
.notice-popup {
height: 100%;
display: flex;
flex-direction: column;
}
.popup-content {
flex: 1;
overflow-y: auto;
padding: 0.3rem;
}
.notice-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.3rem;
// background: #f9f9f9;
margin-bottom: 0.2rem;
border-bottom: 0.1px solid #ededed;
.notice-info {
flex: 1;
}
.notice-matter {
font-size: 16px;
color: #000;
margin-bottom: 10px;
}
.notice-time {
font-size: 14px;
color: #999;
}
.link-btn {
flex: none;
font-size: 12px;
height: 26px;
}
}
.loading-more,
.no-more {
text-align: center;
padding: 0.3rem;
font-size: 0.28rem;
color: #999;
display: flex;
align-items: center;
justify-content: center;
gap: 0.1rem;
}
</style>

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

@ -0,0 +1,39 @@
import { baseUrl } from "@/config/env"
const isAbsoluteUrl = (path) => typeof path === "string" && /^https?:\/\//i.test(path)
const getApiBase = () => (baseUrl || "").replace(/\/+$/, "")
/**
* 去掉 VITE_BASE_URL 多余前缀,支持重复前缀,得到相对路径,如 /profile/2026/...
*/
export const toRelativeFilePath = (path) => {
if (typeof path !== "string" || !path) return ""
if (isAbsoluteUrl(path)) return path
const prefix = getApiBase()
let p = path.startsWith("/") ? path : `/${path}`
if (prefix) {
const normalizedPrefix = prefix.startsWith("/") ? prefix : `/${prefix}`
while (p === normalizedPrefix || p.startsWith(`${normalizedPrefix}/`)) {
p = p.slice(normalizedPrefix.length) || "/"
if (!p.startsWith("/")) p = `/${p}`
}
}
return p
}
/**
* 拼接为前端可访问路径,如 /profile/2026/06/12/xxx.pdf
*/
export const toFullFileUrl = (path) => {
if (typeof path !== "string" || !path) return ""
if (isAbsoluteUrl(path)) return path
const prefix = getApiBase()
const relative = toRelativeFilePath(path)
if (!prefix) return relative
return prefix + relative
}

1
zdxt-web-client/zdxt-efcode-enforcement-app/vite-env/.env.enforcement-dev-zrb

@ -5,3 +5,4 @@ VITE_CONTEXT_PATH = '/efcode20-enf-app/'
VITE_PROXY_TARGET = 'http://localhost:17000'
VITE_DEFAULT_LOGIN = '/enforcement/loginByPassword'
VITE_APP_ENV = 'dev'
VITE_FILE_URL = ''

1
zdxt-web-client/zdxt-efcode-enforcement-app/vite-env/.env.enforcement-prod

@ -5,3 +5,4 @@ VITE_PROXY_TARGET = 'http://localhost:17000'
VITE_CONTEXT_PATH = '/efcode20-enf-app/'
VITE_DEFAULT_LOGIN = '/enforce/loginByPassword'
VITE_APP_ENV = 'prod'
VITE_FILE_URL = ''

Loading…
Cancel
Save