committed by
masiyuan
36 changed files with 3791 additions and 473 deletions
|
After Width: | Height: | Size: 7.1 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 7.4 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 7.8 KiB |
|
After Width: | Height: | Size: 14 KiB |
@ -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(/ /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> |
|||
@ -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> |
|||
@ -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> |
|||
@ -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> |
|||
@ -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" |
|||
} |
|||
@ -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> |
|||
@ -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> |
|||
@ -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> |
|||
Loading…
Reference in new issue