Browse Source

feat:代码提交

main
上官小雨 5 months ago
committed by masiyuan
parent
commit
94f2a3f762
  1. 5
      zdxt-web-client/zdxt-admin-nos-plus-ui/.env.development
  2. 2
      zdxt-web-client/zdxt-admin-nos-plus-ui/package.json
  3. 46
      zdxt-web-client/zdxt-admin-nos-plus-ui/src/api/nos/index.ts
  4. 212
      zdxt-web-client/zdxt-admin-nos-plus-ui/src/api/nos/type.ts
  5. BIN
      zdxt-web-client/zdxt-admin-nos-plus-ui/src/assets/images/eval-bad-dis.png
  6. BIN
      zdxt-web-client/zdxt-admin-nos-plus-ui/src/assets/images/eval-bad.png
  7. BIN
      zdxt-web-client/zdxt-admin-nos-plus-ui/src/assets/images/eval-good-dis.png
  8. BIN
      zdxt-web-client/zdxt-admin-nos-plus-ui/src/assets/images/eval-good.png
  9. BIN
      zdxt-web-client/zdxt-admin-nos-plus-ui/src/assets/images/eval-verygood-dis.png
  10. BIN
      zdxt-web-client/zdxt-admin-nos-plus-ui/src/assets/images/eval-verygood.png
  11. 57
      zdxt-web-client/zdxt-admin-nos-plus-ui/src/router/index.ts
  12. 1
      zdxt-web-client/zdxt-admin-nos-plus-ui/src/types/env.d.ts
  13. 5
      zdxt-web-client/zdxt-admin-nos-plus-ui/src/utils/request.ts
  14. 443
      zdxt-web-client/zdxt-admin-nos-plus-ui/src/views/nos/add.vue
  15. 397
      zdxt-web-client/zdxt-admin-nos-plus-ui/src/views/nos/detail.vue
  16. 43
      zdxt-web-client/zdxt-admin-nos-plus-ui/src/views/nos/index.vue
  17. 344
      zdxt-web-client/zdxt-admin-nos-plus-ui/src/views/nos/legalTech.vue
  18. 400
      zdxt-web-client/zdxt-admin-nos-plus-ui/src/views/nos/legalTechEdit.vue
  19. 2
      zdxt-web-client/zdxt-efcode-nos-enforcement-app/.prettierrc
  20. 11
      zdxt-web-client/zdxt-efcode-nos-enforcement-app/src/api/nos/enfDemo.js
  21. 9
      zdxt-web-client/zdxt-efcode-nos-enforcement-app/src/router/nos/nos-base-router.js
  22. 47
      zdxt-web-client/zdxt-efcode-nos-enforcement-app/src/views/enforcement/nos/components/NosDetailMaterialsTab.vue
  23. 381
      zdxt-web-client/zdxt-efcode-nos-enforcement-app/src/views/enforcement/nos/nosCloudRecord.vue
  24. 191
      zdxt-web-client/zdxt-efcode-nos-enforcement-app/src/views/enforcement/nos/nosDetail.vue
  25. 141
      zdxt-web-client/zdxt-efcode-nos-enforcement-app/src/views/enforcement/nos/nosList.vue
  26. 2
      zdxt-web-client/zdxt-efcode-nos-enforcement-app/vite-env/.env.enforcement-dev
  27. 20
      zdxt-web-client/zdxt-efcode-nos-enterprise-app/.prettierrc
  28. 17
      zdxt-web-client/zdxt-efcode-nos-enterprise-app/src/api/nos/enfDemo.js
  29. 2
      zdxt-web-client/zdxt-efcode-nos-enterprise-app/src/components/FooterBar.vue
  30. 32
      zdxt-web-client/zdxt-efcode-nos-enterprise-app/src/router/nos/nos-base-router.js
  31. 417
      zdxt-web-client/zdxt-efcode-nos-enterprise-app/src/views/enterprise/nos/components/NosDetailMaterialsTab.vue
  32. 105
      zdxt-web-client/zdxt-efcode-nos-enterprise-app/src/views/enterprise/nos/message/index.vue
  33. 330
      zdxt-web-client/zdxt-efcode-nos-enterprise-app/src/views/enterprise/nos/message/nosTaskList.vue
  34. 571
      zdxt-web-client/zdxt-efcode-nos-enterprise-app/src/views/enterprise/nos/nosCloudRecord.vue
  35. 29
      zdxt-web-client/zdxt-efcode-nos-enterprise-app/src/views/enterprise/nos/nosDetail.vue
  36. 2
      zdxt-web-client/zdxt-efcode-nos-enterprise-app/vite-env/.env.enterprise-dev

5
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://139.186.148.21:8080'
VITE_APP_PROXY_TARGET = 'http://192.168.1.7:28080' VITE_APP_PROXY_TARGET = 'http://192.168.1.11:28080'
# 页面标题 # 页面标题
VITE_APP_TITLE = zdxt-Vue-Plus多租户管理系统 VITE_APP_TITLE = zdxt-Vue-Plus多租户管理系统
VITE_APP_LOGO_TITLE = 行政执法监督码 VITE_APP_LOGO_TITLE = 行政执法监督码
@ -10,6 +10,9 @@ VITE_APP_ENV = 'development'
# 开发环境 # 开发环境
VITE_APP_BASE_API = '/dev-api' VITE_APP_BASE_API = '/dev-api'
# 执法端接口(如 /app-enf/...),开发环境直连
VITE_APP_ENF_BASE_API = 'http://139.186.148.21/dev-api'
# 应用访问路径 例如使用前缀 /admin/ # 应用访问路径 例如使用前缀 /admin/
VITE_APP_CONTEXT_PATH = '/' VITE_APP_CONTEXT_PATH = '/'

2
zdxt-web-client/zdxt-admin-nos-plus-ui/package.json

@ -7,7 +7,7 @@
"license": "MIT", "license": "MIT",
"type": "module", "type": "module",
"scripts": { "scripts": {
"vite-server-web-supervision-dev": "vite serve --mode development", "vite": "vite serve --mode development",
"vite-server-web-supervision-dev-zrb": "vite serve --mode development-zrb-cust", "vite-server-web-supervision-dev-zrb": "vite serve --mode development-zrb-cust",
"build-zdxt-admin-plus-ui:prod": "vite build --mode production --outDir dist/prod", "build-zdxt-admin-plus-ui:prod": "vite build --mode production --outDir dist/prod",
"build-zdxt-admin-plus-ui:dev": "vite build --mode development --outDir dist/dev", "build-zdxt-admin-plus-ui:dev": "vite build --mode development --outDir dist/dev",

46
zdxt-web-client/zdxt-admin-nos-plus-ui/src/api/nos/index.ts

@ -1,5 +1,15 @@
import request from '@/utils/request'; import request, { enfBaseURL } from '@/utils/request';
import { TaskPageRequest, Response, DatailResponse } from './type'; import {
TaskPageRequest,
Response,
DatailResponse,
LegalTechRecordsPageRequest,
LegalTechRecordsPageResponse,
LegalTechRecordsDetailResponse,
SaveOrEditLegalTechRecordsRequest,
SaveOrEditLegalTechRecordsResponse,
LawEnforcementAgencyOption
} from './type';
import { AxiosPromise } from 'axios'; import { AxiosPromise } from 'axios';
// 分页查询非现场执法任务 // 分页查询非现场执法任务
@ -15,6 +25,38 @@ export function getTaskPage(data: TaskPageRequest): AxiosPromise<Response> {
export function getTaskDetail(taskId: Number): AxiosPromise<DatailResponse> { export function getTaskDetail(taskId: Number): AxiosPromise<DatailResponse> {
return request({ return request({
url: `/web-sup/nos-law-enforcement-task/detail?taskId=${taskId}`, url: `/web-sup/nos-law-enforcement-task/detail?taskId=${taskId}`,
method: 'get'
});
}
// 分页查询法制与技术材料留存
export function getLegalTechRecordsPage(data: LegalTechRecordsPageRequest): AxiosPromise<LegalTechRecordsPageResponse> {
return request({
url: `/web-sup/nos-law-enforcement-filing/page`,
method: 'post',
data
});
}
// 分页查询法制与技术材料留存
export function getLegalTechRecordsDetail(id: string): AxiosPromise<LegalTechRecordsDetailResponse> {
return request({
url: `/web-sup/nos-law-enforcement-filing/detail`,
method: 'get',
params: { id }
});
}
// 新增/编辑非现场执法备案
export function saveOrEditLegalTechRecords(data: SaveOrEditLegalTechRecordsRequest): AxiosPromise<SaveOrEditLegalTechRecordsResponse> {
return request({
url: `/web-sup/nos-law-enforcement-filing/save-or-edit`,
method: 'post',
data
});
}
// 查询执法单位
export function selectLawEnforcementAgency(): AxiosPromise<LawEnforcementAgencyOption[]> {
return request({
baseURL: enfBaseURL,
url: `/web/enforcementcode/educationServices/publishDeptOptions`,
method: 'get', method: 'get',
}); });
} }

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

@ -140,17 +140,33 @@ export interface NosLawEnforcementTaskDetailVO {
*/ */
checkTime?: string; checkTime?: string;
/** /**
* 评价json(id + 名称 + 顺序 + 分数) * 云文字
*/ */
comment?: NosLawEnforcementTaskCommentItem[]; cloudText?: string;
/**
* 云视频
*/
cloudVideo?: string;
/**
* 评价原因
*/
commentReason?: string;
/** /**
* 创建时间 * 创建时间
*/ */
createTime?: string; createTime?: string;
/**
* 是否创建人
*/
creator?: boolean;
/** /**
* 截止日期 * 截止日期
*/ */
deadline?: string; deadline?: string;
/**
* 云笔录状态(0-未发起,1-待企业提交,2-待执法人员确认,3-已完成)
*/
digitalVoiceLoggingStatus?: number;
/** /**
* 企业处理时间 * 企业处理时间
*/ */
@ -164,6 +180,10 @@ export interface NosLawEnforcementTaskDetailVO {
* 任务id * 任务id
*/ */
id?: string; id?: string;
/**
* 是否评价(0-未评价,1-已评价)
*/
isEvaluation?: number;
/** /**
* 执法单位名称 * 执法单位名称
*/ */
@ -185,13 +205,13 @@ export interface NosLawEnforcementTaskDetailVO {
*/ */
proclamation?: TaskProclamation[]; proclamation?: TaskProclamation[];
/** /**
* 评价原因 * 满意度(0-满意,1-较满意,2-不满意)
*/ */
commentReason?: string; satisfaction?: number;
/** /**
* 满意度(0-满意,1-一般-不满意) * 云笔录意见
*/ */
satisfaction?: number; suggestion?: string;
/** /**
* 检查项 * 检查项
*/ */
@ -220,6 +240,10 @@ export interface NosLawEnforcementTaskDetailVO {
* 任务状态名称 * 任务状态名称
*/ */
taskStatusName?: string; taskStatusName?: string;
/**
* 类别id
*/
taskTypeId?: number;
/** /**
* 类别名称 * 类别名称
*/ */
@ -338,3 +362,179 @@ export interface MapObject {
key?: { [key: string]: any }; key?: { [key: string]: any };
[property: string]: any; [property: string]: any;
} }
/**
* 查询参数
*
* NosLawEnforcementFilingPageBO
*/
export interface LegalTechRecordsPageRequest {
/**
* 结束时间
*/
endTime?: string;
/**
* 执法单位名称
*/
lawEnforcementAgency?: string;
pageNo: number;
pageSize: number;
/**
* 开始时间
*/
startTime?: string;
[property: string]: any;
}
/**
* 分页结果
*
* TableDataInfoNosLawEnforcementFilingPageVO
*/
export interface LegalTechRecordsPageResponse {
code?: number;
msg?: string;
rows?: NosLawEnforcementFilingPageVO[];
total?: number;
[property: string]: any;
}
/**
* NosLawEnforcementFilingPageVO
*/
export interface NosLawEnforcementFilingPageVO {
/**
* 创建时间
*/
createTime?: string;
/**
* id
*/
id?: string;
/**
* 执法单位名称
*/
lawEnforcementAgency?: string;
/**
* 执法单位id
*/
lawEnforcementAgencyId?: string;
/**
* 法制材料
*/
legalMaterials?: string;
/**
* 技术材料
*/
technicalMaterials?: string;
[property: string]: any;
}
// LegalTechRecordsDetailResponse
/**
* 备案详情
*
* RNosLawEnforcementFilingDetailVO
*/
export interface LegalTechRecordsDetailResponse {
code?: number;
data?: NosLawEnforcementFilingDetailVO;
errorCode?: number;
message?: string;
msg?: string;
success?: boolean;
[property: string]: any;
}
/**
* NosLawEnforcementFilingDetailVO
*/
export interface NosLawEnforcementFilingDetailVO {
/**
* 创建时间
*/
createTime?: string;
/**
* id
*/
id?: string;
/**
* 执法单位名称
*/
lawEnforcementAgency?: string;
/**
* 执法单位id
*/
lawEnforcementAgencyId?: string;
/**
* 法制材料
*/
legalMaterials?: string;
/**
* 技术材料
*/
technicalMaterials?: string;
[property: string]: any;
}
/**
* 执法单位选项
*/
export interface LawEnforcementAgencyOption {
deptId: string;
deptName: string;
parentId?: string;
parentName?: string | null;
ancestors?: string;
orderNum?: number;
leaderName?: string | null;
status?: string;
zone?: number;
[property: string]: any;
}
/**
* 参数
*
* NosLawEnforcementFilingBo
*/
export interface SaveOrEditLegalTechRecordsRequest {
/**
* id
*/
id?: string;
/**
* 执法单位名称
*/
lawEnforcementAgency: string;
/**
* 执法单位id
*/
lawEnforcementAgencyId: string;
/**
* 法制材料
*/
legalMaterials: string;
/**
* 技术材料
*/
technicalMaterials: string;
[property: string]: any;
}
/**
* 结果
*
* RBoolean
*/
export interface SaveOrEditLegalTechRecordsResponse {
code?: number;
data?: boolean;
errorCode?: number;
message?: string;
msg?: string;
success?: boolean;
[property: string]: any;
}

BIN
zdxt-web-client/zdxt-admin-nos-plus-ui/src/assets/images/eval-bad-dis.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

BIN
zdxt-web-client/zdxt-admin-nos-plus-ui/src/assets/images/eval-bad.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
zdxt-web-client/zdxt-admin-nos-plus-ui/src/assets/images/eval-good-dis.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

BIN
zdxt-web-client/zdxt-admin-nos-plus-ui/src/assets/images/eval-good.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
zdxt-web-client/zdxt-admin-nos-plus-ui/src/assets/images/eval-verygood-dis.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

BIN
zdxt-web-client/zdxt-admin-nos-plus-ui/src/assets/images/eval-verygood.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

57
zdxt-web-client/zdxt-admin-nos-plus-ui/src/router/index.ts

@ -84,33 +84,74 @@ export const constantRoutes: RouteRecordRaw[] = [
} }
] ]
}, },
// 非现场执法列表
{ {
path: '/statistics/non-administrative/non-administrative-list', path: '/non/non-enforcement',
component: Layout, component: Layout,
hidden: true, hidden: true,
redirect: 'noredirect',
children: [ children: [
{ {
path: '', path: '',
component: () => import('@/views/nos/index.vue'), component: () => import('@/views/nos/index.vue'),
name: 'nosList', name: 'NosList',
meta: { title: '非现场执法', icon: 'user' } meta: { title: '非现场执法', icon: 'plus' }
} }
] ]
}, },
// 详情
{ {
path: '/statistics/non-administrative/non-administrative-detail', path: '/non/non-enforcement/detail/:id',
component: Layout, component: Layout,
hidden: true, hidden: true,
children: [ children: [
{ {
path: '', path: '',
component: () => import('@/views/nos/detail.vue'), component: () => import('@/views/nos/detail.vue'),
name: 'nosDetail', name: 'NosDetail',
meta: { title: '非现场执法详情', icon: 'user' } meta: { title: '非现场执法详情', icon: 'plus' }
} }
] ]
} },
// 法制与技术材料留存
{
path: '/non/legal-tech-records',
component: Layout,
hidden: true,
children: [
{
path: '',
component: () => import('@/views/nos/legalTech.vue'),
name: 'LegalTech',
meta: { title: '法制与技术材料留存', icon: 'plus' }
}
]
},
{
path: '/non/legal-tech-records/add',
component: Layout,
hidden: true,
children: [
{
path: '',
component: () => import('@/views/nos/legalTechEdit.vue'),
name: 'LegalTechAdd',
meta: { title: '法制与技术材料留存新增', icon: 'plus' }
}
]
},
{
path: '/non/legal-tech-records/edit/:id',
component: Layout,
hidden: true,
children: [
{
path: '',
component: () => import('@/views/nos/legalTechEdit.vue'),
name: 'LegalTechEdit',
meta: { title: '法制与技术材料留存编辑', icon: 'plus' }
}
]
},
]; ];
// 动态路由,基于用户权限动态去加载 // 动态路由,基于用户权限动态去加载

1
zdxt-web-client/zdxt-admin-nos-plus-ui/src/types/env.d.ts

