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