@ -9,6 +9,7 @@ interface ImportMetaEnv {
VITE_APP_TITLE: string; VITE_APP_TITLE: string;
VITE_APP_PORT: number; VITE_APP_PORT: number;
VITE_APP_BASE_API: string; VITE_APP_BASE_API: string;
VITE_APP_ENF_BASE_API?: string;
VITE_APP_BASE_URL: string; VITE_APP_BASE_URL: string;
VITE_APP_CONTEXT_PATH: string; VITE_APP_CONTEXT_PATH: string;
VITE_APP_MONITOR_ADMIN: string; VITE_APP_MONITOR_ADMIN: string;

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

@ -26,6 +26,9 @@ export const globalHeaders = () => {
axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'; axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8';
axios.defaults.headers['clientid'] = import.meta.env.VITE_APP_CLIENT_ID; axios.defaults.headers['clientid'] = import.meta.env.VITE_APP_CLIENT_ID;
// 创建 axios 实例 // 创建 axios 实例
/** 执法端等独立服务 baseURL,单请求传入 baseURL 可覆盖实例默认值 */
export const enfBaseURL = import.meta.env.VITE_APP_ENF_BASE_API;
const service = axios.create({ const service = axios.create({
baseURL: import.meta.env.VITE_APP_BASE_API, baseURL: import.meta.env.VITE_APP_BASE_API,
timeout: 50000, timeout: 50000,
@ -58,7 +61,7 @@ service.interceptors.request.use(
config.url = url; config.url = url;
} }
if (!isRepeatSubmit && (config.method === 'post' || config.method === 'put')) { if (!isRepeatSubmit && (config.method === 'post' || config.method === 'put') && !(config.data instanceof FormData)) {
const requestObj = { const requestObj = {
url: config.url, url: config.url,
data: typeof config.data === 'object' ? JSON.stringify(config.data) : config.data, data: typeof config.data === 'object' ? JSON.stringify(config.data) : config.data,

443
zdxt-web-client/zdxt-admin-nos-plus-ui/src/views/nos/add.vue

@ -0,0 +1,443 @@
<template>
<div class="add-carousel-container">
<div class="page-header">
<h1>新增普法服务</h1>
</div>
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px" class="form-container">
<el-form-item label="标题:" prop="title">
<el-input v-model="form.title" placeholder="请输入标题" class="form-input" />
</el-form-item>
<el-form-item label="发布部门:" prop="publishDeptIds">
<el-select
v-model="form.publishDeptIds"
multiple
filterable
collapse-tags
collapse-tags-tooltip
placeholder="请选择发布部门"
class="form-select"
>
<el-option
v-for="item in deptOptions"
:key="item.deptId"
:label="item.deptName"
:value="String(item.deptId)"
/>
</el-select>
</el-form-item>
<el-form-item label="内容发布在:" prop="targetPlatform">
<el-select v-model="form.targetPlatform" placeholder="请选择" class="form-select">
<el-option v-for="item in platformOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="内容分类:" prop="contentType">
<el-select v-model="form.contentType" placeholder="请选择" class="form-select" clearable>
<el-option v-for="item in contentTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="服务信息:" prop="content">
<div class="editor-wrapper">
<QuillEditor
ref="quillEditorRef"
v-model:content="form.content"
content-type="html"
:toolbar="toolbarOptions"
theme="snow"
class="quill-editor"
placeholder="请输入服务信息"
/>
</div>
</el-form-item>
<el-form-item label="附件:">
<div class="upload-area">
<el-upload
drag
multiple
:http-request="handleFileUpload"
:show-file-list="true"
:file-list="uploadFileList"
:on-remove="handleFileRemove"
accept=".pdf,.doc,.docx"
>
<div class="upload-box">
<div class="upload-icon">📁</div>
<p>点击或将文件拖拽到这里上传</p>
<p class="upload-tip">支持扩展名称:.pdf .word;文件大小:10MB以内</p>
</div>
</el-upload>
</div>
</el-form-item>
<el-form-item class="form-actions">
<el-button type="primary" @click="handlePreview">预览</el-button>
<el-button @click="handleCancel">取消</el-button>
<el-button type="info" @click="handleSaveDraft">保存草稿箱</el-button>
<el-button type="success" @click="handleSubmit">发布</el-button>
</el-form-item>
</el-form>
<MobilePreview
v-model:visible="previewVisible"
:title="form.title"
:dept-names="selectedDeptNames"
:content-type-label="selectedContentTypeLabel"
:content="previewContentHtml"
:attachments="previewAttachments"
/>
</div>
</template>
<script setup lang="ts">
import { computed, getCurrentInstance, onMounted, reactive, ref } from 'vue';
import type { ComponentInternalInstance } from 'vue';
import type { UploadFile, UploadRequestOptions, UploadUserFile } from 'element-plus';
import { ElMessage, ElForm, FormRules } from 'element-plus';
import { QuillEditor } from '@vueup/vue-quill';
import '@vueup/vue-quill/dist/vue-quill.snow.css';
import {
addEducationServices,
listPublishDeptOptions,
type EducationServicesForm,
type PublishDeptOption
} from '@/api/supervision/educationServices';
import { getDicts } from '@/api/system/dict/data';
import { useUserStore } from '@/store/modules/user';
import request from '@/utils/request';
import MobilePreview from './components/MobilePreview.vue';
interface AttachmentItem {
fileName: string;
uploadPath: string;
uploadPersonId: string;
uploadPersonName: string;
}
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const userStore = useUserStore();
const formRef = ref<InstanceType<typeof ElForm>>();
const quillEditorRef = ref<InstanceType<typeof QuillEditor>>();
const previewVisible = ref(false);
const previewContentHtml = ref('');
const uploadFileList = ref<UploadUserFile[]>([]);
const attachmentList = ref<AttachmentItem[]>([]);
const platformOptions = ref<{ label: string; value: number }[]>([
{ label: '企业端', value: 3 },
{ label: '执法端', value: 2 },
{ label: '公众端', value: 0 }
]);
const contentTypeOptions = ref<{ label: string; value: number }[]>([]);
const deptOptions = ref<PublishDeptOption[]>([]);
const rules: FormRules = {
title: [{ required: true, message: '请输入标题', trigger: 'blur' }],
publishDeptIds: [{ required: true, message: '请选择发布部门', trigger: 'change', type: 'array', min: 1 }],
targetPlatform: [{ required: true, message: '请选择内容发布到', trigger: 'change' }],
content: [{ required: true, message: '请输入服务信息', trigger: 'blur' }]
};
const toolbarOptions = [
['bold', 'italic', 'underline', 'strike'],
['blockquote', 'code-block'],
[{ header: 1 }, { header: 2 }],
[{ list: 'ordered' }, { list: 'bullet' }],
[{ indent: '-1' }, { indent: '+1' }],
[{ size: ['small', false, 'large', 'huge'] }],
[{ header: [1, 2, 3, 4, 5, 6, false] }],
[{ color: [] }, { background: [] }],
[{ align: [] }],
['link', 'image', 'video'],
['clean']
];
const form = reactive({
title: '',
publishDeptIds: [] as string[],
targetPlatform: undefined as number | undefined,
contentType: undefined as number | undefined,
content: ''
});
const selectedDeptNames = computed(() => {
return deptOptions.value
.filter((item) => form.publishDeptIds.includes(String(item.deptId)))
.map((item) => item.deptName)
.join(',');
});
const selectedContentTypeLabel = computed(() => {
const item = contentTypeOptions.value.find((option) => option.value === form.contentType);
return item?.label || '';
});
const previewAttachments = computed(() => {
return attachmentList.value.map((item) => ({ fileName: item.fileName }));
});
const buildPayload = (status: number): EducationServicesForm => ({
title: form.title,
publishDeptIds: form.publishDeptIds.join(','),
targetPlatform: form.targetPlatform,
contentType: form.contentType,
content: form.content,
sourceType: 1,
status,
fileList: attachmentList.value
});
const submitForm = async (status: number, successMessage: string) => {
if (!formRef.value) return;
const isValid = await formRef.value.validate().catch(() => false);
if (!isValid) return;
try {
const res: any = await addEducationServices(buildPayload(status));
if (res.code === 200) {
ElMessage.success(successMessage);
proxy?.$tab.closePage();
} else {
ElMessage.error(res.msg || '操作失败');
}
} catch (error) {
console.error('提交失败:', error);
ElMessage.error('操作失败,请重试');
}
};
const getEditorHtml = () => {
const html = quillEditorRef.value?.getHTML?.();
if (html) {
form.content = html;
return html;
}
return form.content;
};
const isEmptyEditorContent = (html: string) => {
return !html
.replace(/<[^>]+>/g, '')
.replace(/&nbsp;/g, ' ')
.trim();
};
const handlePreview = () => {
const html = getEditorHtml();
if (!form.title && isEmptyEditorContent(html)) {
ElMessage.warning('请先填写标题或服务信息');
return;
}
previewContentHtml.value = html;
previewVisible.value = true;
};
const handleCancel = () => {
proxy?.$tab.closePage();
};
const handleSaveDraft = () => {
submitForm(1, '已保存到草稿箱');
};
const handleSubmit = () => {
submitForm(2, '发布成功');
};
const handleFileUpload = async (options: UploadRequestOptions) => {
const formData = new FormData();
formData.append('file', options.file);
try {
const res: any = await request({
url: '/app-enf/common/file/upload',
method: 'post',
data: formData,
headers: { 'Content-Type': 'multipart/form-data' }
});
if (res.code !== 200 || !res.data) {
throw new Error(res.msg || '上传失败');
}
attachmentList.value.push({
fileName: options.file.name,
uploadPath: res.data,
uploadPersonId: String(userStore.userId || ''),
uploadPersonName: userStore.nickname || ''
});
uploadFileList.value.push({
name: options.file.name,
url: res.data,
uid: options.file.uid
});
options.onSuccess?.(res);
} catch (error) {
console.error('附件上传失败:', error);
ElMessage.error('附件上传失败,请重试');
options.onError?.(error as any);
}
};
const handleFileRemove = (file: UploadFile) => {
const index = uploadFileList.value.findIndex((item) => item.uid === file.uid);
if (index > -1) {
uploadFileList.value.splice(index, 1);
attachmentList.value.splice(index, 1);
}
};
const loadDeptOptions = async () => {
try {
const res: any = await listPublishDeptOptions();
deptOptions.value = res.data || [];
} catch (error) {
console.error('加载发布部门失败:', error);
}
};
const loadDictOptions = async () => {
try {
const [contentRes, platformRes] = await Promise.all([
getDicts('web_content_classification'),
getDicts('web_publishing_platform')
]);
contentTypeOptions.value = (contentRes.data || [])
.map((item: any) => ({
label: item.dictLabel,
value: Number(item.dictValue)
}))
.filter((item) => !Number.isNaN(item.value));
const platformDict = (platformRes.data || [])
.map((item: any) => ({
label: item.dictLabel,
value: Number(item.dictValue)
}))
.filter((item) => !Number.isNaN(item.value) && item.value !== 1);
if (platformDict.length > 0) {
platformOptions.value = platformDict;
if (!platformOptions.value.some((item) => item.value === 0)) {
platformOptions.value.push({ label: '公众端', value: 0 });
}
}
} catch (error) {
console.error('加载字典失败:', error);
}
};
onMounted(() => {
loadDeptOptions();
loadDictOptions();
});
</script>
<style scoped>
.add-carousel-container {
padding: 20px;
background: #f5f7fa;
min-height: calc(100vh - 100px);
}
.page-header {
margin-bottom: 20px;
}
.page-header h1 {
font-size: 24px;
font-weight: bold;
color: #303133;
}
.form-container {
background: #fff;
border-radius: 8px;
padding: 24px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.form-input,
.form-select {
width: 100%;
max-width: 500px;
}
.editor-wrapper {
width: 100%;
max-width: 900px;
border: 1px solid #dcdfe6;
border-radius: 4px;
overflow: hidden;
}
.quill-editor {
height: 500px;
}
:deep(.ql-container) {
min-height: 500px;
height: 100%;
}
:deep(.ql-editor) {
min-height: 450px;
}
.upload-area {
width: 100%;
max-width: 500px;
}
:deep(.el-upload-dragger) {
padding: 0;
border: none;
background: transparent;
}
.upload-box {
border: 2px dashed #d9d9d9;
border-radius: 8px;
padding: 40px;
text-align: center;
transition: all 0.3s;
}
.upload-box:hover {
border-color: #409eff;
background: #f0f5ff;
}
.upload-icon {
font-size: 48px;
color: #409eff;
margin-bottom: 16px;
}
.upload-box p {
color: #606266;
margin: 8px 0;
}
.upload-tip {
font-size: 12px;
color: #909399 !important;
}
.form-actions {
display: flex;
justify-content: flex-end;
gap: 12px;
padding-top: 20px;
border-top: 1px solid #ebeef5;
}
:deep(.el-form-item__label) {
font-weight: bold;
white-space: nowrap;
}
</style>

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

@ -13,107 +13,113 @@
<span class="case-code-value">{{ detailData?.taskNo || '--' }}</span> <span class="case-code-value">{{ detailData?.taskNo || '--' }}</span>
</div> </div>
<!-- 通知/公告 --> <div class="detail-info-columns">
<div class="section"> <div class="detail-info-left">
<div class="section-title">通知/公告</div> <!-- 通知/公告 -->
<div class="detail-row"> <div class="section">
<span class="detail-label">通知/公告</span> <div class="section-title">通知/公告</div>
<div class="detail-value"> <div class="detail-row">
<template v-if="(detailData?.proclamation || []).length"> <span class="detail-label">通知/公告</span>
<div v-for="p in detailData?.proclamation || []" :key="p.fileName" class="detail-value--notice"> <div class="detail-value">
<span class="notice-file"> <template v-if="(detailData?.proclamation || []).length">
<el-icon class="notice-icon"><Paperclip /></el-icon> <div v-for="p in detailData?.proclamation || []" :key="p.fileName" class="detail-value--notice">
<span class="notice-name" :title="p.fileName">{{ p.fileName }}</span> <span class="notice-file">
</span> <el-icon class="notice-icon"><Paperclip /></el-icon>
<el-button v-if="p.filePath" size="small" @click="downloadFile(p.filePath, p.fileName)">下载</el-button> <span class="notice-name" :title="p.fileName">{{ p.fileName }}</span>
</span>
<el-button v-if="p.filePath" size="small" @click="downloadFile(p.filePath, p.fileName)">下载</el-button>
</div>
</template>
<span v-else class="detail-empty">--</span>
</div> </div>
</template> </div>
<span v-else class="detail-empty">--</span> <div class="detail-row">
<span class="detail-label">任务来源</span>
<span class="detail-value">{{ detailData?.taskSourceName || '--' }}</span>
</div>
</div> </div>
</div>
<div class="detail-row">
<span class="detail-label">任务来源</span>
<span class="detail-value">{{ detailData?.taskSourceName || '--' }}</span>
</div>
</div>
<!-- 检查对象 --> <!-- 检查对象 -->
<div class="section"> <div class="section">
<div class="section-title">检查对象</div> <div class="section-title">检查对象</div>
<div class="detail-row"> <div class="detail-row">
<span class="detail-label">执法单位</span> <span class="detail-label">{{detailData?.taskTypeId != 5 ? '执法单位' : '巡查单位'}}</span>
<span class="detail-value">{{ detailData?.lawEnforcementAgency || '--' }}</span> <span class="detail-value">{{ detailData?.lawEnforcementAgency || '--' }}</span>
</div> </div>
<div class="detail-row"> <div class="detail-row">
<span class="detail-label">执法人员</span> <span class="detail-label">{{detailData?.taskTypeId != 5 ? '执法人员' : '参与人员'}}</span>
<div class="detail-value"> <div class="detail-value">
<template v-if="(detailData?.lawEnforcer || []).length"> <template v-if="(detailData?.lawEnforcer || []).length">
<p v-for="(person, index) in detailData?.lawEnforcer || []" :key="index" class="detail-text"> <p v-for="(person, index) in detailData?.lawEnforcer || []" :key="index" class="detail-text">
{{ formatLawEnforcerName(person) }} {{ formatLawEnforcerName(person) }}
</p> </p>
</template> </template>
<span v-else class="detail-empty">--</span> <span v-else class="detail-empty">--</span>
</div>
</div>
</div> </div>
</div> </div>
</div>
<!-- 执法信息 --> <!-- 执法信息 -->
<div class="section"> <div class="detail-info-right">
<div class="section-title">执法信息</div> <div class="section">
<div class="detail-row"> <div class="section-title">{{detailData?.taskTypeId != 5 ? '执法信息' : '巡查信息'}}</div>
<span class="detail-label">企业名称</span> <div class="detail-row">
<div class="detail-value"> <span class="detail-label">企业名称</span>
<p v-if="detailData?.enterpriseName || detailData?.usciCode" class="detail-text"> <div class="detail-value">
{{ formatEnterpriseName(detailData?.enterpriseName, detailData?.usciCode) }} <p v-if="detailData?.enterpriseName || detailData?.usciCode" class="detail-text">
</p> {{ formatEnterpriseName(detailData?.enterpriseName, detailData?.usciCode) }}
<span v-else class="detail-empty">--</span> </p>
</div> <span v-else class="detail-empty">--</span>
</div> </div>
<div class="detail-row"> </div>
<span class="detail-label">类  别</span> <div class="detail-row">
<span class="detail-value">{{ detailData?.taskTypeName || '--' }}</span> <span class="detail-label">类  别</span>
</div> <span class="detail-value">{{ detailData?.taskTypeName || '--' }}</span>
<div class="detail-row"> </div>
<span class="detail-label">执法事项</span> <div class="detail-row" v-if="detailData?.taskTypeId == 5 || detailData?.taskTypeId == 1">
<span class="detail-value">{{ detailData?.lawEnforcementMatters || '--' }}</span> <span class="detail-label">{{detailData?.taskTypeId != 5 ? '执法事项' : '巡查事项'}}</span>
</div> <span class="detail-value">{{ detailData?.lawEnforcementMatters || '--' }}</span>
<div v-if="requirementLabel" class="detail-row"> </div>
<span class="detail-label">{{ requirementLabel }}</span> <div v-if="requirementLabel" class="detail-row">
<div class="detail-value"> <span class="detail-label">{{ requirementLabel }}</span>
<ul v-if="(detailData?.taskItemList || []).length" class="requirement-list"> <div class="detail-value">
<li v-for="item in detailData?.taskItemList || []" :key="item.id || item.itemName" class="requirement-item"> <ul v-if="(detailData?.taskItemList || []).length" class="requirement-list">
<el-icon class="requirement-icon"><CircleCheck /></el-icon> <li v-for="item in detailData?.taskItemList || []" :key="item.id || item.itemName" class="requirement-item">
<span>{{ item.itemName }}</span> <el-icon class="requirement-icon"><CircleCheck /></el-icon>
</li> <span>{{ item.itemName }}</span>
</ul> </li>
<span v-else class="detail-empty">--</span> </ul>
</div> <span v-else class="detail-empty">--</span>
</div> </div>
<div class="detail-row"> </div>
<span class="detail-label">执法依据</span> <div class="detail-row">
<div class="detail-value"> <span class="detail-label">{{detailData?.taskTypeId != 5 ? '执法依据' : '依  据'}}</span>
<template v-if="(detailData?.legalBasis || []).length"> <div class="detail-value">
<p v-for="(basis, index) in detailData?.legalBasis || []" :key="index" class="detail-text"> <template v-if="(detailData?.legalBasis || []).length">
{{ basis }} <p v-for="(basis, index) in detailData?.legalBasis || []" :key="index" class="detail-text">
</p> {{ basis }}
</template> </p>
<span v-else class="detail-empty">--</span> </template>
<span v-else class="detail-empty">--</span>
</div>
</div>
<div class="detail-row">
<span class="detail-label">开始时间</span>
<span class="detail-value">{{ formatDateTime(detailData?.createTime) || '--' }}</span>
</div>
<div class="detail-row">
<span class="detail-label">截止日期</span>
<span class="detail-value">{{ formatDateTime(detailData?.deadline) || '--' }}</span>
</div>
<!-- <div v-if="detailData?.taskStatusName" class="detail-row">
<span class="detail-label">任务状态</span>
<span class="detail-value">{{ detailData.taskStatusName }}</span>
</div> -->
</div> </div>
</div> </div>
<div class="detail-row">
<span class="detail-label">开始时间</span>
<span class="detail-value">{{ formatDateTime(detailData?.createTime) || '--' }}</span>
</div>
<div class="detail-row">
<span class="detail-label">截止日期</span>
<span class="detail-value">{{ formatDateTime(detailData?.deadline) || '--' }}</span>
</div>
<!-- <div v-if="detailData?.taskStatusName" class="detail-row">
<span class="detail-label">任务状态</span>
<span class="detail-value">{{ detailData.taskStatusName }}</span>
</div> -->
</div> </div>
<el-divider />
<!-- 企业材料 --> <!-- 企业材料 -->
<div class="section"> <div class="section">
<div class="section-title">企业材料</div> <div class="section-title">企业材料</div>
@ -122,10 +128,7 @@
<div v-for="item in detailData?.taskItemList || []" :key="item.id || item.itemName" class="material-block"> <div v-for="item in detailData?.taskItemList || []" :key="item.id || item.itemName" class="material-block">
<div class="material-title">{{ item.itemName || '--' }}</div> <div class="material-title">{{ item.itemName || '--' }}</div>
<div class="material-status">
<span class="status-label">审核:</span>
<span class="status-value">{{ getItemAuditText(item.pass) }}</span>
</div>
<div class="material-attachments"> <div class="material-attachments">
<div v-if="!(item.itemValue || []).length" class="detail-empty">暂无附件</div> <div v-if="!(item.itemValue || []).length" class="detail-empty">暂无附件</div>
@ -163,13 +166,79 @@
</template> </template>
</div> </div>
</div> </div>
<div class="material-status">
<span class="status-label">审核:</span>
<span class="status-value">{{ getItemAuditText(item.pass) }}</span>
</div>
</div>
</div>
</div>
<el-divider /> <el-divider />
<div class="section">
<div class="section-title">检查结果</div>
<div class="detail-row check-result-row">
<span class="detail-label">检查结果:</span>
<div class="detail-value">{{ getCheckResultText(detailData?.taskStatus) }}</div>
</div>
<div class="detail-row check-result-row">
<span class="detail-label">检查时间:</span>
<div class="detail-value">{{ detailData?.checkTime || '--' }}</div>
</div>
</div>
<el-divider />
<!-- 云笔录 -->
<div v-if="detailData?.digitalVoiceLoggingStatus !== 0" class="section">
<div class="section-title">云笔录</div>
<div class="detail-row">
<span class="detail-label">云视频:</span>
<div class="detail-value">
<template v-if="detailData?.cloudVideo">
<video :src="buildFileUrl(detailData.cloudVideo)" class="attachment-video" controls />
</template>
<span v-else class="detail-empty">暂无视频</span>
</div> </div>
</div> </div>
<div v-if="detailData?.checkResult !== undefined" class="detail-row"> <div class="detail-row">
<span class="detail-label">检查结果</span> <span class="detail-label">云文字:</span>
<span class="detail-value">{{ getCheckResultText(detailData?.checkResult) }}</span> <div class="detail-value">
<div class="detail-text">{{ detailData?.cloudText || '--' }}</div>
</div>
</div>
<div class="detail-row">
<span class="detail-label">意 见:</span>
<div class="detail-value">
<div class="detail-text">{{ detailData?.suggestion || '--' }}</div>
</div>
</div>
</div>
<!-- 评价 -->
<div v-if="detailData?.isEvaluation === 1" class="section ">
<div class="section-title">评价</div>
<div class="evaluate-content">
<div class="emoji-row">
<div class="emoji-item" :class="{ active: detailData?.satisfaction === 0 }">
<img class="emoji-img" :src="detailData?.satisfaction === 0 ? veryGoodImg : veryGoodDisImg" alt="满意" />
<span class="emoji-label">满意</span>
</div>
<div class="emoji-item" :class="{ active: detailData?.satisfaction === 1 }">
<img class="emoji-img" :src="detailData?.satisfaction === 1 ? goodImg : goodDisImg" alt="一般" />
<span class="emoji-label">一般</span>
</div>
<div class="emoji-item" :class="{ active: detailData?.satisfaction === 2 }">
<img class="emoji-img" :src="detailData?.satisfaction === 2 ? badImg : badDisImg" alt="不满意" />
<span class="emoji-label">不满意</span>
</div>
</div>
<div v-if="detailData?.satisfaction === 1 || detailData?.satisfaction === 2" class="detail-row reason-row">
<span class="detail-label-pj">原因:</span>
<div class="detail-value">
<div class="detail-text">{{ detailData?.commentReason || '--' }}</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -177,7 +246,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, getCurrentInstance, onMounted, ref } from 'vue'; import { computed, getCurrentInstance, ref, watch } from 'vue';
import { CircleCheck, Paperclip } from '@element-plus/icons-vue'; import { CircleCheck, Paperclip } from '@element-plus/icons-vue';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
@ -185,6 +254,13 @@ import { getTaskDetail } from '@/api/nos/index';
import type { LawEnforcer, NosLawEnforcementTaskDetailVO } from '@/api/nos/type'; import type { LawEnforcer, NosLawEnforcementTaskDetailVO } from '@/api/nos/type';
import { parseTime } from '@/utils/ruoyi'; import { parseTime } from '@/utils/ruoyi';
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 route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
@ -194,16 +270,21 @@ const loading = ref(false);
const CATEGORY_REQUIREMENT_LABEL_MAP: Record<string, string> = { const CATEGORY_REQUIREMENT_LABEL_MAP: Record<string, string> = {
行政检查: '检查要求', 行政检查: '检查要求',
材料备案: '备案文件', 材料备案: '备案文件',
材料验证: '验证文件' 材料验证: '验证文件',
巡查: '巡查要求'
}; };
const TASK_TYPE_TITLE_MAP: Record<string, string> = { const TASK_TYPE_TITLE_MAP: Record<string, string> = {
行政检查: '行政检查任务', 行政检查: '行政检查任务',
材料备案: '材料备案任务', 材料备案: '材料备案任务',
材料验证: '材料验证任务' 材料验证: '材料验证任务',
巡查: '巡查任务'
}; };
const taskId = computed(() => route.query.id); const taskId = computed(() => {
const id = route.params.id;
return Array.isArray(id) ? id[0] : id;
});
const pageTitle = computed(() => { const pageTitle = computed(() => {
const typeName = detailData.value?.taskTypeName || ''; const typeName = detailData.value?.taskTypeName || '';
@ -232,15 +313,17 @@ const formatEnterpriseName = (name?: string, usciCode?: string) => {
if (name && usciCode) return `${name}(${usciCode})`; if (name && usciCode) return `${name}(${usciCode})`;
return name || usciCode || '--'; return name || usciCode || '--';
}; };
const STATUS_OPTIONS = [
const getCheckResultText = (checkResult?: number) => { { text: "企业处理中", value: 1 },
const map: Record<number, string> = { { text: "退回企业补正", value: 2 },
0: '已完成', { text: "待审核", value: 3 },
1: '转为待核实处置案件', { text: "已办结", value: 4 },
2: '退回企业补正材料' { text: "已办结(超期)", value: 5 }
}; ]
if (checkResult === undefined || checkResult === null) return '--'; // 使用上面的类型
return map[checkResult] || '--'; const getCheckResultText = (taskStatus?: number) => {
const map = STATUS_OPTIONS.find(item => item.value === taskStatus)?.text || '--';
return map || '--';
}; };
const getItemAuditText = (pass?: number) => { const getItemAuditText = (pass?: number) => {
@ -386,9 +469,13 @@ const fetchDetail = async () => {
} }
}; };
onMounted(() => { watch(
fetchDetail(); taskId,
}); () => {
fetchDetail();
},
{ immediate: true }
);
</script> </script>
<style scoped> <style scoped>
@ -442,6 +529,23 @@ onMounted(() => {
font-weight: 500; font-weight: 500;
} }
.detail-info-columns {
display: flex;
gap: 32px;
align-items: flex-start;
}
.detail-info-left,
.detail-info-right {
flex: 1;
min-width: 0;
}
.detail-info-right {
border-left: 1px solid #ebeef5;
padding-left: 32px;
}
.section { .section {
margin-bottom: 8px; margin-bottom: 8px;
padding-bottom: 4px; padding-bottom: 4px;
@ -475,6 +579,13 @@ onMounted(() => {
color: #606266; color: #606266;
font-weight: 500; font-weight: 500;
} }
.detail-label-pj {
flex: none;
/* width: 88px; */
text-align: right;
color: #606266;
font-weight: 500;
}
.detail-value { .detail-value {
flex: 1; flex: 1;
@ -626,4 +737,58 @@ onMounted(() => {
gap: 12px; gap: 12px;
justify-content: flex-end; justify-content: flex-end;
} }
.evaluate-section {
border-top: 1px solid #ebeef5;
margin-top: 24px;
padding-top: 16px;
}
.evaluate-content {
padding-left: 32px;
}
.emoji-row {
display: flex;
justify-content: flex-start;
gap: 48px;
padding: 16px 0 24px;
}
.emoji-item {
display: flex;
flex-direction: column;
align-items: center;
opacity: 0.6;
&.active {
opacity: 1;
transform: scale(1.1);
.emoji-label {
color: #fca000;
font-weight: 600;
}
}
.emoji-img {
width: 48px;
height: 48px;
margin-bottom: 8px;
}
.emoji-label {
font-size: 13px;
color: #909399;
}
}
.reason-row {
border-bottom: none;
padding-bottom: 0;
}
.check-result-row {
border-bottom: none;
padding: 4px 0;
}
</style> </style>

43
zdxt-web-client/zdxt-admin-nos-plus-ui/src/views/nos/index.vue

@ -77,7 +77,7 @@
:header-cell-style="{ backgroundColor: '#f5f7fa', fontWeight: 'bold' }" :header-cell-style="{ backgroundColor: '#f5f7fa', fontWeight: 'bold' }"
@selection-change="handleSelectionChange" @selection-change="handleSelectionChange"
> >
<el-table-column type="selection" width="55" /> <!-- <el-table-column type="selection" width="55" /> -->
<el-table-column prop="taskTypeName" label="类别" width="110" show-overflow-tooltip /> <el-table-column prop="taskTypeName" label="类别" width="110" show-overflow-tooltip />
@ -157,7 +157,8 @@ const TASK_TYPE_OPTIONS = [
{ label: '行政检查', value: 1 }, { label: '行政检查', value: 1 },
{ label: '材料备案', value: 2 }, { label: '材料备案', value: 2 },
{ label: '材料验证', value: 3 }, { label: '材料验证', value: 3 },
{ label: '待核实案件', value: 4 } { label: '待核实案件', value: 4 },
{ label: '巡查', value: 5 }
]; ];
const TASK_STATUS_OPTIONS = [ const TASK_STATUS_OPTIONS = [
@ -201,13 +202,7 @@ const formatLawEnforcer = (row: NosLawEnforcementTaskPageVO) => {
}; };
const getStatusClass = (status?: number) => { const getStatusClass = (status?: number) => {
const classMap: Record<number, string> = { return status ? `list-card__status--${status}` : '';
2: 'status-return',
3: 'status-pending',
4: 'status-done',
5: 'status-done'
};
return classMap[status ?? -1] || 'status-default';
}; };
const buildQueryParams = () => { const buildQueryParams = () => {
@ -263,11 +258,9 @@ const handleDetail = (row: NosLawEnforcementTaskPageVO) => {
ElMessage.warning('任务ID不存在'); ElMessage.warning('任务ID不存在');
return; return;
} }
console.log('dddddddddddddddd');
router.push({ router.push({
name: 'nosDetail', name: 'NosDetail',
query: { id: row.id } params: { id: String(row.id) }
}); });
}; };
@ -338,19 +331,27 @@ onMounted(() => {
color: #909399; color: #909399;
} }
.status-default { .list-card__status--1 {
color: #303133; color: #1367fe; /* 企业处理中 */
}
.list-card__status--2 {
color: #ed6a0c; /* 退回企业补正 */
}
.list-card__status--3 {
color: #ed6a0c; /* 待审核 */
} }
.status-return { .list-card__status--4 {
color: #409eff; color: #07c160; /* 已办结 */
} }
.status-pending { .list-card__status--5 {
color: #e6a23c; color: #ee0a24; /* 已办结(超期) */
} }
.status-done { .list-card__status--6 {
color: #67c23a; color: #ed6a0c; /* 待现场处置 */
} }
</style> </style>

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

@ -0,0 +1,344 @@
<template>
<div class="legal-tech-container">
<!-- 搜索区域 -->
<div class="search-panel">
<el-form :model="searchForm" inline>
<el-form-item label="时间筛选:">
<el-date-picker
v-model="searchForm.timeRange"
type="datetimerange"
range-separator="至"
start-placeholder="开始时间"
end-placeholder="截止时间"
class="date-picker"
format="YYYY-MM-DD HH:mm"
value-format="YYYY-MM-DD HH:mm:ss"
/>
</el-form-item>
<el-form-item label="执法单位:">
<el-input
v-model="searchForm.lawEnforcementAgency"
placeholder="请输入关键字"
class="search-input"
clearable
@keyup.enter="handleSearch"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch" :loading="loading">搜索</el-button>
</el-form-item>
<el-form-item>
<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-column prop="lawEnforcementAgency" label="执法单位" min-width="160" show-overflow-tooltip />
<el-table-column label="法制材料" min-width="200">
<template #default="{ row }">
<div v-if="row.legalMaterials" class="file-cell">
<el-icon class="file-icon"><Paperclip /></el-icon>
<span class="file-name" :title="extractFileName(row.legalMaterials)">{{ extractFileName(row.legalMaterials) }}</span>
<el-button size="small" @click="downloadFile(row.legalMaterials)">下载</el-button>
</div>
<span v-else>--</span>
</template>
</el-table-column>
<el-table-column label="技术材料" min-width="200">
<template #default="{ row }">
<div v-if="row.technicalMaterials" class="file-cell">
<el-icon class="file-icon"><Paperclip /></el-icon>
<span class="file-name" :title="extractFileName(row.technicalMaterials)">{{ extractFileName(row.technicalMaterials) }}</span>
<el-button size="small" @click="downloadFile(row.technicalMaterials)">下载</el-button>
</div>
<span v-else>--</span>
</template>
</el-table-column>
<el-table-column label="提交时间" width="180">
<template #default="{ row }">
{{ formatDateTime(row.createTime) }}
</template>
</el-table-column>
<el-table-column label="操作" width="100" fixed="right">
<template #default="{ row }">
<el-button size="small" type="primary" link @click="handleEdit(row)">编辑</el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- 分页 -->
<div class="pagination-container">
<span class="total-text">共 {{ total }} 项数据</span>
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:total="total"
:page-sizes="[10, 20, 50, 100]"
layout="prev, pager, next, jumper, ->, total, sizes"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, getCurrentInstance } from 'vue';
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 { parseTime } from '@/utils/ruoyi';
const router = useRouter();
const { proxy } = getCurrentInstance() as any;
const loading = ref(false);
const tableData = ref<NosLawEnforcementFilingPageVO[]>([]);
const total = ref(0);
const currentPage = ref(1);
const pageSize = ref(10);
const searchForm = reactive({
timeRange: [] as string[],
lawEnforcementAgency: ''
});
const formatDateTime = (time?: string) => {
if (!time) return '--';
return parseTime(time, '{y}-{m}-{d} {h}:{i}') || '--';
};
const BASE_API = import.meta.env.VITE_APP_BASE_API || '';
const FILE_SERVER_PREFIX = 'http://139.186.148.21:8080';
const isOssId = (val: string) => /^[0-9]+$/.test(val.trim());
const toRelativeFilePath = (val: string) => {
const raw = val.trim();
if (!raw) return '';
const prefix = FILE_SERVER_PREFIX.replace(/\/+$/, '');
let path = raw.startsWith('/') ? raw : `/${raw}`;
const normalizedPrefix = prefix.startsWith('http') ? prefix : `/${prefix}`;
while (path === normalizedPrefix || path.startsWith(`${normalizedPrefix}/`)) {
path = path.slice(normalizedPrefix.length) || '/';
if (!path.startsWith('/')) path = `/${path}`;
}
return path;
};
const buildFileUrl = (val: string) => {
if (!val) return '';
const raw = val.trim();
if (raw.startsWith('http://') || raw.startsWith('https://') || raw.startsWith('blob:') || raw.startsWith('data:')) {
return raw;
}
if (isOssId(raw)) {
return `${BASE_API}/resource/oss/download/${raw}`;
}
const relative = toRelativeFilePath(raw);
return `${FILE_SERVER_PREFIX.replace(/\/+$/, '')}${relative}`;
};
const extractFileName = (val: string) => {
if (!val) return '';
const raw = val.trim();
if (isOssId(raw)) return raw;
try {
const withoutQuery = raw.split('?')[0].split('#')[0];
const parts = withoutQuery.split('/');
return parts[parts.length - 1] || raw;
} catch {
return raw;
}
};
const downloadFile = async (file: string, fileName?: string) => {
if (!file) return;
const raw = file.trim();
if (isOssId(raw) && proxy?.$download?.oss) {
proxy.$download.oss(raw);
return;
}
const url = buildFileUrl(raw);
if (!url) {
ElMessage.error('文件下载失败');
return;
}
const link = document.createElement('a');
link.href = url;
link.target = '_blank';
if (fileName) link.download = fileName;
link.rel = 'noopener noreferrer';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
const buildQueryParams = () => {
return {
startTime: searchForm.timeRange?.[0] || undefined,
endTime: searchForm.timeRange?.[1] || undefined,
lawEnforcementAgency: searchForm.lawEnforcementAgency || undefined,
pageNo: currentPage.value,
pageSize: pageSize.value
};
};
const getList = async () => {
loading.value = true;
try {
const res = (await getLegalTechRecordsPage(buildQueryParams()) as unknown) as LegalTechRecordsPageResponse;
tableData.value = res.rows || [];
total.value = res.total || 0;
} catch (error) {
console.error('获取列表数据失败:', error);
ElMessage.error('获取数据失败,请检查网络连接');
} finally {
loading.value = false;
}
};
const handleSearch = () => {
currentPage.value = 1;
getList();
};
const handleReset = () => {
searchForm.timeRange = [];
searchForm.lawEnforcementAgency = '';
currentPage.value = 1;
getList();
};
const handleAdd = () => {
router.push({ name: 'LegalTechAdd' });
};
const handleEdit = (row: NosLawEnforcementFilingPageVO) => {
if (!row.id) {
ElMessage.warning('记录ID不存在');
return;
}
router.push({
name: 'LegalTechEdit',
params: { id: row.id }
});
};
const handleSizeChange = (val: number) => {
pageSize.value = val;
currentPage.value = 1;
getList();
};
const handleCurrentChange = (val: number) => {
currentPage.value = val;
getList();
};
onMounted(() => {
getList();
});
</script>
<style scoped>
.legal-tech-container {
padding: 20px;
background: #f5f7fa;
min-height: calc(100vh - 100px);
}
.search-panel {
background: #fff;
padding: 16px 20px;
border-radius: 8px;
margin-bottom: 16px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.action-panel {
margin-bottom: 16px;
}
:deep(.el-form-item__label) {
white-space: nowrap;
margin-right: 8px;
}
.search-input {
width: 200px;
}
.date-picker {
width: 360px;
}
.table-container {
background: #fff;
border-radius: 8px;
padding: 16px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
margin-bottom: 16px;
}
.file-cell {
display: flex;
align-items: center;
gap: 8px;
}
.file-icon {
color: #909399;
}
.file-name {
flex: 1;
color: #409eff;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.pagination-container {
display: flex;
justify-content: space-between;
align-items: center;
background: #fff;
padding: 12px 20px;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.total-text {
color: #909399;
}
</style>

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

@ -0,0 +1,400 @@
<template>
<div class="legal-tech-edit-container" v-loading="loading">
<div class="page-header">
<h2>{{ isEdit ? '编辑法制与技术材料' : '新增法制与技术材料' }}</h2>
</div>
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px" class="form-container">
<el-form-item label="执法单位:" prop="lawEnforcementAgencyId">
<el-select
v-model="form.lawEnforcementAgencyId"
filterable
placeholder="请选择执法单位"
class="form-input"
@change="handleDeptChange"
>
<el-option
v-for="item in deptOptions"
:key="item.deptId"
:label="item.deptName"
:value="String(item.deptId)"
/>
</el-select>
<div class="form-tip">温馨提示:开展非现场执法,同1个单位,仅需提交1次法制和技术材料</div>
</el-form-item>
<div class="section-title">法制材料</div>
<el-form-item label="附件:" prop="legalMaterials">
<div class="upload-area">
<el-upload
drag
:http-request="(opts) => handleFileUpload(opts, 'legal')"
:show-file-list="true"
:file-list="legalFileList"
:on-remove="(file) => handleFileRemove(file, 'legal')"
accept=".pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx"
:limit="1"
:on-exceed="handleExceed"
>
<div class="upload-box">
<el-icon class="upload-icon"><FolderOpened /></el-icon>
<p>点击或将文件拖拽到这里上传</p>
<p class="upload-tip">文件格式:word、excel、ppt、PDF等文档</p>
</div>
</el-upload>
</div>
</el-form-item>
<div class="section-title">技术材料</div>
<el-form-item label="附件:" prop="technicalMaterials">
<div class="upload-area">
<el-upload
drag
:http-request="(opts) => handleFileUpload(opts, 'technical')"
:show-file-list="true"
:file-list="technicalFileList"
:on-remove="(file) => handleFileRemove(file, 'technical')"
accept=".pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx"
:limit="1"
:on-exceed="handleExceed"
>
<div class="upload-box">
<el-icon class="upload-icon"><FolderOpened /></el-icon>
<p>点击或将文件拖拽到这里上传</p>
<p class="upload-tip">文件格式:word、excel、ppt、PDF等文档</p>
</div>
</el-upload>
</div>
</el-form-item>
<el-form-item class="form-actions">
<el-button @click="handleCancel">取消</el-button>
<el-button type="primary" @click="handleSubmit" :loading="submitLoading">提交</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, computed } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { ElMessage, ElForm, FormRules } from 'element-plus';
import type { UploadFile, UploadRequestOptions, UploadUserFile } from 'element-plus';
import { FolderOpened } from '@element-plus/icons-vue';
import { getLegalTechRecordsDetail, saveOrEditLegalTechRecords, selectLawEnforcementAgency } from '@/api/nos/index';
import type { SaveOrEditLegalTechRecordsRequest, LawEnforcementAgencyOption } from '@/api/nos/type';
import request, { enfBaseURL } from '@/utils/request';
const route = useRoute();
const router = useRouter();
const formRef = ref<InstanceType<typeof ElForm>>();
const loading = ref(false);
const submitLoading = ref(false);
const recordId = computed(() => {
const id = route.params.id;
return Array.isArray(id) ? id[0] : id;
});
const isEdit = computed(() => !!recordId.value);
const form = reactive({
lawEnforcementAgency: '',
lawEnforcementAgencyId: '',
legalMaterials: '',
technicalMaterials: ''
});
const deptOptions = ref<LawEnforcementAgencyOption[]>([]);
const legalFileList = ref<UploadUserFile[]>([]);
const technicalFileList = ref<UploadUserFile[]>([]);
const rules: FormRules = {
lawEnforcementAgencyId: [{ required: true, message: '请选择执法单位', trigger: 'change' }],
legalMaterials: [{ required: true, message: '请上传法制材料附件', trigger: 'change' }],
technicalMaterials: [{ required: true, message: '请上传技术材料附件', trigger: 'change' }]
};
const handleDeptChange = (deptId: string) => {
const selected = deptOptions.value.find((item) => String(item.deptId) === String(deptId));
form.lawEnforcementAgency = selected?.deptName || '';
};
const loadDeptOptions = async () => {
try {
const res: any = await selectLawEnforcementAgency();
deptOptions.value = res.data || [];
} catch (error) {
console.error('加载执法单位失败:', error);
ElMessage.error('加载执法单位失败');
}
};
const handleExceed = () => {
ElMessage.warning('只能上传一个文件,请先删除已上传的文件');
};
const handleFileUpload = async (options: UploadRequestOptions, type: 'legal' | 'technical') => {
const formData = new FormData();
formData.append('file', options.file);
try {
const res: any = await request({
baseURL: enfBaseURL,
url: '/app-enf/common/file/upload',
method: 'post',
data: formData,
headers: { 'Content-Type': 'multipart/form-data' }
});
if (res.code !== 200 || !res.data) {
throw new Error(res.msg || '上传失败');
}
const fileUrl = res.data;
const fileObj = {
name: options.file.name,
url: fileUrl,
uid: options.file.uid
};
if (type === 'legal') {
form.legalMaterials = fileUrl;
legalFileList.value = [fileObj];
formRef.value?.validateField('legalMaterials');
} else {
form.technicalMaterials = fileUrl;
technicalFileList.value = [fileObj];
formRef.value?.validateField('technicalMaterials');
}
options.onSuccess?.(res);
ElMessage.success('上传成功');
} catch (error) {
console.error('附件上传失败:', error);
ElMessage.error('附件上传失败,请重试');
options.onError?.(error as any);
}
};
const handleFileRemove = (file: UploadFile, type: 'legal' | 'technical') => {
if (type === 'legal') {
form.legalMaterials = '';
legalFileList.value = [];
formRef.value?.validateField('legalMaterials');
} else {
form.technicalMaterials = '';
technicalFileList.value = [];
formRef.value?.validateField('technicalMaterials');
}
};
const fetchDetail = async () => {
if (!isEdit.value) return;
loading.value = true;
try {
const res: any = await getLegalTechRecordsDetail(recordId.value);
const data = res.data;
if (data) {
form.lawEnforcementAgency = data.lawEnforcementAgency || '';
form.lawEnforcementAgencyId = data.lawEnforcementAgencyId ? String(data.lawEnforcementAgencyId) : '';
form.legalMaterials = data.legalMaterials || '';
form.technicalMaterials = data.technicalMaterials || '';
if (form.legalMaterials) {
legalFileList.value = [{
name: extractFileName(form.legalMaterials),
url: form.legalMaterials,
uid: Date.now()
}];
}
if (form.technicalMaterials) {
technicalFileList.value = [{
name: extractFileName(form.technicalMaterials),
url: form.technicalMaterials,
uid: Date.now() + 1
}];
}
}
} catch (error) {
console.error('获取详情失败:', error);
ElMessage.error('获取详情失败');
} finally {
loading.value = false;
}
};
const extractFileName = (val: string) => {
if (!val) return '';
const raw = val.trim();
const isOssId = /^[0-9]+$/.test(raw);
if (isOssId) return raw;
try {
const withoutQuery = raw.split('?')[0].split('#')[0];
const parts = withoutQuery.split('/');
return parts[parts.length - 1] || raw;
} catch {
return raw;
}
};
const handleCancel = () => {
router.back();
};
const handleSubmit = async () => {
if (!formRef.value) return;
const isValid = await formRef.value.validate().catch(() => false);
if (!isValid) return;
submitLoading.value = true;
try {
const payload: SaveOrEditLegalTechRecordsRequest = {
lawEnforcementAgency: form.lawEnforcementAgency,
lawEnforcementAgencyId: form.lawEnforcementAgencyId,
legalMaterials: form.legalMaterials,
technicalMaterials: form.technicalMaterials
};
if (isEdit.value) {
payload.id = recordId.value;
}
const res: any = await saveOrEditLegalTechRecords(payload);
if (res.code === 200) {
ElMessage.success(isEdit.value ? '编辑成功' : '新增成功');
router.back();
} else {
ElMessage.error(res.msg || '操作失败');
}
} catch (error) {
console.error('提交失败:', error);
ElMessage.error('操作失败,请重试');
} finally {
submitLoading.value = false;
}
};
onMounted(async () => {
await loadDeptOptions();
fetchDetail();
});
</script>
<style scoped>
.legal-tech-edit-container {
padding: 20px;
background: #f5f7fa;
min-height: calc(100vh - 100px);
}
.page-header {
margin-bottom: 20px;
}
.page-header h2 {
font-size: 20px;
font-weight: bold;
color: #303133;
margin: 0;
}
.form-container {
background: #fff;
border-radius: 8px;
padding: 32px 24px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.form-input {
width: 100%;
max-width: 600px;
}
.form-tip {
font-size: 12px;
color: #909399;
margin-top: 8px;
line-height: 1.4;
}
.section-title {
font-size: 16px;
font-weight: bold;
color: #303133;
margin: 24px 0 16px;
padding-left: 12px;
position: relative;
}
.section-title::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 4px;
height: 16px;
background-color: #409eff;
border-radius: 2px;
}
.upload-area {
width: 100%;
max-width: 400px;
}
:deep(.el-upload-dragger) {
padding: 0;
border: none;
background: transparent;
}
.upload-box {
border: 1px dashed #d9d9d9;
border-radius: 4px;
padding: 30px 20px;
text-align: center;
transition: all 0.3s;
background: #fafafa;
}
.upload-box:hover {
border-color: #409eff;
background: #f0f5ff;
}
.upload-icon {
font-size: 40px;
color: #409eff;
margin-bottom: 12px;
}
.upload-box p {
color: #606266;
margin: 8px 0;
font-size: 14px;
}
.upload-tip {
font-size: 12px !important;
color: #909399 !important;
}
.form-actions {
display: flex;
justify-content: flex-end;
gap: 12px;
padding-top: 32px;
margin-top: 16px;
border-top: 1px solid #ebeef5;
}
:deep(.el-form-item__label) {
font-weight: bold;
}
</style>

2
zdxt-web-client/zdxt-efcode-nos-enforcement-app/.prettierrc

@ -2,7 +2,7 @@
"printWidth": 150, "printWidth": 150,
"tabWidth": 2, "tabWidth": 2,
"useTabs": false, "useTabs": false,
"semi": true, "semi": false,
"singleQuote": true, "singleQuote": true,
"quoteProps": "preserve", "quoteProps": "preserve",
"jsxSingleQuote": false, "jsxSingleQuote": false,

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

@ -22,3 +22,14 @@ export const nosTaskSourceListApi = (params, success, error) => _post("/app-enf/
//查询已评价的任务 //查询已评价的任务
export const nosTaskHasCommentPageApi = (params, success, error) => _post("/app-enf/nos-law-enforcement-task/has-comment/page", 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);

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

@ -27,6 +27,15 @@ const routes = [
isKeepAlive: false, isKeepAlive: false,
}, },
}, },
// 云笔录
{
path: "/enforcement/nos/cloud-record",
name: "nosCloudRecord",
component: () => import("@/views/enforcement/nos/nosCloudRecord.vue"),
meta: {
isKeepAlive: false,
},
},
// 非现场消息 // 非现场消息
{ {
path: "/enforcement/nos/evaluate", path: "/enforcement/nos/evaluate",

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

@ -135,6 +135,21 @@
</div> </div>
</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>
</div>
<div class="evaluate-card" v-if="Number(taskStatus) === 4 && evaluateData"> <div class="evaluate-card" v-if="Number(taskStatus) === 4 && evaluateData">
<div class="card-title">评价</div> <div class="card-title">评价</div>
@ -240,6 +255,12 @@ const props = defineProps({
} }
}); });
const emit = defineEmits(['view-cloud-record']);
const onViewCloudRecord = () => {
emit('view-cloud-record');
};
const overdueCheckTime = computed(() => props.taskDeadline || ''); const overdueCheckTime = computed(() => props.taskDeadline || '');
const evaluateForm = ref({ const evaluateForm = ref({
@ -260,6 +281,7 @@ watch(() => props.evaluateData, (val) => {
}, { immediate: true, deep: true }); }, { immediate: true, deep: true });
const taskItemList = defineModel('taskItemList', { type: Array, default: () => [] }); const taskItemList = defineModel('taskItemList', { type: Array, default: () => [] });
const digitalVoiceLoggingStatus = defineModel('digitalVoiceLoggingStatus', { type: [Number, String], default: '' });
const inspectResult = defineModel('inspectResult', { const inspectResult = defineModel('inspectResult', {
type: Object, type: Object,
default: () => ({ default: () => ({
@ -637,4 +659,29 @@ const onPreview = (url, itemValue) => {
} }
} }
} }
.cloud-record-card {
background: #fff;
margin-top: 8px;
padding: 14px 16px;
border-radius: 4px;
}
.cloud-record-row {
display: flex;
align-items: center;
justify-content: space-between;
}
.cloud-record-label {
font-size: 15px;
font-weight: 600;
color: #323233;
}
.btn-view {
height: 28px;
padding: 0 12px;
font-size: 12px;
}
</style> </style>

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

@ -0,0 +1,381 @@
<template>
<div class="cloud-record-page app_content">
<van-nav-bar title="执法监督码" left-arrow @click-left="onBack" />
<div class="cloud-record-body">
<van-tabs
v-model:active="activeTab"
class="cloud-record-tabs"
color="#2b7de9"
title-active-color="#2b7de9"
title-inactive-color="#646566"
line-width="50%"
>
<van-tab title="云笔录" name="cloud">
<van-pull-refresh v-model="refreshing" class="tab-scroll" @refresh="onRefresh">
<div v-if="loading && !refreshing" class="loading-wrap">
<van-loading type="spinner" color="#2b7de9" />
</div>
<template v-else-if="!hasRecord">
<div class="empty-state">
<van-image :src="noDataImg" width="151" height="120" />
<div class="empty-text">{{ isViewOnly ? '暂无云笔录' : '等待企业处理' }}</div>
</div>
</template>
<template v-else>
<div class="content-scroll">
<div class="section-card">
<div class="section-title">云视频</div>
<div v-if="cloudVideo" class="video-wrap" @click="showVideoPreview = true">
<video :src="cloudVideo" muted preload="metadata" class="video-thumb" />
<van-icon name="play-circle-o" class="play-icon" />
</div>
<div v-else class="media-placeholder">暂无视频</div>
</div>
<div class="section-card">
<div class="section-title">云文字</div>
<div class="text-content">{{ cloudText || '暂无文字内容' }}</div>
</div>
<div class="section-card">
<div class="section-title">意见</div>
<div v-if="isViewOnly" class="text-content">{{ suggestion || '暂无意见' }}</div>
<van-field
v-else
v-model="suggestion"
class="opinion-field"
type="textarea"
rows="5"
autosize
maxlength="300"
show-word-limit
placeholder="请输入"
:readonly="submitting"
/>
</div>
</div>
</template>
</van-pull-refresh>
</van-tab>
</van-tabs>
</div>
<div v-if="hasRecord && canEditOpinion" class="page-footer">
<van-button class="btn-cancel" :disabled="submitting" @click="onBack">取消</van-button>
<van-button class="btn-submit" type="primary" :loading="submitting" @click="onSubmit"> 提交 </van-button>
</div>
<van-popup v-model:show="showVideoPreview" class="video-preview-popup" teleport="body" closeable @closed="showVideoPreview = false">
<video v-if="cloudVideo" :src="cloudVideo" controls autoplay playsinline webkit-playsinline class="preview-video" />
</van-popup>
</div>
</template>
<script setup name="nosCloudRecord">
import { showConfirmDialog, showSuccessToast, showToast } from 'vant'
import noDataImg from '@/assets/images/nos/no-data.png'
import { cloudRecordDetailApi, cloudRecordConfirmApi } from '@/api/nos/enfDemo'
import { toFullFileUrl } from '@/util/fileUrl'
const router = useRouter()
const route = useRoute()
// type:云笔录状态(0-未发起,1-待企业提交,2-待执法人员确认,3-已完成)
const cloudRecordStatus = computed(() => Number(route.query.type) || 0)
const isViewOnly = computed(() => Number(route.query.taskStatus) === 4 || cloudRecordStatus.value === 3)
const canEditOpinion = computed(() => !isViewOnly.value && cloudRecordStatus.value === 2)
const activeTab = ref('cloud')
const loading = ref(true)
const refreshing = ref(false)
const submitting = ref(false)
const hasRecord = ref(false)
const cloudVideo = ref('')
const cloudText = ref('')
const suggestion = ref('')
const showVideoPreview = ref(false)
const recordId = ref('')
const applyDetailData = (data) => {
cloudVideo.value = data.cloudVideo ? toFullFileUrl(data.cloudVideo) : ''
cloudText.value = data.cloudText || ''
suggestion.value = data.suggestion || ''
hasRecord.value = !!(data.cloudVideo || data.cloudText)
recordId.value = data.id || ''
}
const fetchDetail = async () => {
const taskId = route.query.id
if (!taskId) {
showToast('任务信息异常')
return
}
try {
const res = await cloudRecordDetailApi({ taskId })
if (res && res.code === 200) {
if (res.data) {
applyDetailData(res.data)
} else {
hasRecord.value = false
cloudVideo.value = ''
cloudText.value = ''
suggestion.value = ''
}
} else {
showToast(res?.msg || '查询云笔录失败')
}
} catch (err) {
console.error(err)
showToast('查询云笔录失败')
} finally {
loading.value = false
refreshing.value = false
}
}
const onRefresh = () => {
fetchDetail()
}
const onBack = () => {
router.back()
}
const onSubmit = async () => {
if (!suggestion.value.trim()) {
showToast('请输入意见')
return
}
try {
await showConfirmDialog({
title: '确认提交',
message: '确认提交云笔录意见吗?',
width: '80%'
})
submitting.value = true
const res = await cloudRecordConfirmApi({
taskId: route.query.id,
id: recordId.value,
suggestion: suggestion.value.trim()
})
if (res && res.code === 200) {
showSuccessToast('提交成功')
router.back()
} else {
showToast(res?.msg || '提交失败')
}
} catch (err) {
if (err !== 'cancel') {
console.error(err)
showToast('提交失败')
}
} finally {
submitting.value = false
}
}
onMounted(() => {
fetchDetail()
})
</script>
<style lang="less" scoped>
.cloud-record-page {
display: flex;
flex-direction: column;
height: 100%;
background: #f5f7fa;
overflow: hidden;
}
.cloud-record-body {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
background: #fff;
}
.cloud-record-tabs {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
:deep(.van-tabs__wrap) {
flex-shrink: 0;
border-bottom: 1px solid #eee;
}
:deep(.van-tabs__content) {
flex: 1;
min-height: 0;
}
:deep(.van-tab__panel) {
height: 100%;
}
:deep(.van-tab) {
flex: 1;
justify-content: center;
}
}
.tab-scroll {
height: 100%;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
background: #f5f7fa;
}
.loading-wrap {
display: flex;
justify-content: center;
padding-top: 120px;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding-top: 120px;
.empty-text {
margin-top: 16px;
font-size: 16px;
color: #323233;
font-weight: 500;
}
}
.content-scroll {
padding: 12px;
}
.section-card {
background: #fff;
border-radius: 8px;
padding: 12px;
margin-bottom: 12px;
}
.section-title {
font-size: 15px;
font-weight: 600;
color: #323233;
margin-bottom: 12px;
}
.video-wrap {
position: relative;
width: 100%;
height: 180px;
border-radius: 8px;
overflow: hidden;
background: #000;
.video-thumb {
width: 100%;
height: 100%;
object-fit: cover;
}
.play-icon {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 48px;
color: rgba(255, 255, 255, 0.9);
}
}
.media-placeholder {
height: 120px;
display: flex;
align-items: center;
justify-content: center;
background: #f7f8fa;
border-radius: 8px;
color: #969799;
font-size: 14px;
}
.text-content {
font-size: 14px;
color: #323233;
line-height: 22px;
white-space: pre-wrap;
word-break: break-all;
}
.opinion-field {
padding: 0;
background: #f7f8fa;
border-radius: 8px;
:deep(.van-field__control) {
min-height: 120px;
padding: 8px;
}
}
.page-footer {
flex-shrink: 0;
display: flex;
gap: 12px;
padding: 12px 16px;
background: #fff;
box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.06);
.btn-cancel,
.btn-submit {
flex: 1;
height: 44px;
border-radius: 22px;
font-size: 16px;
}
.btn-cancel {
border: 1px solid #2b7de9;
color: #2b7de9;
background: #fff;
}
.btn-submit {
border: none;
background: linear-gradient(135deg, #1367fe 0%, #4a8cff 100%);
}
}
.video-preview-popup {
width: 100vw;
height: 100vh;
background-color: #000;
display: flex;
align-items: center;
justify-content: center;
.preview-video {
width: 100%;
max-height: 100%;
outline: none;
}
:deep(.van-popup__close-icon) {
color: #fff;
font-size: 24px;
z-index: 2;
}
}
</style>

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

@ -33,14 +33,16 @@
v-model:inspect-result="inspectResult" v-model:inspect-result="inspectResult"
:task-status="taskDetail.taskStatus" :task-status="taskDetail.taskStatus"
:task-deadline="taskDetail.deadline" :task-deadline="taskDetail.deadline"
:digital-voice-logging-status="taskDetail.digitalVoiceLoggingStatus"
:evaluate-data="evaluateData" :evaluate-data="evaluateData"
@view-cloud-record="onViewCloudRecord"
/> />
</div> </div>
</van-tab> </van-tab>
</van-tabs> </van-tabs>
<div v-if="activeTab === 'materials' && Number(taskDetail.taskStatus) === 3" class="detail-footer"> <div v-if="activeTab === 'materials' && Number(taskDetail.taskStatus) === 3" class="detail-footer">
<van-button class="btn-submit" @click="onCloudRecord" native-type="submit">云笔录</van-button> <van-button class="btn-cancel" @click="onCloudRecord">云笔录</van-button>
<van-button class="btn-cancel" @click="onCancel">取消</van-button> <van-button class="btn-cancel" @click="onCancel">取消</van-button>
<van-button class="btn-submit" type="primary" native-type="submit">提交</van-button> <van-button class="btn-submit" type="primary" native-type="submit">提交</van-button>
</div> </div>
@ -49,73 +51,73 @@
</template> </template>
<script setup name="nosDetail"> <script setup name="nosDetail">
import { showConfirmDialog, showSuccessToast, showToast } from 'vant'; import { showConfirmDialog, showSuccessToast, showToast } from 'vant'
import NosDetailTaskTab from './components/NosDetailTaskTab.vue'; import NosDetailTaskTab from './components/NosDetailTaskTab.vue'
import NosDetailMaterialsTab from './components/NosDetailMaterialsTab.vue'; import NosDetailMaterialsTab from './components/NosDetailMaterialsTab.vue'
import { nosInfoApi } from "@/api/nos/enfDemo" import { nosInfoApi, nosApproveApi, cloudRecordStartApi } from '@/api/nos/enfDemo'
import { toFullFileUrl } from "@/util/fileUrl"; import { toFullFileUrl } from '@/util/fileUrl'
import {nosApproveApi} from "@/api/nos/enfDemo"
const router = useRouter(); const router = useRouter()
const route = useRoute(); const route = useRoute()
const TAB_TASK_TITLE_MAP = { const TAB_TASK_TITLE_MAP = {
1: '行政检查任务', 1: '行政检查任务',
2: '材料备案任务', 2: '材料备案任务',
3: '材料验证任务', 3: '材料验证任务',
4: '待核实案件', 4: '待核实案件',
5: '巡查任务', 5: '巡查任务'
}; }
const activeTab = ref('task'); const activeTab = ref('task')
const taskTabTitle = computed(() => TAB_TASK_TITLE_MAP[route.query.tab] || '行政检查任务'); const taskTabTitle = computed(() => TAB_TASK_TITLE_MAP[route.query.tab] || '行政检查任务')
const taskDetail = ref({}); const taskDetail = ref({})
const taskItemList = ref([]); const taskItemList = ref([])
const evaluateData = ref(null); const evaluateData = ref(null)
const inspectResult = ref({ const inspectResult = ref({
checkResult: '', checkResult: '',
deadline: '', deadline: ''
}); })
const applyDetailData = (data) => { const applyDetailData = (data) => {
taskDetail.value = { taskDetail.value = {
taskStatus: data.taskStatus || '', taskStatus: data.taskStatus || '',
caseCode: data.taskNo || '', caseCode: data.taskNo || '',
proclamations: (data.proclamation || []).map(p => ({ proclamations: (data.proclamation || []).map((p) => ({
fileName: p.fileName, fileName: p.fileName,
filePath: toFullFileUrl(p.filePath), filePath: toFullFileUrl(p.filePath)
})), })),
taskSource: data.taskSourceName || '', taskSource: data.taskSourceName || '',
enforcementUnit: data.lawEnforcementAgency || '', enforcementUnit: data.lawEnforcementAgency || '',
enforcementPerson: (data.lawEnforcer || []).map(e => `${e.lawEnforcerName || ''} ${e.lawEnforcerPhone || ''}`.trim()).filter(Boolean), enforcementPerson: (data.lawEnforcer || []).map((e) => `${e.lawEnforcerName || ''} ${e.lawEnforcerPhone || ''}`.trim()).filter(Boolean),
enterpriseName: data.enterpriseName || '', enterpriseName: data.enterpriseName || '',
enterpriseNumber: data.usciCode || '', enterpriseNumber: data.usciCode || '',
category: data.taskTypeName || '', category: data.taskTypeName || '',
enforcementMatter: data.lawEnforcementMatters || '', enforcementMatter: data.lawEnforcementMatters || '',
materialRequirements: (data.taskItemList || []).map(item => ({ name: item.itemName })), materialRequirements: (data.taskItemList || []).map((item) => ({ name: item.itemName })),
enforcementBases: data.legalBasis || [], enforcementBases: data.legalBasis || [],
startTime: data.createTime || '', startTime: data.createTime || '',
deadline: data.deadline || '', deadline: data.deadline || '',
taskTypeId: data.taskTypeId || '', taskTypeId: data.taskTypeId || '',
}; digitalVoiceLoggingStatus: data.digitalVoiceLoggingStatus || ''
taskItemList.value = (data.taskItemList || []).map(item => ({ }
taskItemList.value = (data.taskItemList || []).map((item) => ({
...item, ...item,
itemValue: (item.itemValue || []).map((url) => toFullFileUrl(url)), itemValue: (item.itemValue || []).map((url) => toFullFileUrl(url)),
pass: item.pass === 1 ? 1 : (item.pass === 0 ? 0 : undefined) pass: item.pass === 1 ? 1 : item.pass === 0 ? 0 : undefined
})); }))
// 回显检查结果、检查时间及补正时限 // 回显检查结果、检查时间及补正时限
if (data.checkResult !== undefined && data.checkResult !== null) { if (data.checkResult !== undefined && data.checkResult !== null) {
inspectResult.value.checkResult = data.checkResult; inspectResult.value.checkResult = data.checkResult
} }
if (data.checkTime) { if (data.checkTime) {
inspectResult.value.checkTime = data.checkTime; inspectResult.value.checkTime = data.checkTime
} }
if (data.checkResult === 2 && data.deadline) { if (data.checkResult === 2 && data.deadline) {
inspectResult.value.deadline = data.deadline; inspectResult.value.deadline = data.deadline
} }
// 评价回显 // 评价回显
@ -124,107 +126,155 @@ const applyDetailData = (data) => {
comment: data.comment, comment: data.comment,
satisfaction: data.satisfaction, satisfaction: data.satisfaction,
commentReason: data.commentReason commentReason: data.commentReason
}; }
} else { } else {
evaluateData.value = null; evaluateData.value = null
} }
}; }
const fetchTaskDetail = async () => { const fetchTaskDetail = async () => {
const taskId = route.query.id; const taskId = route.query.id
try { try {
const res = await nosInfoApi({ taskId }); const res = await nosInfoApi({ taskId })
if (res && res.code === 200 && res.data) { if (res && res.code === 200 && res.data) {
applyDetailData(res.data); applyDetailData(res.data)
} }
} catch (err) { } catch (err) {
console.error(err); console.error(err)
} }
}; }
onMounted(() => { onMounted(() => {
if (route.query.id) { if (route.query.id) {
fetchTaskDetail(); fetchTaskDetail()
} }
}); })
const onBack = () => router.back(); const onBack = () => router.back()
const onClose = () => { const onClose = () => {
router.replace({ name: 'nosList' }); router.replace({ name: 'nosList' })
}; }
const onDownloadNotice = (url) => { const onDownloadNotice = (url) => {
if (url) { if (url) {
window.location.href = url; window.location.href = url
} else { } else {
showToast('暂无文件'); showToast('暂无文件')
} }
}; }
const onCancel = () => { const onCancel = () => {
router.back(); router.back()
}; }
const navigateToCloudRecord = (taskId, status, viewOnly = false) => {
const query = { id: taskId, type: status }
if (viewOnly) {
query.taskStatus = 4
}
router.push({
name: 'nosCloudRecord',
query
})
}
const onViewCloudRecord = () => {
const taskId = route.query.id
if (!taskId) {
showToast('任务信息异常')
return
}
navigateToCloudRecord(taskId, 3, true)
}
const onCloudRecord = async () => {
const taskId = route.query.id
if (!taskId) {
showToast('任务信息异常')
return
}
// digitalVoiceLoggingStatus:云笔录状态(0-未发起,1-待企业提交,2-待执法人员确认,3-已完成)
const status = Number(taskDetail.value.digitalVoiceLoggingStatus)
if (status !== 0) {
navigateToCloudRecord(taskId, status)
return
}
try {
const res = await cloudRecordStartApi({ taskId })
if (res && res.code === 200) {
navigateToCloudRecord(taskId, 1)
} else {
showToast(res?.msg || '开启云笔录失败')
}
} catch (err) {
console.error(err)
showToast('开启云笔录失败')
}
}
const validateMaterials = () => { const validateMaterials = () => {
const unaudited = taskItemList.value.find((item) => item.pass !== 0 && item.pass !== 1); const unaudited = taskItemList.value.find((item) => item.pass !== 0 && item.pass !== 1)
if (unaudited) { if (unaudited) {
showToast(`请完成「${unaudited.itemName}」的审核`); showToast(`请完成「${unaudited.itemName}」的审核`)
return false; return false
} }
const hasObjection = taskItemList.value.some(item => item.pass === 0); const hasObjection = taskItemList.value.some((item) => item.pass === 0)
if (hasObjection) { if (hasObjection) {
const isValidResult = inspectResult.value.checkResult === 1 || inspectResult.value.checkResult === 2; const isValidResult = inspectResult.value.checkResult === 1 || inspectResult.value.checkResult === 2
if (!isValidResult) { if (!isValidResult) {
showToast('存在异议项,请选择检查结果'); showToast('存在异议项,请选择检查结果')
return false; return false
} }
} else { } else {
inspectResult.value.checkResult = 0; // 默认已完成 inspectResult.value.checkResult = 0 // 默认已完成
inspectResult.value.deadline = ''; inspectResult.value.deadline = ''
} }
return true; return true
}; }
const onSubmit = async () => { const onSubmit = async () => {
if (!validateMaterials()) return; if (!validateMaterials()) return
try { try {
await showConfirmDialog({ await showConfirmDialog({
title: '确认提交', title: '确认提交',
message: '确认提交审核结果吗?', message: '确认提交审核结果吗?',
width: '80%', width: '80%'
}); })
const params = { const params = {
taskId: route.query.id, taskId: route.query.id,
checkResult: inspectResult.value.checkResult, checkResult: inspectResult.value.checkResult,
deadline: inspectResult.value.deadline || undefined, deadline: inspectResult.value.deadline || undefined,
itemList: taskItemList.value.map(item => ({ itemList: taskItemList.value.map((item) => ({
itemId: item.id || item.itemId, itemId: item.id || item.itemId,
pass: item.pass === 1 ? true : false, pass: item.pass === 1 ? true : false,
reason: item.pass === 0 ? item.reason : undefined reason: item.pass === 0 ? item.reason : undefined
})) }))
}; }
const res = await nosApproveApi(params); const res = await nosApproveApi(params)
if (res && res.code === 200) { if (res && res.code === 200) {
showSuccessToast('提交成功'); showSuccessToast('提交成功')
router.back(); router.back()
} else { } else {
showToast(res?.msg || '提交失败'); showToast(res?.msg || '提交失败')
} }
} catch (err) { } catch (err) {
if (err !== 'cancel') { if (err !== 'cancel') {
console.error(err); console.error(err)
showToast('提交失败'); showToast('提交失败')
} }
} }
}; }
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
@ -274,7 +324,6 @@ const onSubmit = async () => {
:deep(.van-tab__panel) { :deep(.van-tab__panel) {
height: 100%; height: 100%;
} }
} }
.detail-scroll { .detail-scroll {

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

@ -19,11 +19,42 @@
<FooterBar active="offsite" /> <FooterBar active="offsite" />
</div> </div>
<!-- 未备案温馨提示弹窗 -->
<van-popup
v-model:show="showFilingTip"
round
class="filing-tip-popup"
teleport="body"
:close-on-click-overlay="false"
>
<div class="filing-tip">
<div class="filing-tip__header">
<span class="filing-tip__title">温馨提示</span>
<van-icon name="cross" class="filing-tip__close" @click="closeFilingTip" />
</div>
<div class="filing-tip__body">
<p class="filing-tip__line">当前所在单位,未备案法制材料和技术材料;</p>
<p>
备案位置:<span class="filing-tip__path">登录监督端 -> 非现场执法 -> 法制与技术材料留存</span>
</p>
</div>
<div class="filing-tip__footer">
<van-button round type="primary" class="filing-tip__btn" @click="closeFilingTip">
知道了
</van-button>
</div>
</div>
</van-popup>
</template> </template>
<script setup name="nosList"> <script setup name="nosList">
import { onMounted } from 'vue';
import FooterBar from '@/components/FooterBar.vue'; import FooterBar from '@/components/FooterBar.vue';
import NosListPanel from './components/NosListPanel.vue'; import NosListPanel from './components/NosListPanel.vue';
import { isFilingApi } from '@/api/nos/enfDemo';
const FILING_TIP_STORAGE_KEY = 'nos_filing_tip_shown';
const router = useRouter(); const router = useRouter();
// 判断是否是执法人员 // 判断是否是执法人员
@ -60,6 +91,42 @@ const onItemClick = (item) => {
query: { id: item.id, tab: activeTab.value, code: item.code }, query: { id: item.id, tab: activeTab.value, code: item.code },
}); });
}; };
const showFilingTip = ref(false);
const getFilingTipStorageKey = () => {
const userId = $globalStore.useCommon.userId || 'default';
return `${FILING_TIP_STORAGE_KEY}_${userId}`;
};
const closeFilingTip = () => {
showFilingTip.value = false;
};
/** 登录后仅提示一次:未备案时弹窗温馨提示 */
const checkFilingStatus = async () => {
if (sessionStorage.getItem(getFilingTipStorageKey())) {
return;
}
try {
const res = await isFilingApi();
if (!res || res.code !== 200) {
return;
}
if (res.data == null) {
showFilingTip.value = true;
sessionStorage.setItem(getFilingTipStorageKey(), '1');
}
} catch (err) {
console.error('checkFilingStatus error:', err);
}
};
onMounted(() => {
checkFilingStatus();
});
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
@ -121,4 +188,78 @@ const onItemClick = (item) => {
display: none; display: none;
} }
} }
.filing-tip-popup {
width: calc(100vw - 32px);
max-width: 360px;
overflow: visible;
}
.filing-tip {
padding: 20px 12px 24px;
&__header {
position: relative;
text-align: center;
margin-bottom: 20px;
}
&__title {
font-size: 16px;
font-weight: 600;
color: #323233;
line-height: 22px;
}
&__close {
position: absolute;
top: 0;
right: 0;
font-size: 18px;
color: #969799;
padding: 2px;
cursor: pointer;
}
&__body {
font-size: 14px;
color: #323233;
line-height: 22px;
text-align: center;
p {
margin: 0;
& + p {
margin-top: 8px;
}
}
}
&__line {
white-space: nowrap;
font-size: 13px;
letter-spacing: -0.2px;
}
&__path {
font-weight: 600;
color: #323233;
}
&__footer {
display: flex;
justify-content: center;
margin-top: 24px;
}
&__btn {
min-width: 120px;
height: 36px;
padding: 0 28px;
font-size: 14px;
background: #2b7de9;
border-color: #2b7de9;
}
}
</style> </style>

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

20
zdxt-web-client/zdxt-efcode-nos-enterprise-app/.prettierrc

@ -0,0 +1,20 @@
{
"printWidth": 150,
"tabWidth": 2,
"useTabs": false,
"semi": false,
"singleQuote": true,
"quoteProps": "preserve",
"jsxSingleQuote": false,
"bracketSameLine": false,
"trailingComma": "none",
"bracketSpacing": true,
"embeddedLanguageFormatting": "auto",
"arrowParens": "always",
"requirePragma": false,
"insertPragma": false,
"proseWrap": "preserve",
"htmlWhitespaceSensitivity": "css",
"vueIndentScriptAndStyle": false,
"endOfLine": "auto"
}

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

@ -18,3 +18,20 @@ export const nosTaskListByCommentStatusAPi = (params, success, error) =>
// 分页查询非现场执法任务未读消息 // 分页查询非现场执法任务未读消息
export const nosTaskListUnReadAPi = (params, success, error) => export const nosTaskListUnReadAPi = (params, success, error) =>
_post("/app-ent/nos-law-enforcement-task/msg/un-read/page", params, success, error); _post("/app-ent/nos-law-enforcement-task/msg/un-read/page", params, success, error);
// ----------------------------- 非现场执法任务通知 -----------------------------
// 分页查询非现场执法未读任务通知
export const nosTaskListNoticeAPi = (params, success, error) =>
_post("/app-ent/nos-law-enforcement-task/msg/un-read/page", params, success, error);
// 查询未读消息数量
export const nosTaskListUnReadCountAPi = (params, success, error) =>
_post("/app-ent/nos-law-enforcement-task/msg/un-read/count", params, success, error);
// 标记消息为已读
export const nosTaskMarkReadAPi = (params, success, error) =>
_get("/app-ent/nos-law-enforcement-task/msg/mark-read", params, success, error);
// 提交云笔录材料
export const submitCloudRecordApi = (params, success, error) =>
_post("/app-ent/nos-law-enforcement-task/digital-voice-logging/submit", params, success, error);
// 查询云笔录详情
export const cloudRecordDetailApi = (params, success, error) =>
_get("/app-ent/nos-law-enforcement-task/digital-voice-logging/detail", params, success, error);

2
zdxt-web-client/zdxt-efcode-nos-enterprise-app/src/components/FooterBar.vue

@ -79,7 +79,7 @@ const tabs = [
{ {
key: "message", key: "message",
label: "消息", label: "消息",
route: "/enforcement/message", route: "/enterprise/nos/message",
path: '<path d="M12 22c1.1 0 2-.9 2-2h-4c0 1.1.89 2 2 2zm6-6v-5c0-3.07-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.63 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2z"/>' path: '<path d="M12 22c1.1 0 2-.9 2-2h-4c0 1.1.89 2 2 2zm6-6v-5c0-3.07-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.63 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2z"/>'
}, },
{ {

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

@ -10,6 +10,7 @@ const routes = [
title: "enf 测试单列表", title: "enf 测试单列表",
}, },
}, },
// 主页面
{ {
path: "/enterprise/nos", path: "/enterprise/nos",
name: "nosList", name: "nosList",
@ -27,9 +28,28 @@ const routes = [
isKeepAlive: false, isKeepAlive: false,
}, },
}, },
// 云笔录
{
path: "/enterprise/nos/cloud-record",
name: "nosCloudRecord",
component: () => import("@/views/enterprise/nos/nosCloudRecord.vue"),
meta: {
isKeepAlive: false,
},
},
// 消息中心入口
{
path: "/enterprise/nos/message",
name: "nosMessageHub",
component: () => import("@/views/enterprise/nos/message/index.vue"),
meta: {
isKeepAlive: false,
title: "消息",
},
},
// 非现场执法评价列表 // 非现场执法评价列表
{ {
path: "/enterprise/nos/evaluation-list", path: "/enterprise/nos/evaluationList",
name: "nosEvaluationList", name: "nosEvaluationList",
component: () => import("@/views/enterprise/nos/message/nosEvaluationList.vue"), component: () => import("@/views/enterprise/nos/message/nosEvaluationList.vue"),
meta: { meta: {
@ -37,6 +57,16 @@ const routes = [
title: "非现场执法评价", title: "非现场执法评价",
}, },
}, },
// 非现场执法任务通知
{
path: "/enterprise/nos/noticeList",
name: "nosNoticeList",
component: () => import("@/views/enterprise/nos/message/nosTaskList.vue"),
meta: {
isKeepAlive: false,
title: "非现场执法任务通知",
},
},
]; ];
export { routes }; export { routes };

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

@ -8,17 +8,9 @@
</div> </div>
</template> </template>
<template v-else> <template v-else>
<div <div v-for="(item, index) in taskItemList" :key="index" class="material-card">
v-for="(item, index) in taskItemList"
:key="index"
class="material-card"
>
<div class="material-title"> <div class="material-title">
<van-icon <van-icon v-if="Number(item.pass) === 0" name="info-o" class="required-mark" />
v-if="Number(item.pass) === 0"
name="info-o"
class="required-mark"
/>
<span v-else class="required-mark">*</span> <span v-else class="required-mark">*</span>
{{ item.itemName }} {{ item.itemName }}
<span class="material-desc">(小于20M/个,最多30个文件)</span> <span class="material-desc">(小于20M/个,最多30个文件)</span>
@ -26,36 +18,17 @@
<div class="material-media" v-if="!isEdit"> <div class="material-media" v-if="!isEdit">
<template v-for="(url, fileIndex) in item.itemValue" :key="fileIndex"> <template v-for="(url, fileIndex) in item.itemValue" :key="fileIndex">
<div <div v-if="isVideo(url)" class="media-item media-item--video" @click="onPreview(url, item.itemValue)">
v-if="isVideo(url)" <video :src="resolveMediaUrl(url)" muted preload="metadata" class="video-thumb"></video>
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" /> <van-icon name="play-circle-o" class="play-icon" />
</div> </div>
<div <div v-else-if="isPdf(url)" class="media-item media-item--pdf" @click="onPreview(url, item.itemValue)">
v-else-if="isPdf(url)"
class="media-item media-item--pdf"
@click="onPreview(url, item.itemValue)"
>
<div class="pdf-icon-wrapper"> <div class="pdf-icon-wrapper">
<van-icon name="description" class="pdf-icon" /> <van-icon name="description" class="pdf-icon" />
<span class="pdf-text">PDF</span> <span class="pdf-text">PDF</span>
</div> </div>
</div> </div>
<van-image <van-image v-else :src="resolveMediaUrl(url)" fit="cover" class="media-item" @click="onPreview(url, item.itemValue)" />
v-else
:src="resolveMediaUrl(url)"
fit="cover"
class="media-item"
@click="onPreview(url, item.itemValue)"
/>
</template> </template>
</div> </div>
@ -94,25 +67,27 @@
<div class="readonly-result-text" style="padding-top: 4px"> <div class="readonly-result-text" style="padding-top: 4px">
检查结果:{{ 检查结果:{{
inspectResult.checkResult === 0 inspectResult.checkResult === 0
? "通过" ? '通过'
: inspectResult.checkResult === 1 : inspectResult.checkResult === 1
? "转为待核实处置案件" ? '转为待核实处置案件'
: inspectResult.checkResult === 2 : inspectResult.checkResult === 2
? "退回企业补正材料" ? '退回企业补正材料'
: "待审核" : '待审核'
}} }}
</div> </div>
<div class="readonly-result-text" v-if="inspectResult.checkTime"> <div class="readonly-result-text" v-if="inspectResult.checkTime">检查时间:{{ inspectResult.checkTime }}</div>
检查时间:{{ inspectResult.checkTime }} <div class="readonly-result-text" v-if="inspectResult.checkResult === 2 && inspectResult.deadline">
</div>
<div
class="readonly-result-text"
v-if="inspectResult.checkResult === 2 && inspectResult.deadline"
>
补正时限:{{ inspectResult.deadline }} 补正时限:{{ inspectResult.deadline }}
</div> </div>
</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>
</div>
<div class="evaluate-card" v-if="Number(taskStatus) === 4"> <div class="evaluate-card" v-if="Number(taskStatus) === 4">
<div class="card-title">评价</div> <div class="card-title">评价</div>
@ -122,65 +97,41 @@
class="emoji-item" class="emoji-item"
:class="{ :class="{
active: evaluateForm.satisfactionLevel === 3, active: evaluateForm.satisfactionLevel === 3,
disabled: readonly, disabled: readonly
}" }"
@click="!readonly && (evaluateForm.satisfactionLevel = 3)" @click="!readonly && (evaluateForm.satisfactionLevel = 3)"
> >
<img <img class="emoji-img" :src="evaluateForm.satisfactionLevel === 3 ? veryGoodImg : veryGoodDisImg" alt="满意" />
class="emoji-img"
:src="
evaluateForm.satisfactionLevel === 3
? veryGoodImg
: veryGoodDisImg
"
alt="满意"
/>
<span class="emoji-label">满意</span> <span class="emoji-label">满意</span>
</div> </div>
<div <div
class="emoji-item" class="emoji-item"
:class="{ :class="{
active: evaluateForm.satisfactionLevel === 2, active: evaluateForm.satisfactionLevel === 2,
disabled: readonly, disabled: readonly
}" }"
@click="!readonly && (evaluateForm.satisfactionLevel = 2)" @click="!readonly && (evaluateForm.satisfactionLevel = 2)"
> >
<img <img class="emoji-img" :src="evaluateForm.satisfactionLevel === 2 ? goodImg : goodDisImg" alt="一般" />
class="emoji-img"
:src="evaluateForm.satisfactionLevel === 2 ? goodImg : goodDisImg"
alt="一般"
/>
<span class="emoji-label">一般</span> <span class="emoji-label">一般</span>
</div> </div>
<div <div
class="emoji-item" class="emoji-item"
:class="{ :class="{
active: evaluateForm.satisfactionLevel === 1, active: evaluateForm.satisfactionLevel === 1,
disabled: readonly, disabled: readonly
}" }"
@click="!readonly && (evaluateForm.satisfactionLevel = 1)" @click="!readonly && (evaluateForm.satisfactionLevel = 1)"
> >
<img <img class="emoji-img" :src="evaluateForm.satisfactionLevel === 1 ? badImg : badDisImg" alt="不满意" />
class="emoji-img"
:src="evaluateForm.satisfactionLevel === 1 ? badImg : badDisImg"
alt="不满意"
/>
<span class="emoji-label">不满意</span> <span class="emoji-label">不满意</span>
</div> </div>
</div> </div>
<!-- 评价原因 --> <!-- 评价原因 -->
<div <div class="reason-section" v-if="evaluateForm.satisfactionLevel === 1 || evaluateForm.satisfactionLevel === 2">
class="reason-section"
v-if="
evaluateForm.satisfactionLevel === 1 ||
evaluateForm.satisfactionLevel === 2
"
>
<div class="reason-row"> <div class="reason-row">
<span class="reason-label" <span class="reason-label"><span class="required-mark" v-if="!readonly">*</span>原因:</span>
><span class="required-mark" v-if="!readonly">*</span>原因:</span
>
<div class="reason-input-wrap"> <div class="reason-input-wrap">
<van-field <van-field
v-model="evaluateForm.commentReason" v-model="evaluateForm.commentReason"
@ -197,220 +148,181 @@
</div> </div>
<div class="submit-btn-wrap" v-if="!readonly"> <div class="submit-btn-wrap" v-if="!readonly">
<van-button <van-button class="btn-submit-eval" type="primary" block @click="onSubmitEvaluate">提交评价</van-button>
class="btn-submit-eval"
type="primary"
block
@click="onSubmitEvaluate"
>提交评价</van-button
>
</div> </div>
</div> </div>
</template> </template>
<van-popup <van-popup v-model:show="showDatePicker" round position="bottom" teleport="body">
v-model:show="showDatePicker" <van-date-picker v-model="pickerDate" title="选择补正时限" :min-date="minDate" @confirm="onDateConfirm" @cancel="showDatePicker = false" />
round
position="bottom"
teleport="body"
>
<van-date-picker
v-model="pickerDate"
title="选择补正时限"
:min-date="minDate"
@confirm="onDateConfirm"
@cancel="showDatePicker = false"
/>
</van-popup> </van-popup>
<!-- 视频预览弹窗 --> <!-- 视频预览弹窗 -->
<van-popup <van-popup v-model:show="showVideoPreview" class="video-preview-popup" teleport="body" closeable @closed="previewVideoUrl = ''">
v-model:show="showVideoPreview" <video v-if="previewVideoUrl" :src="previewVideoUrl" controls autoplay playsinline webkit-playsinline class="preview-video"></video>
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> </van-popup>
</div> </div>
</template> </template>
<script setup name="NosDetailMaterialsTab"> <script setup name="NosDetailMaterialsTab">
import { showImagePreview, showToast, showSuccessToast } from "vant"; import { showImagePreview, showToast, showSuccessToast } from 'vant'
import { useRoute } from "vue-router"; import { useRoute } from 'vue-router'
import noDataImg from "@/assets/images/nos/no-data.png"; import noDataImg from '@/assets/images/nos/no-data.png'
import { uploadFile } from "@/util/fileUpload"; import { uploadFile } from '@/util/fileUpload'
import { toFullFileUrl } from "@/util/fileUrl"; import { toFullFileUrl } from '@/util/fileUrl'
import { nosTaskCommentAPi } from "@/api/nos/enfDemo"; import { nosTaskCommentAPi } from '@/api/nos/enfDemo'
import veryGoodImg from "@/assets/images/eval-verygood.png"; import veryGoodImg from '@/assets/images/eval-verygood.png'
import veryGoodDisImg from "@/assets/images/eval-verygood-dis.png"; import veryGoodDisImg from '@/assets/images/eval-verygood-dis.png'
import goodImg from "@/assets/images/eval-good.png"; import goodImg from '@/assets/images/eval-good.png'
import goodDisImg from "@/assets/images/eval-good-dis.png"; import goodDisImg from '@/assets/images/eval-good-dis.png'
import badImg from "@/assets/images/eval-bad.png"; import badImg from '@/assets/images/eval-bad.png'
import badDisImg from "@/assets/images/eval-bad-dis.png"; import badDisImg from '@/assets/images/eval-bad-dis.png'
const resolveMediaUrl = (url) => toFullFileUrl(url); const resolveMediaUrl = (url) => toFullFileUrl(url)
// 与 NosListPanel STATUS_OPTIONS 保持一致 // 与 NosListPanel STATUS_OPTIONS 保持一致
const TASK_STATUS_TEXT = { const TASK_STATUS_TEXT = {
1: "待处理", 1: '待处理',
2: "待修改", 2: '待修改',
3: "待审核", 3: '待审核',
4: "已办结", 4: '已办结',
5: "超期", 5: '超期'
}; }
const props = defineProps({ const props = defineProps({
taskStatus: { taskStatus: {
type: [Number, String], type: [Number, String],
default: "", default: ''
}, },
isEdit: { isEdit: {
type: Boolean, type: Boolean,
default: false, default: false
}, },
evaluateData: { evaluateData: {
type: Object, type: Object,
default: null, default: null
}, }
}); })
const emit = defineEmits(['view-cloud-record'])
const onViewCloudRecord = () => {
emit('view-cloud-record')
}
const taskItemList = defineModel("taskItemList", { const taskItemList = defineModel('taskItemList', {
type: Array, type: Array,
default: () => [], default: () => []
}); })
const inspectResult = defineModel("inspectResult", { const inspectResult = defineModel('inspectResult', {
type: Object, type: Object,
default: () => ({ default: () => ({
checkResult: null, checkResult: null,
deadline: "", deadline: '',
checkTime: "", checkTime: ''
}), })
}); })
const digitalVoiceLoggingStatus = defineModel('digitalVoiceLoggingStatus', {
const route = useRoute(); type: [Number, String],
const taskId = route.query.id; default: ''
})
const route = useRoute()
const taskId = route.query.id
// ========== 评价相关 ========== // ========== 评价相关 ==========
const hasEvaluated = ref(false); const hasEvaluated = ref(false)
const readonly = computed(() => hasEvaluated.value); const readonly = computed(() => hasEvaluated.value)
const evaluateForm = ref({ const evaluateForm = ref({
satisfactionLevel: 3, // 3: 满意, 2: 一般, 1: 不满意 satisfactionLevel: 3, // 3: 满意, 2: 一般, 1: 不满意
commentReason: "", commentReason: ''
}); })
watch( watch(
() => props.evaluateData, () => props.evaluateData,
(val) => { (val) => {
if (val && val.satisfaction !== undefined) { if (val && val.satisfaction !== undefined) {
const satMap = { 0: 3, 1: 2, 2: 1 }; const satMap = { 0: 3, 1: 2, 2: 1 }
evaluateForm.value.satisfactionLevel = evaluateForm.value.satisfactionLevel = satMap[val.satisfaction] !== undefined ? satMap[val.satisfaction] : 3
satMap[val.satisfaction] !== undefined ? satMap[val.satisfaction] : 3;
if (val.commentReason) { if (val.commentReason) {
evaluateForm.value.commentReason = val.commentReason; evaluateForm.value.commentReason = val.commentReason
} }
hasEvaluated.value = true; hasEvaluated.value = true
} }
}, },
{ immediate: true, deep: true }, { immediate: true, deep: true }
); )
const onSubmitEvaluate = async () => { const onSubmitEvaluate = async () => {
if ( if ((evaluateForm.value.satisfactionLevel === 1 || evaluateForm.value.satisfactionLevel === 2) && !evaluateForm.value.commentReason?.trim()) {
(evaluateForm.value.satisfactionLevel === 1 || showToast('请输入原因')
evaluateForm.value.satisfactionLevel === 2) && return
!evaluateForm.value.commentReason?.trim()
) {
showToast("请输入原因");
return;
} }
try { try {
const params = { const params = {
taskId: taskId, taskId: taskId,
satisfaction: satisfaction: evaluateForm.value.satisfactionLevel === 3 ? 0 : evaluateForm.value.satisfactionLevel === 2 ? 1 : 2,
evaluateForm.value.satisfactionLevel === 3 reason: evaluateForm.value.commentReason?.trim()
? 0 }
: evaluateForm.value.satisfactionLevel === 2
? 1 const res = await nosTaskCommentAPi(params)
: 2,
reason: evaluateForm.value.commentReason?.trim(),
};
const res = await nosTaskCommentAPi(params);
if (res && res.code === 200) { if (res && res.code === 200) {
showSuccessToast("评价提交成功"); showSuccessToast('评价提交成功')
hasEvaluated.value = true; hasEvaluated.value = true
router.back(); router.back()
} else { } else {
showToast(res.msg || "评价提交失败"); showToast(res.msg || '评价提交失败')
} }
} catch (err) { } catch (err) {
console.error(err); console.error(err)
// showToast("评价提交失败"); // showToast("评价提交失败");
} }
}; }
const showDatePicker = ref(false); const showDatePicker = ref(false)
const minDate = new Date(); const minDate = new Date()
const showVideoPreview = ref(false); const showVideoPreview = ref(false)
const previewVideoUrl = ref(""); const previewVideoUrl = ref('')
const pickerDate = ref([ const pickerDate = ref([String(minDate.getFullYear()), String(minDate.getMonth() + 1).padStart(2, '0'), String(minDate.getDate()).padStart(2, '0')])
String(minDate.getFullYear()),
String(minDate.getMonth() + 1).padStart(2, "0"),
String(minDate.getDate()).padStart(2, "0"),
]);
const onOversize = () => { const onOversize = () => {
showToast("文件大小不能超过 20MB"); showToast('文件大小不能超过 20MB')
}; }
const isVideo = (url) => { const isVideo = (url) => {
if (!url) return false; if (!url) return false
return /\.(mp4|webm|ogg|mov)$/i.test(url); return /\.(mp4|webm|ogg|mov)$/i.test(url)
}; }
const isPdf = (url) => { const isPdf = (url) => {
if (!url) return false; if (!url) return false
return /\.pdf$/i.test(url); return /\.pdf$/i.test(url)
}; }
const onAfterReadImage = (file) => { const onAfterReadImage = (file) => {
const files = Array.isArray(file) ? file : [file]; const files = Array.isArray(file) ? file : [file]
files.forEach((f) => { files.forEach((f) => {
uploadFile(f, (res) => { uploadFile(f, (res) => {
const data = res && res.data ? res.data : res; const data = res && res.data ? res.data : res
const filePath = const filePath = typeof data === 'string' ? data : (data && (data.url || data.fileUrl || data.path)) || ''
typeof data === "string"
? data
: (data && (data.url || data.fileUrl || data.path)) || "";
if (filePath) { if (filePath) {
f.url = toFullFileUrl(filePath); f.url = toFullFileUrl(filePath)
if (isVideo(filePath) || isPdf(filePath)) { if (isVideo(filePath) || isPdf(filePath)) {
f.isImage = false; f.isImage = false
} }
f.status = "done"; f.status = 'done'
} else { } else {
f.status = "failed"; f.status = 'failed'
f.message = "上传失败"; f.message = '上传失败'
} }
}); })
}); })
}; }
watch( watch(
() => taskItemList.value, () => taskItemList.value,
@ -421,53 +333,51 @@ watch(
// Initialize fileList from itemValue if editing is possible // Initialize fileList from itemValue if editing is possible
item.fileList = (item.itemValue || []).map((url) => ({ item.fileList = (item.itemValue || []).map((url) => ({
url: toFullFileUrl(url), url: toFullFileUrl(url),
isImage: !isVideo(url) && !isPdf(url), isImage: !isVideo(url) && !isPdf(url)
})); }))
} }
}); })
} }
}, },
{ immediate: true, deep: true }, { immediate: true, deep: true }
); )
const onAuditChange = (item) => { const onAuditChange = (item) => {
if (Number(item.pass) !== 0) { if (Number(item.pass) !== 0) {
item.reason = ""; item.reason = ''
} }
}; }
const onResultTypeChange = () => { const onResultTypeChange = () => {
if (inspectResult.value.checkResult !== 2) { if (inspectResult.value.checkResult !== 2) {
inspectResult.value.deadline = ""; inspectResult.value.deadline = ''
} }
}; }
const onDateConfirm = ({ selectedValues }) => { const onDateConfirm = ({ selectedValues }) => {
inspectResult.value.deadline = selectedValues.join("-"); inspectResult.value.deadline = selectedValues.join('-')
showDatePicker.value = false; showDatePicker.value = false
}; }
const onPreview = (url, itemValue) => { const onPreview = (url, itemValue) => {
if (!url) return; if (!url) return
const fullUrl = resolveMediaUrl(url); const fullUrl = resolveMediaUrl(url)
if (isVideo(url)) { if (isVideo(url)) {
previewVideoUrl.value = fullUrl; previewVideoUrl.value = fullUrl
showVideoPreview.value = true; showVideoPreview.value = true
return; return
} }
if (isPdf(url)) { if (isPdf(url)) {
window.open(fullUrl, "_blank"); window.open(fullUrl, '_blank')
return; return
} }
const images = itemValue const images = itemValue.filter((u) => !isVideo(u) && !isPdf(u)).map((u) => resolveMediaUrl(u))
.filter((u) => !isVideo(u) && !isPdf(u)) const start = images.indexOf(fullUrl)
.map((u) => resolveMediaUrl(u));
const start = images.indexOf(fullUrl);
showImagePreview({ showImagePreview({
images, images,
startPosition: start >= 0 ? start : 0, startPosition: start >= 0 ? start : 0
}); })
}; }
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
@ -778,7 +688,7 @@ const onPreview = (url, itemValue) => {
margin-bottom: 16px; margin-bottom: 16px;
.reason-label { .reason-label {
width: 72px; width: 52px;
font-size: 14px; font-size: 14px;
color: #323233; color: #323233;
margin-top: 8px; margin-top: 8px;
@ -808,4 +718,29 @@ const onPreview = (url, itemValue) => {
} }
} }
} }
.cloud-record-card {
background: #fff;
margin-top: 8px;
padding: 14px 16px;
border-radius: 4px;
}
.cloud-record-row {
display: flex;
align-items: center;
justify-content: space-between;
}
.cloud-record-label {
font-size: 15px;
font-weight: 600;
color: #323233;
}
.btn-view {
height: 28px;
padding: 0 12px;
font-size: 12px;
}
</style> </style>

105
zdxt-web-client/zdxt-efcode-nos-enterprise-app/src/views/enterprise/nos/message/index.vue

@ -0,0 +1,105 @@
<template>
<div class="nos-message-page app_content">
<van-nav-bar title="消息" fixed placeholder />
<div class="message-body">
<van-cell-group inset>
<van-cell title="任务通知" is-link @click="goNoticeList">
<template #value>
<span v-if="unreadCount > 0" class="unread-badge">
{{ unreadCount > 99 ? '99+' : unreadCount }}
</span>
</template>
</van-cell>
<van-cell title="非现场执法评价" is-link @click="goEvaluationList" />
</van-cell-group>
</div>
<FooterBar active="message" />
</div>
</template>
<script setup name="nosMessageHub">
import { ref, onMounted, onActivated } from 'vue';
import { useRouter } from 'vue-router';
import FooterBar from '@/components/FooterBar.vue';
import { nosTaskListUnReadCountAPi } from '@/api/nos/enfDemo';
import { setMessageUnread } from '@/store/messageState';
const router = useRouter();
const unreadCount = ref(0);
const fetchUnreadCount = () => {
nosTaskListUnReadCountAPi(
{},
(res) => {
if (res.code === 200) {
const count = Number(res.data ?? 0) || 0;
unreadCount.value = count;
setMessageUnread(count);
}
},
() => {},
);
};
const goNoticeList = () => {
router.push('/enterprise/nos/noticeList');
};
const goEvaluationList = () => {
router.push('/enterprise/nos/evaluationList');
};
onMounted(fetchUnreadCount);
onActivated(fetchUnreadCount);
</script>
<style lang="less" scoped>
.nos-message-page {
display: flex;
flex-direction: column;
height: 100%;
background: #f5f7fa;
overflow: hidden;
}
:deep(.van-nav-bar) {
background-color: #fff;
}
:deep(.van-nav-bar__title) {
font-size: 17px;
font-weight: 600;
color: #333;
}
.message-body {
flex: 1;
min-height: 0;
overflow-y: auto;
padding: 12px 0;
}
:deep(.van-cell-group--inset) {
margin: 0 12px;
}
:deep(.van-cell__title) {
font-size: 15px;
color: #333;
}
.unread-badge {
display: inline-block;
min-width: 18px;
height: 18px;
line-height: 18px;
padding: 0 6px;
background: #f5222d;
color: #fff;
font-size: 12px;
text-align: center;
border-radius: 9px;
}
</style>

330
zdxt-web-client/zdxt-efcode-nos-enterprise-app/src/views/enterprise/nos/message/nosTaskList.vue

@ -0,0 +1,330 @@
<template>
<div class="plan-notice-page app_content">
<van-nav-bar
title="任务通知"
left-text="返回"
left-arrow
@click-left="onClickLeft"
/>
<div class="list-container">
<van-pull-refresh v-model="state.refreshing" @refresh="onRefresh">
<van-list
v-model:loading="state.loading"
:finished="state.finished"
finished-text="已全部加载"
:error="state.error"
error-text="请求失败,点击重试"
:offset="50"
@load="onLoad"
@update:error="(v) => (state.error = v)"
>
<van-empty
v-if="!state.loading && state.finished && state.list.length === 0"
description="暂无任务通知"
/>
<div
v-for="item in state.list"
:key="item.id ?? item.taskId"
class="plan-card"
>
<div class="card-header">
<van-icon name="bell" class="bell-icon" />
<span class="plan-title">
<template
v-for="(seg, idx) in formatPlanTitleSegments(item.title)"
:key="idx"
>
<span v-if="seg.highlight" class="title-deadline-tip">{{ seg.text }}</span>
<template v-else>{{ seg.text }}</template>
</template>
</span>
</div>
<div class="card-content">
<div class="info-row">
<span class="label">企业名称</span>
<span class="value">{{ item.enterpriseName || '-' }}</span>
</div>
<div class="info-row">
<span class="label">执法单位</span>
<span class="value">{{ item.lawEnforcementAgency || '-' }}</span>
</div>
<div class="info-row">
<span class="label">截止日期</span>
<span class="value">{{ formatTime(item.deadline) }}</span>
</div>
<div class="info-row-bottom">
<span class="item-time"></span>
<span class="item-action" @click="onView(item)">
处理
<van-icon name="arrow" />
</span>
</div>
</div>
</div>
</van-list>
</van-pull-refresh>
</div>
</div>
</template>
<script setup name="nosNoticeList">
import { reactive } from 'vue';
import { useRouter } from 'vue-router';
import { showFailToast } from 'vant';
import 'vant/es/toast/style';
import { nosTaskListNoticeAPi, nosTaskMarkReadAPi } from '@/api/nos/enfDemo';
import { formattedTimeWithoutSecond } from '@/util/util';
import { messageUnread, setMessageUnread } from '@/store/messageState';
const router = useRouter();
const state = reactive({
list: [],
pageNum: 1,
pageSize: 10,
loading: false,
finished: false,
refreshing: false,
error: false,
});
const formatTime = (time) => {
if (!time) return '-';
return formattedTimeWithoutSecond(time);
};
const DEADLINE_TIP_RE = /[((]距截止日期\d+天[))]/;
const formatPlanTitleSegments = (title) => {
if (!title) return [{ text: '-', highlight: false }];
const match = title.match(DEADLINE_TIP_RE);
if (!match || match.index === undefined) {
return [{ text: title, highlight: false }];
}
const segments = [];
const main = title.slice(0, match.index);
const highlight = match[0];
const rest = title.slice(match.index + highlight.length);
if (main) segments.push({ text: main, highlight: false });
segments.push({ text: highlight, highlight: true });
if (rest) segments.push({ text: rest, highlight: false });
return segments;
};
const fetchList = () => {
nosTaskListNoticeAPi(
{
pageNum: state.pageNum,
pageSize: state.pageSize,
},
(res) => {
if (res.code !== 200) {
state.finished = true;
state.error = true;
state.refreshing = false;
showFailToast(res.msg || '加载失败');
return;
}
const rows = Array.isArray(res.rows) ? res.rows : [];
const total = res.total ?? 0;
if (state.refreshing) {
state.list = rows;
state.refreshing = false;
} else {
state.list.push(...rows);
}
if (state.list.length >= total || rows.length === 0) {
state.finished = true;
} else {
state.pageNum += 1;
}
},
() => {
state.finished = true;
state.error = true;
state.refreshing = false;
showFailToast('网络请求失败');
},
).finally(() => {
state.loading = false;
});
};
const markAsRead = (item) => {
if (!item?.id) return;
nosTaskMarkReadAPi(
{ msgId: item.id },
(res) => {
if (res.code !== 200) return;
state.list = state.list.filter((row) => row.id !== item.id);
if (messageUnread.value > 0) {
setMessageUnread(messageUnread.value - 1);
}
},
);
};
const onView = (item) => {
if (!item?.taskId) {
showFailToast('任务信息不完整');
return;
}
router.push({
name: 'nosDetail',
query: { id: item.taskId, tab: item.taskType },
});
markAsRead(item);
};
const onLoad = () => {
if (state.refreshing) return;
state.loading = true;
fetchList();
};
const onRefresh = () => {
state.pageNum = 1;
state.list = [];
state.finished = false;
state.error = false;
state.loading = true;
fetchList();
};
const onClickLeft = () => {
router.back();
};
</script>
<style lang="less" scoped>
.plan-notice-page {
height: 100%;
display: flex;
flex-direction: column;
background-color: #ebf3fb;
}
:deep(.van-nav-bar) {
background-color: #fff;
}
:deep(.van-nav-bar__title) {
font-size: 17px;
font-weight: 600;
color: #333;
}
:deep(.van-nav-bar__text) {
font-size: 15px;
color: #333;
}
:deep(.van-nav-bar .van-icon) {
color: #333;
}
.list-container {
flex: 1;
min-height: 0;
overflow-y: auto;
margin: 10px;
}
.plan-card {
background: #fff;
border-radius: 10px;
padding: 15px;
margin-bottom: 15px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.card-header {
display: flex;
align-items: center;
margin-bottom: 12px;
}
.bell-icon {
color: #2d94fa;
font-size: 18px;
margin-right: 8px;
}
.plan-title {
font-size: 16px;
font-weight: bold;
color: #333;
flex: 1;
line-height: 1.4;
.title-deadline-tip {
color: #ee0a24;
font-weight: bold;
}
}
.card-content {
padding-left: 28px;
}
.info-row {
display: flex;
margin-bottom: 8px;
font-size: 14px;
color: #666;
line-height: 1.6;
.label {
color: #000;
min-width: 80px;
}
.value {
flex: 1;
color: #999;
word-break: break-all;
}
}
.info-row-bottom {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 4px;
padding-top: 10px;
border-top: 1px solid #f0f0f0;
.item-time {
font-size: 12px;
color: #999;
}
.item-action {
display: flex;
align-items: center;
font-size: 14px;
color: #2b7de9;
flex-shrink: 0;
.van-icon {
font-size: 12px;
margin-left: 2px;
}
}
}
:deep(.van-empty) {
padding: 60px 0;
}
</style>

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

@ -0,0 +1,571 @@
<template>
<div class="cloud-record-page app_content">
<van-nav-bar title="执法监督码" left-arrow @click-left="onBack" />
<div class="cloud-record-body">
<van-tabs
v-model:active="activeTab"
class="cloud-record-tabs"
color="#2b7de9"
title-active-color="#2b7de9"
title-inactive-color="#646566"
line-width="50%"
>
<van-tab title="云笔录" name="cloud">
<van-pull-refresh v-if="isViewOnly" v-model="refreshing" class="tab-scroll" @refresh="onRefresh">
<div v-if="loading && !refreshing" class="loading-wrap">
<van-loading type="spinner" color="#2b7de9" />
</div>
<template v-else-if="!hasRecord">
<div class="empty-state">
<van-image :src="noDataImg" width="151" height="120" />
<div class="empty-text">暂无云笔录</div>
</div>
</template>
<template v-else>
<div class="content-scroll">
<div class="section-card">
<div class="section-title">云视频</div>
<div v-if="cloudVideo" class="video-wrap" @click="showVideoPreview = true">
<video :src="cloudVideo" muted preload="metadata" class="video-thumb" />
<van-icon name="play-circle-o" class="play-icon" />
</div>
<div v-else class="media-placeholder">暂无视频</div>
</div>
<div class="section-card">
<div class="section-title">云文字</div>
<div class="text-content">{{ cloudText || '暂无文字内容' }}</div>
</div>
<div class="section-card">
<div class="section-title">意见</div>
<div class="text-content">{{ suggestion || '暂无意见' }}</div>
</div>
</div>
</template>
</van-pull-refresh>
<div v-else class="tab-scroll">
<div class="form-card">
<div class="form-row">
<div class="form-label">
<span class="required-mark">*</span>云视频
</div>
<div class="form-value">
<van-uploader
v-model="videoFileList"
:max-count="1"
:max-size="VIDEO_MAX_SIZE"
accept="video/*"
:after-read="onAfterReadVideo"
@oversize="onVideoOversize"
@delete="onVideoDelete"
>
<template #default>
<div class="uploader-btn">
<van-icon name="plus" size="24" color="#dcdee0" />
</div>
</template>
</van-uploader>
<div class="form-tip">(仅支持视频,200MB以下)</div>
</div>
</div>
<div class="form-row form-row--textarea">
<div class="form-label">
<span class="required-mark">*</span>云文字:
</div>
<div class="form-value">
<van-field
v-model="cloudText"
class="text-field"
type="textarea"
rows="5"
autosize
maxlength="300"
show-word-limit
placeholder="请输入"
:readonly="submitting"
/>
</div>
</div>
</div>
</div>
</van-tab>
</van-tabs>
</div>
<div v-if="!isViewOnly" class="page-footer">
<van-button class="btn-cancel" :disabled="submitting" @click="onBack">取消</van-button>
<van-button class="btn-submit" type="primary" :loading="submitting" @click="onSubmit">提交</van-button>
</div>
<van-popup
v-model:show="showVideoPreview"
class="video-preview-popup"
teleport="body"
closeable
@closed="showVideoPreview = false"
>
<video
v-if="cloudVideo"
:src="cloudVideo"
controls
autoplay
playsinline
webkit-playsinline
class="preview-video"
/>
</van-popup>
</div>
</template>
<script setup name="nosCloudRecord">
import { showConfirmDialog, showSuccessToast, showToast } from 'vant';
import noDataImg from '@/assets/images/nos/no-data.png';
import { submitCloudRecordApi, cloudRecordDetailApi } from '@/api/nos/enfDemo';
import { uploadFile } from '@/util/fileUpload';
import { toFullFileUrl, toRelativeFilePath } from '@/util/fileUrl';
const VIDEO_MAX_SIZE = 200 * 1024 * 1024;
const router = useRouter();
const route = useRoute();
// taskStatus=4 时已办结,仅查看
const isViewOnly = computed(() => Number(route.query.taskStatus) === 4);
const activeTab = ref('cloud');
const loading = ref(false);
const refreshing = ref(false);
const submitting = ref(false);
const hasRecord = ref(false);
const cloudVideo = ref('');
const cloudText = ref('');
const suggestion = ref('');
const cloudVideoPath = ref('');
const videoFileList = ref([]);
const showVideoPreview = ref(false);
const applyDetailData = (data) => {
cloudVideo.value = data.cloudVideo ? toFullFileUrl(data.cloudVideo) : '';
cloudText.value = data.cloudText || '';
suggestion.value = data.suggestion || '';
hasRecord.value = !!(data.cloudVideo || data.cloudText || data.suggestion);
};
const fetchDetail = async () => {
const taskId = route.query.id;
if (!taskId) {
showToast('任务信息异常');
return;
}
try {
const res = await cloudRecordDetailApi({ taskId });
if (res && res.code === 200) {
if (res.data) {
applyDetailData(res.data);
} else {
hasRecord.value = false;
cloudVideo.value = '';
cloudText.value = '';
suggestion.value = '';
}
} else {
showToast(res?.msg || '查询云笔录失败');
}
} catch (err) {
console.error(err);
showToast('查询云笔录失败');
} finally {
loading.value = false;
refreshing.value = false;
}
};
const onRefresh = () => {
fetchDetail();
};
const isVideo = (url) => {
if (!url) return false;
return /\.(mp4|webm|ogg|mov|avi|mkv)$/i.test(url);
};
const onVideoOversize = () => {
showToast('视频大小不能超过 200MB');
};
const onVideoDelete = () => {
cloudVideoPath.value = '';
};
const onAfterReadVideo = (file) => {
const files = Array.isArray(file) ? file : [file];
files.forEach((f) => {
if (f.file && !f.file.type.startsWith('video/')) {
showToast('请上传视频文件');
f.status = 'failed';
f.message = '格式错误';
videoFileList.value = [];
cloudVideoPath.value = '';
return;
}
uploadFile(f, (res) => {
const body = res?.data?.data ?? res?.data ?? res;
const filePath =
typeof body === 'string'
? body
: (body && (body.url || body.fileUrl || body.path)) || '';
if (filePath) {
if (!isVideo(filePath) && f.file && !f.file.type.startsWith('video/')) {
showToast('请上传视频文件');
videoFileList.value = [];
cloudVideoPath.value = '';
return;
}
cloudVideoPath.value = filePath;
f.url = toFullFileUrl(filePath);
f.isImage = false;
f.status = 'done';
} else {
f.status = 'failed';
f.message = '上传失败';
cloudVideoPath.value = '';
}
});
});
};
const validateForm = () => {
if (!cloudVideoPath.value) {
showToast('请上传云视频');
return false;
}
if (!cloudText.value.trim()) {
showToast('请输入云文字');
return false;
}
return true;
};
const onBack = () => {
router.back();
};
const onSubmit = async () => {
if (!validateForm()) return;
const taskId = route.query.id;
if (!taskId) {
showToast('任务信息异常');
return;
}
try {
await showConfirmDialog({
title: '确认提交',
message: '确认提交云笔录材料吗?',
width: '80%',
});
submitting.value = true;
const res = await submitCloudRecordApi({
taskId,
cloudVideo: toRelativeFilePath(cloudVideoPath.value),
cloudText: cloudText.value.trim(),
});
if (res && res.code === 200) {
showSuccessToast('提交成功');
router.back();
} else {
showToast(res?.msg || '提交失败');
}
} catch (err) {
if (err !== 'cancel') {
console.error(err);
showToast('提交失败');
}
} finally {
submitting.value = false;
}
};
onMounted(() => {
if (isViewOnly.value) {
loading.value = true;
fetchDetail();
}
});
</script>
<style lang="less" scoped>
.cloud-record-page {
display: flex;
flex-direction: column;
height: 100%;
background: #f5f7fa;
overflow: hidden;
}
.cloud-record-body {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
background: #fff;
}
.cloud-record-tabs {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
:deep(.van-tabs__wrap) {
flex-shrink: 0;
border-bottom: 1px solid #eee;
}
:deep(.van-tabs__content) {
flex: 1;
min-height: 0;
}
:deep(.van-tab__panel) {
height: 100%;
}
:deep(.van-tab) {
flex: 1;
justify-content: center;
}
}
.tab-scroll {
height: 100%;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
background: #f5f7fa;
padding: 12px;
}
.loading-wrap {
display: flex;
justify-content: center;
padding-top: 120px;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding-top: 120px;
.empty-text {
margin-top: 16px;
font-size: 16px;
color: #323233;
font-weight: 500;
}
}
.content-scroll {
padding: 0;
}
.section-card {
background: #fff;
border-radius: 8px;
padding: 12px;
margin-bottom: 12px;
}
.section-title {
font-size: 15px;
font-weight: 600;
color: #323233;
margin-bottom: 12px;
}
.video-wrap {
position: relative;
width: 100%;
height: 180px;
border-radius: 8px;
overflow: hidden;
background: #000;
.video-thumb {
width: 100%;
height: 100%;
object-fit: cover;
}
.play-icon {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 48px;
color: rgba(255, 255, 255, 0.9);
}
}
.media-placeholder {
height: 120px;
display: flex;
align-items: center;
justify-content: center;
background: #f7f8fa;
border-radius: 8px;
color: #969799;
font-size: 14px;
}
.text-content {
font-size: 14px;
color: #323233;
line-height: 22px;
white-space: pre-wrap;
word-break: break-all;
}
.form-card {
background: #fff;
border-radius: 8px;
padding: 16px 12px;
}
.form-row {
& + & {
margin-top: 20px;
}
&--textarea {
.form-label {
margin-bottom: 8px;
}
}
}
.form-label {
font-size: 14px;
color: #323233;
line-height: 20px;
margin-bottom: 12px;
}
.required-mark {
color: #ee0a24;
margin-right: 2px;
}
.form-value {
:deep(.van-uploader) {
display: block;
}
:deep(.van-uploader__wrapper) {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
:deep(.van-uploader__preview) {
margin: 0;
}
}
.uploader-btn {
width: 80px;
height: 80px;
background-color: #f7f8fa;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
border: 1px dashed #ebedf0;
}
.form-tip {
margin-top: 8px;
font-size: 12px;
color: #969799;
line-height: 18px;
}
.text-field {
padding: 0;
background: #fff;
border: 1px solid #ebedf0;
border-radius: 4px;
:deep(.van-field__control) {
min-height: 160px;
padding: 8px 12px;
}
:deep(.van-field__word-limit) {
padding: 0 12px 8px;
}
}
.page-footer {
flex-shrink: 0;
display: flex;
gap: 12px;
padding: 12px 16px;
background: #fff;
box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.06);
.btn-cancel,
.btn-submit {
flex: 1;
height: 44px;
border-radius: 22px;
font-size: 16px;
}
.btn-cancel {
border: 1px solid #2b7de9;
color: #2b7de9;
background: #fff;
}
.btn-submit {
border: none;
background: linear-gradient(135deg, #1367fe 0%, #4a8cff 100%);
}
}
.video-preview-popup {
width: 100vw;
height: 100vh;
background-color: #000;
display: flex;
align-items: center;
justify-content: center;
.preview-video {
width: 100%;
max-height: 100%;
outline: none;
}
:deep(.van-popup__close-icon) {
color: #fff;
font-size: 24px;
z-index: 2;
}
}
</style>

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

@ -33,7 +33,9 @@
v-model:inspect-result="inspectResult" v-model:inspect-result="inspectResult"
:task-status="taskDetail.taskStatus" :task-status="taskDetail.taskStatus"
:evaluate-data="evaluateData" :evaluate-data="evaluateData"
:digital-voice-logging-status="taskDetail.digitalVoiceLoggingStatus"
:is-edit="Number(taskDetail.taskStatus) === 1 || (Number(taskDetail.taskStatus) === 2 && isEdit)" :is-edit="Number(taskDetail.taskStatus) === 1 || (Number(taskDetail.taskStatus) === 2 && isEdit)"
@view-cloud-record="onViewCloudRecord"
/> />
</div> </div>
</van-tab> </van-tab>
@ -46,6 +48,10 @@
<div v-if="activeTab === 'materials' && Number(taskDetail.taskStatus) === 2 && !isEdit" class="detail-footer"> <div v-if="activeTab === 'materials' && Number(taskDetail.taskStatus) === 2 && !isEdit" class="detail-footer">
<van-button class="btn-submit" type="primary" @click="isEdit = true">修改</van-button> <van-button class="btn-submit" type="primary" @click="isEdit = true">修改</van-button>
</div> </div>
<!-- 云笔录 -->
<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> </van-form>
</div> </div>
</template> </template>
@ -97,6 +103,7 @@ const applyDetailData = (data) => {
startTime: data.createTime || '', startTime: data.createTime || '',
deadline: data.deadline || '', deadline: data.deadline || '',
taskTypeId: data.taskTypeId || '', taskTypeId: data.taskTypeId || '',
digitalVoiceLoggingStatus: data.digitalVoiceLoggingStatus || '',
}; };
taskItemList.value = (data.taskItemList || []).map((item) => { taskItemList.value = (data.taskItemList || []).map((item) => {
const { fileList, ...rest } = item const { fileList, ...rest } = item
@ -172,6 +179,28 @@ const onCancel = () => {
router.back(); router.back();
}; };
const onCloudRecord = () => {
if (!route.query.id) {
showToast('任务信息异常');
return;
}
router.push({
name: 'nosCloudRecord',
query: { id: route.query.id, tab: route.query.tab },
});
};
const onViewCloudRecord = () => {
if (!route.query.id) {
showToast('任务信息异常');
return;
}
router.push({
name: 'nosCloudRecord',
query: { id: route.query.id, tab: route.query.tab, taskStatus: 4 },
});
};
const validateMaterials = () => { const validateMaterials = () => {
if (isEdit.value || Number(taskDetail.value.taskStatus) === 1) { if (isEdit.value || Number(taskDetail.value.taskStatus) === 1) {
const allFilled = taskItemList.value.every(item => item.fileList && item.fileList.length > 0); const allFilled = taskItemList.value.every(item => item.fileList && item.fileList.length > 0);

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' VITE_NOS_BASE_URL = '/nosapi'
# 仅 dev server 代理目标(生产由 nginx 转发,不读这两项) # 仅 dev server 代理目标(生产由 nginx 转发,不读这两项)
VITE_PROXY_TARGET = 'http://139.186.148.21:8080' VITE_PROXY_TARGET = 'http://139.186.148.21:8080'
VITE_NOS_PROXY_TARGET = 'http://192.168.1.12:28080' VITE_NOS_PROXY_TARGET = 'http://192.168.1.11:28080'

Loading…
Cancel
Save