21 changed files with 2340 additions and 137 deletions
@ -1,22 +1,99 @@ |
|||
package com.zdxt.enforcementcode.controller.app.enforcement; |
|||
|
|||
import com.zdxt.enforcementcode.controller.web.supervision.EfcodeCpNoticeController; |
|||
import com.zdxt.enforcementcode.domain.bo.EfcodeCpNoticeBo; |
|||
import com.zdxt.enforcementcode.domain.bo.EfcodeCpNoticeMessageBo; |
|||
import com.zdxt.enforcementcode.domain.vo.EfcodeCpNoticeMessageVo; |
|||
import com.zdxt.enforcementcode.domain.vo.EfcodeCpNoticeVo; |
|||
import com.zdxt.enforcementcode.service.IEfcodeCpNoticeMessageService; |
|||
import com.zdxt.enforcementcode.service.IEfcodeCpNoticeService; |
|||
import jakarta.validation.constraints.NotBlank; |
|||
import jakarta.validation.constraints.NotEmpty; |
|||
import lombok.RequiredArgsConstructor; |
|||
import org.dromara.common.core.domain.R; |
|||
import org.dromara.common.mybatis.core.page.PageQuery; |
|||
import org.dromara.common.mybatis.core.page.TableDataInfo; |
|||
import org.springframework.validation.annotation.Validated; |
|||
import org.springframework.web.bind.annotation.*; |
|||
|
|||
import java.util.List; |
|||
|
|||
/** |
|||
* 行政执法检查计划-预约 |
|||
* 执法端-预告管理 |
|||
* |
|||
* @author Lion Li |
|||
* @date 2026-05-16 |
|||
* @author zdxt |
|||
* @date 2026-05-28 |
|||
*/ |
|||
@Validated |
|||
@RequiredArgsConstructor |
|||
@RestController |
|||
@RequestMapping("/app-enf/cpNotice") |
|||
public class AppEnfCpNoticeController extends EfcodeCpNoticeController { |
|||
public class AppEnfCpNoticeController { |
|||
|
|||
private final IEfcodeCpNoticeService efcodeCpNoticeService; |
|||
private final IEfcodeCpNoticeMessageService efcodeCpNoticeMessageService; |
|||
|
|||
/** |
|||
* 查询我的预告列表(分页,只返回当前用户创建的预告) |
|||
* 通过status区分tab:0=草稿, 1=待执法(已发布), 3=历史(已执行) |
|||
*/ |
|||
@GetMapping("/myList") |
|||
public TableDataInfo<EfcodeCpNoticeVo> myList(EfcodeCpNoticeBo bo, PageQuery pageQuery) { |
|||
return efcodeCpNoticeService.queryMyPageList(bo, pageQuery); |
|||
} |
|||
|
|||
/** |
|||
* 获取预告详情 |
|||
*/ |
|||
@GetMapping("/{id}") |
|||
public R<EfcodeCpNoticeVo> getInfo(@NotBlank(message = "主键不能为空") @PathVariable String id) { |
|||
return R.ok(efcodeCpNoticeService.queryById(id)); |
|||
} |
|||
|
|||
/** |
|||
* 保存预告(草稿/发布) |
|||
* status=0 存为草稿, status=1 发布预告 |
|||
*/ |
|||
@PostMapping() |
|||
public R<String> save(@RequestBody EfcodeCpNoticeBo bo) { |
|||
return R.ok(efcodeCpNoticeService.saveNotice(bo)); |
|||
} |
|||
|
|||
/** |
|||
* 修改预告(仅草稿状态可修改) |
|||
*/ |
|||
@PutMapping() |
|||
public R<String> update(@RequestBody EfcodeCpNoticeBo bo) { |
|||
return R.ok(efcodeCpNoticeService.updateNotice(bo)); |
|||
} |
|||
|
|||
/** |
|||
* 删除预告(仅草稿状态可删除) |
|||
*/ |
|||
@DeleteMapping("/{ids}") |
|||
public R<String> remove(@NotEmpty(message = "主键不能为空") @PathVariable String[] ids) { |
|||
return R.ok(efcodeCpNoticeService.deleteNotice(List.of(ids))); |
|||
} |
|||
|
|||
// =================== 留言相关 ===================
|
|||
|
|||
/** |
|||
* 查询某个预告的留言列表 |
|||
*/ |
|||
@GetMapping("/message/list") |
|||
public R<List<EfcodeCpNoticeMessageVo>> messageList(@RequestParam String noticeId) { |
|||
EfcodeCpNoticeMessageBo bo = new EfcodeCpNoticeMessageBo(); |
|||
bo.setNoticeId(noticeId); |
|||
bo.setIsDelete(0L); |
|||
return R.ok(efcodeCpNoticeMessageService.queryList(bo)); |
|||
} |
|||
|
|||
public AppEnfCpNoticeController(IEfcodeCpNoticeService efcodeCpNoticeService) { |
|||
super(efcodeCpNoticeService); |
|||
/** |
|||
* 新增留言 |
|||
*/ |
|||
@PostMapping("/message") |
|||
public R<Void> addMessage(@RequestBody EfcodeCpNoticeMessageBo bo) { |
|||
bo.setIsDelete(0L); |
|||
efcodeCpNoticeMessageService.insertByBo(bo); |
|||
return R.ok(); |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,888 @@ |
|||
<template> |
|||
<div class="notice-edit-page app_content"> |
|||
<!-- 顶部导航 --> |
|||
<van-nav-bar |
|||
:title="pageTitle" |
|||
left-arrow |
|||
@click-left="onBack" |
|||
/> |
|||
|
|||
<div class="content"> |
|||
<!-- 表单 --> |
|||
<div class="form-card"> |
|||
<!-- 从计划导入 --> |
|||
<van-field |
|||
v-model="planImportDisplay" |
|||
label="从计划导入" |
|||
placeholder="请选择计划" |
|||
readonly |
|||
is-link |
|||
@click="showPlanPicker = true" |
|||
/> |
|||
|
|||
<!-- 执法日期 --> |
|||
<van-field |
|||
v-model="form.enforceDate" |
|||
label="执法日期" |
|||
placeholder="请选择" |
|||
required |
|||
readonly |
|||
is-link |
|||
@click="showDatePicker = true" |
|||
/> |
|||
|
|||
<!-- 执法时段 --> |
|||
<van-field |
|||
v-model="enforceTimeDisplay" |
|||
label="执法时段" |
|||
placeholder="请选择" |
|||
required |
|||
readonly |
|||
is-link |
|||
@click="showTimePicker = true" |
|||
/> |
|||
|
|||
<!-- 执法单位 --> |
|||
<van-field |
|||
v-model="form.bureauDeptName" |
|||
label="执法单位" |
|||
required |
|||
readonly |
|||
/> |
|||
|
|||
<!-- 执法人员 --> |
|||
<van-field label="执法人员" required> |
|||
<template #input> |
|||
<div class="person-box"> |
|||
<div |
|||
v-for="(person, index) in personList" |
|||
:key="index" |
|||
class="person-line" |
|||
> |
|||
<span class="person-name">{{ person.name }}</span> |
|||
<span class="person-phone">{{ person.phone }}</span> |
|||
<van-icon |
|||
v-if="personList.length > 1" |
|||
name="close" |
|||
class="person-remove" |
|||
@click="removePerson(index)" |
|||
/> |
|||
</div> |
|||
<div class="add-person-line" @click="onAddPerson"> |
|||
<span class="placeholder-text">请输入</span> |
|||
<van-icon name="add-o" color="#2382cf" size="20" /> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
</van-field> |
|||
|
|||
<!-- 企业名称 --> |
|||
<van-field |
|||
v-model="form.enterpriseName" |
|||
label="企业名称" |
|||
placeholder="请选择企业" |
|||
required |
|||
readonly |
|||
is-link |
|||
@click="onOpenEnterprisePicker" |
|||
/> |
|||
|
|||
<!-- 企业联系人 --> |
|||
<van-field |
|||
v-model="form.contactPerson" |
|||
label="企业联系人" |
|||
placeholder="请输入联系人" |
|||
/> |
|||
|
|||
<!-- 企业联系电话 --> |
|||
<van-field |
|||
v-model="form.contactPhone" |
|||
label="企业联系电话" |
|||
placeholder="请输入联系电话" |
|||
/> |
|||
|
|||
<!-- 执法事项 --> |
|||
<van-field |
|||
v-model="form.enforceContent" |
|||
label="执法事项" |
|||
type="textarea" |
|||
placeholder="请输入执法事项" |
|||
required |
|||
:maxlength="1000" |
|||
show-word-limit |
|||
rows="3" |
|||
autosize |
|||
/> |
|||
|
|||
<!-- 执法要求 --> |
|||
<van-field |
|||
v-model="form.enforceRequirement" |
|||
label="执法要求" |
|||
type="textarea" |
|||
placeholder="请输入执法要求" |
|||
required |
|||
:maxlength="1000" |
|||
show-word-limit |
|||
rows="2" |
|||
autosize |
|||
/> |
|||
</div> |
|||
|
|||
<!-- 底部按钮 --> |
|||
<div class="submit-wrap"> |
|||
<van-button |
|||
plain |
|||
type="default" |
|||
class="draft-btn" |
|||
:loading="savingDraft" |
|||
loading-text="保存中..." |
|||
@click="onSaveDraft" |
|||
> |
|||
保存草稿 |
|||
</van-button> |
|||
<van-button |
|||
type="primary" |
|||
class="publish-btn" |
|||
:loading="publishing" |
|||
loading-text="发布中..." |
|||
@click="onPublish" |
|||
> |
|||
提交预告 |
|||
</van-button> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- 日期+时段选择器 --> |
|||
<van-popup v-model:show="showDatePicker" position="bottom" round> |
|||
<van-date-picker |
|||
v-model="datePickerValue" |
|||
title="选择执法日期" |
|||
:min-date="minDate" |
|||
@confirm="onDateConfirm" |
|||
@cancel="showDatePicker = false" |
|||
/> |
|||
</van-popup> |
|||
|
|||
<!-- 时段选择 --> |
|||
<van-popup v-model:show="showTimePicker" position="bottom" round> |
|||
<van-picker |
|||
:columns="timeColumns" |
|||
@confirm="onTimeConfirm" |
|||
@cancel="showTimePicker = false" |
|||
/> |
|||
</van-popup> |
|||
|
|||
<!-- 计划选择弹层 --> |
|||
<van-popup v-model:show="showPlanPicker" position="bottom" round :style="{ height: '60%' }"> |
|||
<div class="plan-picker"> |
|||
<div class="plan-picker-header"> |
|||
<span class="plan-picker-title">选择我的计划</span> |
|||
<van-icon name="cross" @click="showPlanPicker = false" /> |
|||
</div> |
|||
<div class="plan-picker-body"> |
|||
<div v-if="planLoading" class="plan-loading"> |
|||
<van-loading size="18" /> 加载中... |
|||
</div> |
|||
<template v-else> |
|||
<div |
|||
v-for="(plan, idx) in planOptions" |
|||
:key="plan.id || idx" |
|||
class="plan-item" |
|||
@click="onSelectPlan(plan)" |
|||
> |
|||
<div class="plan-item-title">{{ plan.enforceContent || '无事项' }}</div> |
|||
<div class="plan-item-meta"> |
|||
{{ plan.planDate }} | {{ plan.enterpriseName }} |
|||
</div> |
|||
</div> |
|||
<van-empty v-if="!planOptions.length" description="暂无可导入计划" image-size="80" /> |
|||
</template> |
|||
</div> |
|||
</div> |
|||
</van-popup> |
|||
|
|||
<!-- 执法人员检索弹层 --> |
|||
<van-popup |
|||
v-model:show="showPersonPicker" |
|||
position="bottom" |
|||
round |
|||
:style="{ height: '70%' }" |
|||
> |
|||
<div class="person-picker"> |
|||
<div class="person-picker-header"> |
|||
<span class="person-picker-title">选择执法人员</span> |
|||
<van-icon name="cross" @click="showPersonPicker = false" /> |
|||
</div> |
|||
<van-search |
|||
v-model="personSearchKeyword" |
|||
placeholder="输入姓名/账号检索单位人员" |
|||
shape="round" |
|||
@update:model-value="onPersonSearchInput" |
|||
/> |
|||
<div class="person-picker-body"> |
|||
<div v-if="personSearching" class="person-loading"> |
|||
<van-loading size="18" /> 加载中... |
|||
</div> |
|||
<template v-else> |
|||
<div |
|||
v-for="(user, idx) in personSearchResult" |
|||
:key="user.userId || idx" |
|||
class="person-item" |
|||
@click="onSelectPerson(user)" |
|||
> |
|||
<div class="person-item-name">{{ user.fullname || user.userName }}</div> |
|||
<div class="person-item-meta"> |
|||
<span v-if="user.mobileNumber">{{ user.mobileNumber }}</span> |
|||
<span v-if="user.loginName" class="person-item-login">@{{ user.loginName }}</span> |
|||
</div> |
|||
</div> |
|||
<van-empty v-if="!personSearchResult.length" description="未检索到人员" image-size="80" /> |
|||
</template> |
|||
</div> |
|||
</div> |
|||
</van-popup> |
|||
|
|||
<!-- 企业检索弹层 --> |
|||
<van-popup |
|||
v-model:show="showEnterprisePicker" |
|||
position="bottom" |
|||
round |
|||
:style="{ height: '70%' }" |
|||
> |
|||
<div class="enterprise-picker"> |
|||
<div class="enterprise-picker-header"> |
|||
<span class="enterprise-picker-title">选择企业</span> |
|||
<van-icon name="cross" @click="showEnterprisePicker = false" /> |
|||
</div> |
|||
<van-search |
|||
v-model="enterpriseSearchKeyword" |
|||
placeholder="输入≥ 3 个字符检索企业,不要输入深圳,深圳市" |
|||
shape="round" |
|||
@update:model-value="onEnterpriseSearchInput" |
|||
/> |
|||
<div class="enterprise-picker-body"> |
|||
<div v-if="enterpriseSearching" class="enterprise-loading"> |
|||
<van-loading size="18" /> 检索中... |
|||
</div> |
|||
<template v-else> |
|||
<div |
|||
v-for="(item, idx) in enterpriseOptions" |
|||
:key="idx" |
|||
class="enterprise-item" |
|||
@click="onSelectEnterprise(item)" |
|||
> |
|||
<div class="enterprise-item-name">{{ item.enterpriseName }}</div> |
|||
<div class="enterprise-item-number">{{ item.enterpriseNumber }}</div> |
|||
</div> |
|||
<van-empty v-if="enterpriseSearchKeyword.length >= 3 && !enterpriseOptions.length" description="未检索到匹配企业" image-size="80" /> |
|||
</template> |
|||
</div> |
|||
</div> |
|||
</van-popup> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup name="cpNoticeEdit"> |
|||
import { useRouter, useRoute } from 'vue-router'; |
|||
import { showToast, showSuccessToast, showConfirmDialog } from 'vant'; |
|||
import { addNotice, editNotice, getNoticeDetail } from '@/api/enforcement/notice'; |
|||
import { getPlanList, searchUnionByEnterpriseName, searchBureauUnitUsers, getEnterpriseContactInfo } from '@/api/enforcement/plan'; |
|||
|
|||
const router = useRouter(); |
|||
const route = useRoute(); |
|||
const { useCommon } = $globalStore; |
|||
|
|||
const isEdit = computed(() => !!route.query.id); |
|||
const isReinitiate = computed(() => !!route.query.reinitiate); |
|||
const pageTitle = computed(() => { |
|||
if (isReinitiate.value) return '新增预告'; |
|||
return isEdit.value ? '编辑预告' : '新增预告'; |
|||
}); |
|||
|
|||
const savingDraft = ref(false); |
|||
const publishing = ref(false); |
|||
const showDatePicker = ref(false); |
|||
const showTimePicker = ref(false); |
|||
const showPlanPicker = ref(false); |
|||
|
|||
// 表单数据 |
|||
const form = ref({ |
|||
id: '', |
|||
planId: null, |
|||
enforceDate: '', |
|||
enforceTime: 1, |
|||
bureauDeptId: '', |
|||
bureauDeptName: '', |
|||
organId: '', |
|||
organName: '', |
|||
deptId: '', |
|||
deptName: '', |
|||
enforceUserNames: '', |
|||
enforceUserIds: '', |
|||
enforceUserTels: '', |
|||
enforceUserCodes: '', |
|||
enterpriseId: '', |
|||
enterpriseName: '', |
|||
enterpriseNumber: '', |
|||
contactPerson: '', |
|||
contactPhone: '', |
|||
enforceContent: '', |
|||
enforceRequirement: '', |
|||
status: 0, |
|||
}); |
|||
|
|||
// 人员列表 |
|||
const personList = ref([]); |
|||
const maxPersonNum = 3; |
|||
|
|||
// 日期相关 |
|||
const today = new Date(); |
|||
const minDate = new Date(today.getFullYear(), today.getMonth(), today.getDate()); |
|||
const datePickerValue = ref([ |
|||
String(today.getFullYear()), |
|||
String(today.getMonth() + 1).padStart(2, '0'), |
|||
String(today.getDate()).padStart(2, '0'), |
|||
]); |
|||
|
|||
const timeColumns = [ |
|||
{ text: '上午', value: 1 }, |
|||
{ text: '下午', value: 2 }, |
|||
{ text: '其他', value: 3 }, |
|||
]; |
|||
|
|||
// 日期+时段展示 |
|||
const enforceTimeDisplay = computed(() => { |
|||
const timeText = { 1: '上午', 2: '下午', 3: '其他' }; |
|||
return timeText[form.value.enforceTime] || ''; |
|||
}); |
|||
|
|||
// 计划导入展示 |
|||
const planImportDisplay = computed(() => { |
|||
if (form.value.planId) return '已导入计划'; |
|||
return ''; |
|||
}); |
|||
|
|||
// 企业搜索状态 |
|||
const enterpriseOptions = ref([]); |
|||
const showEnterprisePicker = ref(false); |
|||
const enterpriseSearchKeyword = ref(''); |
|||
const enterpriseSearching = ref(false); |
|||
let enterpriseSearchTimer = null; |
|||
let enterpriseSearchSeq = 0; |
|||
|
|||
// 执法人员检索 |
|||
const showPersonPicker = ref(false); |
|||
const personSearchKeyword = ref(''); |
|||
const personSearchResult = ref([]); |
|||
const personSearching = ref(false); |
|||
let personSearchTimer = null; |
|||
let personSearchSeq = 0; |
|||
|
|||
// 计划列表 |
|||
const planOptions = ref([]); |
|||
const planLoading = ref(false); |
|||
|
|||
onMounted(() => { |
|||
// 默认绑定当前用户的执法单位 |
|||
form.value.bureauDeptId = useCommon.bureauUnitId || ''; |
|||
form.value.bureauDeptName = useCommon.bureauUnitName || ''; |
|||
form.value.organId = useCommon.organId || ''; |
|||
form.value.organName = useCommon.organName || ''; |
|||
form.value.deptId = useCommon.deptId || ''; |
|||
form.value.deptName = useCommon.deptName || ''; |
|||
// 默认绑定当前用户为首位执法人员 |
|||
bindCurrentUserAsDefaultPerson(); |
|||
|
|||
if (isEdit.value) { |
|||
loadDetail(route.query.id); |
|||
} else if (isReinitiate.value) { |
|||
loadDetail(route.query.reinitiate, true); |
|||
} |
|||
}); |
|||
|
|||
const bindCurrentUserAsDefaultPerson = () => { |
|||
const name = useCommon.nickName || useCommon.userName || ''; |
|||
const phone = useCommon.phonenumber || ''; |
|||
const id = useCommon.userId || ''; |
|||
if (!name && !id) return; |
|||
personList.value = [{ id, name, phone, code: '' }]; |
|||
syncPersonData(); |
|||
}; |
|||
|
|||
const loadDetail = (id, isReinitiateMode = false) => { |
|||
getNoticeDetail(id, (res) => { |
|||
if (res.code === 200 && res.data) { |
|||
const data = res.data; |
|||
Object.keys(form.value).forEach(key => { |
|||
if (data[key] !== undefined && data[key] !== null) { |
|||
form.value[key] = data[key]; |
|||
} |
|||
}); |
|||
// 如果是再次发起,清除id,变为新增 |
|||
if (isReinitiateMode) { |
|||
form.value.id = ''; |
|||
form.value.status = 0; |
|||
form.value.planId = null; |
|||
} |
|||
// 解析人员 |
|||
if (data.enforceUserNames) { |
|||
const names = data.enforceUserNames.split(','); |
|||
const ids = data.enforceUserIds ? data.enforceUserIds.split(',') : []; |
|||
const tels = data.enforceUserTels ? data.enforceUserTels.split(',') : []; |
|||
personList.value = names.map((name, i) => ({ |
|||
id: i < ids.length ? ids[i] : '', |
|||
name, |
|||
phone: i < tels.length ? tels[i] : '', |
|||
code: '', |
|||
})); |
|||
} |
|||
} |
|||
}); |
|||
}; |
|||
|
|||
// 日期确认 |
|||
const onDateConfirm = ({ selectedValues }) => { |
|||
form.value.enforceDate = selectedValues.join('-'); |
|||
showDatePicker.value = false; |
|||
}; |
|||
|
|||
// 时段确认 |
|||
const onTimeConfirm = ({ selectedValues }) => { |
|||
form.value.enforceTime = selectedValues[0]; |
|||
showTimePicker.value = false; |
|||
}; |
|||
|
|||
// 打开企业检索弹层 |
|||
const onOpenEnterprisePicker = () => { |
|||
enterpriseSearchKeyword.value = ''; |
|||
enterpriseOptions.value = []; |
|||
showEnterprisePicker.value = true; |
|||
}; |
|||
|
|||
// 企业检索 |
|||
const onEnterpriseSearchInput = (val) => { |
|||
if (enterpriseSearchTimer) clearTimeout(enterpriseSearchTimer); |
|||
const keyword = (val || '').trim(); |
|||
if (keyword.length < 3) { |
|||
enterpriseOptions.value = []; |
|||
return; |
|||
} |
|||
enterpriseSearchTimer = setTimeout(() => { |
|||
const seq = ++enterpriseSearchSeq; |
|||
enterpriseSearching.value = true; |
|||
searchUnionByEnterpriseName(keyword, (res) => { |
|||
if (seq !== enterpriseSearchSeq) return; |
|||
enterpriseSearching.value = false; |
|||
if (res && res.code === 200 && Array.isArray(res.data)) { |
|||
enterpriseOptions.value = res.data; |
|||
} else { |
|||
enterpriseOptions.value = []; |
|||
} |
|||
}, () => { |
|||
if (seq !== enterpriseSearchSeq) return; |
|||
enterpriseSearching.value = false; |
|||
enterpriseOptions.value = []; |
|||
}); |
|||
}, 350); |
|||
}; |
|||
|
|||
const onSelectEnterprise = (item) => { |
|||
form.value.enterpriseName = item.enterpriseName || ''; |
|||
form.value.enterpriseNumber = item.enterpriseNumber || ''; |
|||
form.value.enterpriseId = item.enterpriseNumber || ''; |
|||
showEnterprisePicker.value = false; |
|||
enterpriseOptions.value = []; |
|||
// 根据企业信用代码拉取联系人和联系电话 |
|||
if (item.enterpriseNumber) { |
|||
getEnterpriseContactInfo(item.enterpriseNumber, (res) => { |
|||
if (res && res.code === 200 && res.data) { |
|||
if (res.data.contactPerson) { |
|||
form.value.contactPerson = res.data.contactPerson; |
|||
} |
|||
if (res.data.contactPhone) { |
|||
form.value.contactPhone = res.data.contactPhone; |
|||
} |
|||
} |
|||
}); |
|||
} |
|||
}; |
|||
|
|||
// 人员添加 |
|||
const onAddPerson = () => { |
|||
if (personList.value.length >= maxPersonNum) { |
|||
showToast(`最多只能添加 ${maxPersonNum} 名执法人员`); |
|||
return; |
|||
} |
|||
personSearchKeyword.value = ''; |
|||
personSearchResult.value = []; |
|||
showPersonPicker.value = true; |
|||
triggerPersonSearch(''); |
|||
}; |
|||
|
|||
const onPersonSearchInput = (val) => { |
|||
if (personSearchTimer) clearTimeout(personSearchTimer); |
|||
const keyword = (val || '').trim(); |
|||
personSearchTimer = setTimeout(() => triggerPersonSearch(keyword), 300); |
|||
}; |
|||
|
|||
const triggerPersonSearch = (keyword) => { |
|||
const seq = ++personSearchSeq; |
|||
personSearching.value = true; |
|||
searchBureauUnitUsers( |
|||
{ sysDeptId: useCommon.bureauUnitId || '', keyword, globalSearch: false, pageNum: 1, pageSize: 30 }, |
|||
(res) => { |
|||
if (seq !== personSearchSeq) return; |
|||
personSearching.value = false; |
|||
if (res && res.code === 200 && Array.isArray(res.rows)) { |
|||
personSearchResult.value = res.rows; |
|||
} else { |
|||
personSearchResult.value = []; |
|||
} |
|||
}, |
|||
() => { |
|||
if (seq !== personSearchSeq) return; |
|||
personSearching.value = false; |
|||
personSearchResult.value = []; |
|||
} |
|||
); |
|||
}; |
|||
|
|||
const onSelectPerson = (user) => { |
|||
const uid = user.userId != null ? String(user.userId) : ''; |
|||
const exists = personList.value.some(p => String(p.id || '') === uid); |
|||
if (exists) { showToast('该人员已添加'); return; } |
|||
if (personList.value.length >= maxPersonNum) { showToast(`最多只能添加 ${maxPersonNum} 名执法人员`); return; } |
|||
personList.value.push({ id: uid, name: user.fullname || user.userName || '', phone: user.mobileNumber || '', code: '' }); |
|||
syncPersonData(); |
|||
showPersonPicker.value = false; |
|||
}; |
|||
|
|||
const removePerson = (index) => { |
|||
personList.value.splice(index, 1); |
|||
syncPersonData(); |
|||
}; |
|||
|
|||
const syncPersonData = () => { |
|||
form.value.enforceUserNames = personList.value.map(p => p.name).join(','); |
|||
form.value.enforceUserIds = personList.value.map(p => p.id || '').join(','); |
|||
form.value.enforceUserTels = personList.value.map(p => p.phone || '').join(','); |
|||
form.value.enforceUserCodes = personList.value.map(p => p.code || '').join(','); |
|||
}; |
|||
|
|||
// 计划导入 |
|||
watch(showPlanPicker, (val) => { |
|||
if (val && planOptions.value.length === 0) { |
|||
loadPlans(); |
|||
} |
|||
}); |
|||
|
|||
const loadPlans = () => { |
|||
planLoading.value = true; |
|||
getPlanList({ pageNum: 1, pageSize: 50 }, (res) => { |
|||
planLoading.value = false; |
|||
if (res.code === 200) { |
|||
planOptions.value = res.rows || []; |
|||
} |
|||
}, () => { planLoading.value = false; }); |
|||
}; |
|||
|
|||
const onSelectPlan = (plan) => { |
|||
form.value.planId = plan.id; |
|||
form.value.enforceDate = plan.planDate || ''; |
|||
form.value.enforceTime = plan.planTime || 1; |
|||
form.value.enterpriseName = plan.enterpriseName || ''; |
|||
form.value.enterpriseNumber = plan.enterpriseNumber || ''; |
|||
form.value.enterpriseId = plan.enterpriseId || plan.enterpriseNumber || ''; |
|||
form.value.contactPerson = plan.contactPerson || ''; |
|||
form.value.contactPhone = plan.contactPhone || ''; |
|||
form.value.enforceContent = plan.enforceContent || ''; |
|||
// 同步人员 |
|||
if (plan.enforceUserNames) { |
|||
const names = plan.enforceUserNames.split(','); |
|||
const ids = plan.enforceUserIds ? plan.enforceUserIds.split(',') : []; |
|||
const tels = plan.enforceUserTels ? plan.enforceUserTels.split(',') : []; |
|||
personList.value = names.map((name, i) => ({ |
|||
id: i < ids.length ? ids[i] : '', |
|||
name, |
|||
phone: i < tels.length ? tels[i] : '', |
|||
code: '', |
|||
})); |
|||
syncPersonData(); |
|||
} |
|||
showPlanPicker.value = false; |
|||
}; |
|||
|
|||
// 返回 |
|||
const onBack = () => { router.back(); }; |
|||
|
|||
// 表单校验 |
|||
const validate = (isDraft) => { |
|||
if (!isDraft) { |
|||
if (!form.value.enforceDate) { showToast('请选择执法日期'); return false; } |
|||
if (!form.value.enforceTime) { showToast('请选择执法时段'); return false; } |
|||
if (!form.value.enforceContent) { showToast('请输入执法事项'); return false; } |
|||
if (!form.value.enforceRequirement) { showToast('请输入执法要求'); return false; } |
|||
if (!form.value.enterpriseName) { showToast('请输入企业名称'); return false; } |
|||
if (personList.value.length === 0) { showToast('请添加执法人员'); return false; } |
|||
} |
|||
return true; |
|||
}; |
|||
|
|||
// 存为草稿 |
|||
const onSaveDraft = () => { |
|||
if (!validate(true)) return; |
|||
form.value.status = 0; |
|||
doSubmit(true); |
|||
}; |
|||
|
|||
// 发布预告 |
|||
const onPublish = () => { |
|||
if (!validate(false)) return; |
|||
showConfirmDialog({ |
|||
title: '提示', |
|||
message: '预告发布后,不能删除,不能修改。确认发布?', |
|||
confirmButtonText: '确认发布', |
|||
cancelButtonText: '取消', |
|||
}).then(() => { |
|||
form.value.status = 1; |
|||
doSubmit(false); |
|||
}).catch(() => {}); |
|||
}; |
|||
|
|||
// 实际提交 |
|||
const doSubmit = (isDraft) => { |
|||
if (isDraft) { savingDraft.value = true; } else { publishing.value = true; } |
|||
// 兜底字段 |
|||
form.value.organId = form.value.organId || useCommon.organId || '-'; |
|||
form.value.organName = form.value.organName || useCommon.organName || '-'; |
|||
form.value.deptId = form.value.deptId || useCommon.deptId || '-'; |
|||
form.value.deptName = form.value.deptName || useCommon.deptName || '-'; |
|||
form.value.bureauDeptId = form.value.bureauDeptId || useCommon.bureauUnitId || '-'; |
|||
form.value.bureauDeptName = form.value.bureauDeptName || useCommon.bureauUnitName || '-'; |
|||
if (!form.value.enterpriseId) form.value.enterpriseId = form.value.enterpriseNumber || '-'; |
|||
|
|||
const api = isEdit.value ? editNotice : addNotice; |
|||
api( |
|||
form.value, |
|||
(res) => { |
|||
savingDraft.value = false; |
|||
publishing.value = false; |
|||
if (res.code === 200) { |
|||
showSuccessToast(isDraft ? '已保存草稿' : '发布成功'); |
|||
setTimeout(() => router.back(), 800); |
|||
} else { |
|||
showToast(res.msg || '操作失败'); |
|||
} |
|||
}, |
|||
(errRes) => { |
|||
savingDraft.value = false; |
|||
publishing.value = false; |
|||
showToast(errRes?.msg || '操作失败,请重试'); |
|||
} |
|||
); |
|||
}; |
|||
</script> |
|||
|
|||
<style lang="less" scoped> |
|||
.notice-edit-page { |
|||
display: flex; |
|||
flex-direction: column; |
|||
height: 100%; |
|||
background: #eaf4ff; |
|||
overflow: hidden; |
|||
} |
|||
|
|||
:deep(.van-nav-bar) { |
|||
background: #347bc3; |
|||
flex-shrink: 0; |
|||
.van-nav-bar__title { color: #fff; font-size: 18px; font-weight: 600; } |
|||
.van-icon { color: #fff; } |
|||
} |
|||
|
|||
.content { |
|||
flex: 1; |
|||
overflow-y: auto; |
|||
padding: 12px; |
|||
} |
|||
|
|||
.form-card { |
|||
background: #fff; |
|||
border-radius: 14px; |
|||
overflow: hidden; |
|||
box-shadow: 0 4px 14px rgba(44, 116, 190, 0.08); |
|||
:deep(.van-field) { padding: 14px; } |
|||
:deep(.van-field__label) { width: 90px; font-size: 15px; color: #333; } |
|||
} |
|||
|
|||
.person-box { |
|||
flex: 1; |
|||
display: flex; |
|||
flex-direction: column; |
|||
gap: 8px; |
|||
} |
|||
|
|||
.person-line { |
|||
display: flex; |
|||
align-items: center; |
|||
font-size: 15px; |
|||
color: #666; |
|||
gap: 8px; |
|||
.person-name { margin-right: 4px; } |
|||
.person-phone { color: #999; } |
|||
.person-remove { margin-left: auto; color: #999; font-size: 16px; } |
|||
} |
|||
|
|||
.add-person-line { |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: space-between; |
|||
color: #c4c9cf; |
|||
font-size: 15px; |
|||
cursor: pointer; |
|||
.placeholder-text { color: #c4c9cf; } |
|||
} |
|||
|
|||
.enterprise-picker { |
|||
display: flex; |
|||
flex-direction: column; |
|||
height: 100%; |
|||
.enterprise-picker-header { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
padding: 14px 16px 8px; |
|||
.enterprise-picker-title { font-size: 16px; font-weight: 600; color: #333; } |
|||
:deep(.van-icon) { font-size: 18px; color: #999; cursor: pointer; } |
|||
} |
|||
.enterprise-picker-body { |
|||
flex: 1; |
|||
overflow-y: auto; |
|||
padding: 0 12px 12px; |
|||
} |
|||
.enterprise-loading { |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
padding: 30px 0; |
|||
color: #999; |
|||
font-size: 14px; |
|||
} |
|||
.enterprise-item { |
|||
padding: 12px 14px; |
|||
background: #fff; |
|||
border: 1px solid #e6edf5; |
|||
border-radius: 10px; |
|||
margin-bottom: 8px; |
|||
cursor: pointer; |
|||
&:active { background: #f5faff; } |
|||
.enterprise-item-name { font-size: 15px; color: #333; font-weight: 500; } |
|||
.enterprise-item-number { font-size: 12px; color: #999; margin-top: 4px; } |
|||
} |
|||
} |
|||
|
|||
.submit-wrap { |
|||
display: flex; |
|||
gap: 16px; |
|||
padding: 22px 0 0; |
|||
.draft-btn { |
|||
flex: 1; |
|||
height: 50px; |
|||
font-size: 17px; |
|||
font-weight: 500; |
|||
border-radius: 25px; |
|||
} |
|||
.publish-btn { |
|||
flex: 1; |
|||
height: 50px; |
|||
font-size: 17px; |
|||
font-weight: 500; |
|||
border-radius: 25px; |
|||
background: linear-gradient(135deg, #1890ff, #0b63d1); |
|||
border: none; |
|||
box-shadow: 0 8px 20px rgba(24, 144, 255, 0.28); |
|||
} |
|||
} |
|||
|
|||
.plan-picker { |
|||
display: flex; |
|||
flex-direction: column; |
|||
height: 100%; |
|||
.plan-picker-header { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
padding: 14px 16px 8px; |
|||
.plan-picker-title { font-size: 16px; font-weight: 600; color: #333; } |
|||
:deep(.van-icon) { font-size: 18px; color: #999; cursor: pointer; } |
|||
} |
|||
.plan-picker-body { |
|||
flex: 1; |
|||
overflow-y: auto; |
|||
padding: 0 12px 12px; |
|||
} |
|||
.plan-loading { |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
padding: 30px 0; |
|||
color: #999; |
|||
font-size: 14px; |
|||
} |
|||
.plan-item { |
|||
padding: 12px 14px; |
|||
background: #fff; |
|||
border: 1px solid #e6edf5; |
|||
border-radius: 10px; |
|||
margin-bottom: 8px; |
|||
cursor: pointer; |
|||
&:active { background: #f5faff; } |
|||
.plan-item-title { font-size: 15px; color: #333; font-weight: 500; } |
|||
.plan-item-meta { font-size: 12px; color: #999; margin-top: 4px; } |
|||
} |
|||
} |
|||
|
|||
.person-picker { |
|||
display: flex; |
|||
flex-direction: column; |
|||
height: 100%; |
|||
.person-picker-header { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
padding: 14px 16px 8px; |
|||
.person-picker-title { font-size: 16px; font-weight: 600; color: #333; } |
|||
:deep(.van-icon) { font-size: 18px; color: #999; cursor: pointer; } |
|||
} |
|||
.person-picker-body { |
|||
flex: 1; |
|||
overflow-y: auto; |
|||
padding: 0 12px 12px; |
|||
} |
|||
.person-loading { |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
padding: 30px 0; |
|||
color: #999; |
|||
font-size: 14px; |
|||
} |
|||
.person-item { |
|||
padding: 12px 14px; |
|||
background: #fff; |
|||
border: 1px solid #e6edf5; |
|||
border-radius: 10px; |
|||
margin-bottom: 8px; |
|||
cursor: pointer; |
|||
&:active { background: #f5faff; } |
|||
.person-item-name { font-size: 15px; color: #333; font-weight: 500; } |
|||
.person-item-meta { |
|||
display: flex; |
|||
gap: 12px; |
|||
margin-top: 4px; |
|||
font-size: 12px; |
|||
color: #999; |
|||
.person-item-login { color: #b8c1cc; } |
|||
} |
|||
} |
|||
} |
|||
</style> |
|||
@ -1,11 +1,234 @@ |
|||
<script setup> |
|||
<template> |
|||
<div class="notice-page app_content"> |
|||
<!-- 顶部导航 --> |
|||
<van-nav-bar |
|||
title="预告" |
|||
left-arrow |
|||
@click-left="onBack" |
|||
/> |
|||
|
|||
<!-- Tab 切换 --> |
|||
<van-tabs |
|||
v-model:active="activeTab" |
|||
title-active-color="#111" |
|||
title-inactive-color="#666" |
|||
color="#2f8bd3" |
|||
line-width="36px" |
|||
line-height="2px" |
|||
:border="false" |
|||
@change="onTabChange" |
|||
> |
|||
<van-tab title="草稿" name="draft"> |
|||
<notice-tab-content |
|||
ref="draftRef" |
|||
:status="0" |
|||
:can-publish="true" |
|||
:can-edit="true" |
|||
:can-delete="true" |
|||
:can-message="false" |
|||
:can-reinitiate="false" |
|||
@publish="onPublish" |
|||
@edit="onEdit" |
|||
@delete="onDelete" |
|||
/> |
|||
</van-tab> |
|||
<van-tab title="待执法预告" name="pending"> |
|||
<notice-tab-content |
|||
ref="pendingRef" |
|||
:status="1" |
|||
:can-edit="false" |
|||
:can-delete="false" |
|||
:can-message="true" |
|||
:can-reinitiate="false" |
|||
@message="onMessage" |
|||
/> |
|||
</van-tab> |
|||
<van-tab title="历史预告" name="history"> |
|||
<notice-tab-content |
|||
ref="historyRef" |
|||
:status="3" |
|||
:can-edit="false" |
|||
:can-delete="false" |
|||
:can-message="true" |
|||
:can-reinitiate="true" |
|||
:message-readonly="true" |
|||
@message="onMessage" |
|||
@reinitiate="onReinitiate" |
|||
/> |
|||
</van-tab> |
|||
</van-tabs> |
|||
|
|||
<!-- 悬浮新增按钮(仅草稿Tab显示) --> |
|||
<div v-if="activeTab === 'draft'" class="add-btn" @click="onAdd"> |
|||
<van-icon name="plus" size="22" color="#fff" /> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup name="cpNoticeList"> |
|||
import { useRouter } from 'vue-router'; |
|||
import { showConfirmDialog, showToast } from 'vant'; |
|||
import { deleteNotice, editNotice } from '@/api/enforcement/notice'; |
|||
import NoticeTabContent from './cp_notice_tab_content.vue'; |
|||
|
|||
const router = useRouter(); |
|||
const activeTab = ref('draft'); |
|||
|
|||
const draftRef = ref(null); |
|||
const pendingRef = ref(null); |
|||
const historyRef = ref(null); |
|||
|
|||
const onBack = () => { |
|||
router.back(); |
|||
}; |
|||
|
|||
const onTabChange = () => { |
|||
// Tab切换后自动刷新(子组件内部onMounted会自行加载) |
|||
}; |
|||
|
|||
const onAdd = () => { |
|||
router.push({ path: '/enforcement/notice/edit' }); |
|||
}; |
|||
|
|||
const onEdit = (item) => { |
|||
router.push({ path: '/enforcement/notice/edit', query: { id: item.id } }); |
|||
}; |
|||
|
|||
const onDelete = (item) => { |
|||
showConfirmDialog({ |
|||
title: '提示', |
|||
message: '确定要删除该预告吗?', |
|||
}).then(() => { |
|||
deleteNotice(item.id, (res) => { |
|||
if (res.code === 200) { |
|||
showToast('删除成功'); |
|||
draftRef.value && draftRef.value.refresh(); |
|||
} else { |
|||
showToast(res.msg || '删除失败'); |
|||
} |
|||
}); |
|||
}).catch(() => {}); |
|||
}; |
|||
|
|||
const onMessage = (item) => { |
|||
const readonly = activeTab.value === 'history' ? '1' : '0'; |
|||
router.push({ path: '/enforcement/notice/message', query: { id: item.id, readonly } }); |
|||
}; |
|||
|
|||
const onReinitiate = (item) => { |
|||
// 再次发起:进入新增页面并带入已有信息 |
|||
router.push({ |
|||
path: '/enforcement/notice/edit', |
|||
query: { reinitiate: item.id }, |
|||
}); |
|||
}; |
|||
|
|||
const onPublish = (item) => { |
|||
// 发布前验证必填字段 |
|||
if (!item.enforceDate) { showToast('该预告缺少执法日期,请编辑后再发布'); return; } |
|||
if (!item.enforceTime) { showToast('该预告缺少执法时段,请编辑后再发布'); return; } |
|||
if (!item.enterpriseName) { showToast('该预告缺少企业名称,请编辑后再发布'); return; } |
|||
if (!item.enforceUserNames) { showToast('该预告缺少执法人员,请编辑后再发布'); return; } |
|||
if (!item.enforceContent) { showToast('该预告缺少执法事项,请编辑后再发布'); return; } |
|||
if (!item.enforceRequirement) { showToast('该预告缺少执法要求,请编辑后再发布'); return; } |
|||
|
|||
showConfirmDialog({ |
|||
title: '提示', |
|||
message: '预告发布后,不能删除,不能修改。确认发布?', |
|||
confirmButtonText: '确认发布', |
|||
cancelButtonText: '取消', |
|||
}).then(() => { |
|||
const submitData = { ...item, status: 1 }; |
|||
editNotice(submitData, (res) => { |
|||
if (res.code === 200) { |
|||
showToast('发布成功'); |
|||
draftRef.value && draftRef.value.refresh(); |
|||
} else { |
|||
showToast(res.msg || '发布失败'); |
|||
} |
|||
}, (errRes) => { |
|||
showToast(errRes?.msg || '发布失败,请重试'); |
|||
}); |
|||
}).catch(() => {}); |
|||
}; |
|||
</script> |
|||
|
|||
<template> |
|||
<style lang="less" scoped> |
|||
.notice-page { |
|||
display: flex; |
|||
flex-direction: column; |
|||
height: 100%; |
|||
background: #eaf4ff; |
|||
position: relative; |
|||
overflow: hidden; |
|||
} |
|||
|
|||
</template> |
|||
:deep(.van-nav-bar) { |
|||
background: #347bc3; |
|||
flex-shrink: 0; |
|||
|
|||
.van-nav-bar__title { |
|||
color: #fff; |
|||
font-size: 18px; |
|||
font-weight: 600; |
|||
} |
|||
|
|||
.van-icon { |
|||
color: #fff; |
|||
} |
|||
} |
|||
|
|||
:deep(.van-tabs) { |
|||
display: flex; |
|||
flex-direction: column; |
|||
flex: 1; |
|||
min-height: 0; |
|||
} |
|||
|
|||
:deep(.van-tabs__wrap) { |
|||
height: 54px; |
|||
background: #fff; |
|||
border-bottom: 1px solid #e4edf7; |
|||
flex-shrink: 0; |
|||
} |
|||
|
|||
:deep(.van-tab) { |
|||
font-size: 16px; |
|||
} |
|||
|
|||
:deep(.van-tab--active) { |
|||
font-weight: 600; |
|||
} |
|||
|
|||
:deep(.van-tabs__content), |
|||
:deep(.van-tabs__track), |
|||
:deep(.van-tab__panel-wrapper), |
|||
:deep(.van-tab__panel) { |
|||
flex: 1; |
|||
min-height: 0; |
|||
height: 100%; |
|||
} |
|||
|
|||
:deep(.van-tabs__content) { |
|||
overflow: hidden; |
|||
} |
|||
|
|||
<style scoped lang="less"> |
|||
.add-btn { |
|||
position: fixed; |
|||
right: 22px; |
|||
bottom: 28px; |
|||
width: 56px; |
|||
height: 56px; |
|||
border-radius: 50%; |
|||
background: linear-gradient(135deg, #1890ff, #0b63d1); |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
box-shadow: 0 8px 20px rgba(24, 144, 255, 0.38); |
|||
z-index: 100; |
|||
|
|||
&:active { |
|||
transform: scale(0.96); |
|||
} |
|||
} |
|||
</style> |
|||
|
|||
@ -0,0 +1,277 @@ |
|||
<template> |
|||
<div class="notice-message-page app_content"> |
|||
<!-- 顶部导航 --> |
|||
<van-nav-bar |
|||
title="预告留言" |
|||
left-arrow |
|||
@click-left="onBack" |
|||
/> |
|||
|
|||
<!-- 消息列表区域 --> |
|||
<div class="message-area" ref="messageAreaRef"> |
|||
<div v-if="loading" class="loading-wrap"> |
|||
<van-loading size="18" /> 加载中... |
|||
</div> |
|||
<template v-else> |
|||
<van-empty v-if="!messageList.length" description="暂无留言" image-size="100" /> |
|||
<div |
|||
v-for="(msg, idx) in messageList" |
|||
:key="msg.id || idx" |
|||
class="message-item" |
|||
:class="{ 'message-right': msg.msgFrom === 0, 'message-left': msg.msgFrom === 1 }" |
|||
> |
|||
<!-- 左侧(企业消息) --> |
|||
<div v-if="msg.msgFrom === 1" class="msg-avatar left-avatar">企</div> |
|||
<div class="msg-bubble" :class="{ 'bubble-left': msg.msgFrom === 1, 'bubble-right': msg.msgFrom === 0 }"> |
|||
<div class="msg-sender">{{ msg.userName }}</div> |
|||
<div class="msg-content">{{ msg.message }}</div> |
|||
<div class="msg-time">{{ formatTime(msg.createTime) }}</div> |
|||
</div> |
|||
<!-- 右侧(执法单位消息) --> |
|||
<div v-if="msg.msgFrom === 0" class="msg-avatar right-avatar">执</div> |
|||
</div> |
|||
</template> |
|||
</div> |
|||
|
|||
<!-- 底部输入区域(非只读时显示) --> |
|||
<div v-if="!isReadonly" class="input-area"> |
|||
<van-field |
|||
v-model="inputMessage" |
|||
type="textarea" |
|||
placeholder="输入留言内容..." |
|||
rows="1" |
|||
autosize |
|||
class="input-field" |
|||
/> |
|||
<van-button |
|||
type="primary" |
|||
size="small" |
|||
class="send-btn" |
|||
:disabled="!inputMessage.trim()" |
|||
:loading="sending" |
|||
@click="onSend" |
|||
> |
|||
发送 |
|||
</van-button> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup name="cpNoticeMessage"> |
|||
import { useRouter, useRoute } from 'vue-router'; |
|||
import { showToast } from 'vant'; |
|||
import { getNoticeMessages, addNoticeMessage } from '@/api/enforcement/notice'; |
|||
|
|||
const router = useRouter(); |
|||
const route = useRoute(); |
|||
const { useCommon } = $globalStore; |
|||
|
|||
const noticeId = computed(() => route.query.id || ''); |
|||
const isReadonly = computed(() => route.query.readonly === '1'); |
|||
|
|||
const messageList = ref([]); |
|||
const loading = ref(false); |
|||
const inputMessage = ref(''); |
|||
const sending = ref(false); |
|||
const messageAreaRef = ref(null); |
|||
|
|||
onMounted(() => { |
|||
loadMessages(); |
|||
}); |
|||
|
|||
const loadMessages = () => { |
|||
if (!noticeId.value) return; |
|||
loading.value = true; |
|||
getNoticeMessages(noticeId.value, (res) => { |
|||
loading.value = false; |
|||
if (res.code === 200 && Array.isArray(res.data)) { |
|||
messageList.value = res.data; |
|||
scrollToBottom(); |
|||
} |
|||
}, () => { |
|||
loading.value = false; |
|||
}); |
|||
}; |
|||
|
|||
const onSend = () => { |
|||
const text = inputMessage.value.trim(); |
|||
if (!text) return; |
|||
sending.value = true; |
|||
addNoticeMessage({ |
|||
noticeId: noticeId.value, |
|||
userId: String(useCommon.userId || ''), |
|||
userName: useCommon.nickName || useCommon.userName || '', |
|||
msgFrom: 0, // 执法单位=0 |
|||
message: text, |
|||
}, (res) => { |
|||
sending.value = false; |
|||
if (res.code === 200) { |
|||
inputMessage.value = ''; |
|||
loadMessages(); |
|||
} else { |
|||
showToast(res.msg || '发送失败'); |
|||
} |
|||
}, () => { |
|||
sending.value = false; |
|||
showToast('发送失败,请重试'); |
|||
}); |
|||
}; |
|||
|
|||
const scrollToBottom = () => { |
|||
nextTick(() => { |
|||
if (messageAreaRef.value) { |
|||
messageAreaRef.value.scrollTop = messageAreaRef.value.scrollHeight; |
|||
} |
|||
}); |
|||
}; |
|||
|
|||
const formatTime = (dateStr) => { |
|||
if (!dateStr) return ''; |
|||
return String(dateStr).substring(0, 16).replace('T', ' '); |
|||
}; |
|||
|
|||
const onBack = () => { |
|||
router.back(); |
|||
}; |
|||
</script> |
|||
|
|||
<style lang="less" scoped> |
|||
.notice-message-page { |
|||
display: flex; |
|||
flex-direction: column; |
|||
height: 100%; |
|||
background: #f0f6ff; |
|||
overflow: hidden; |
|||
} |
|||
|
|||
:deep(.van-nav-bar) { |
|||
background: #347bc3; |
|||
flex-shrink: 0; |
|||
.van-nav-bar__title { color: #fff; font-size: 18px; font-weight: 600; } |
|||
.van-icon { color: #fff; } |
|||
} |
|||
|
|||
.message-area { |
|||
flex: 1; |
|||
overflow-y: auto; |
|||
padding: 16px 12px; |
|||
} |
|||
|
|||
.loading-wrap { |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
padding: 30px 0; |
|||
color: #999; |
|||
font-size: 14px; |
|||
} |
|||
|
|||
.message-item { |
|||
display: flex; |
|||
align-items: flex-start; |
|||
margin-bottom: 16px; |
|||
} |
|||
|
|||
.message-left { |
|||
flex-direction: row; |
|||
} |
|||
|
|||
.message-right { |
|||
flex-direction: row-reverse; |
|||
} |
|||
|
|||
.msg-avatar { |
|||
width: 36px; |
|||
height: 36px; |
|||
border-radius: 50%; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
font-size: 14px; |
|||
font-weight: 600; |
|||
color: #fff; |
|||
flex-shrink: 0; |
|||
} |
|||
|
|||
.left-avatar { |
|||
background: linear-gradient(135deg, #f5a623, #f7c948); |
|||
margin-right: 10px; |
|||
} |
|||
|
|||
.right-avatar { |
|||
background: linear-gradient(135deg, #1890ff, #0b63d1); |
|||
margin-left: 10px; |
|||
} |
|||
|
|||
.msg-bubble { |
|||
max-width: 70%; |
|||
padding: 10px 14px; |
|||
border-radius: 12px; |
|||
position: relative; |
|||
} |
|||
|
|||
.bubble-left { |
|||
background: #fff; |
|||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); |
|||
border-top-left-radius: 4px; |
|||
} |
|||
|
|||
.bubble-right { |
|||
background: linear-gradient(135deg, #1890ff, #0b63d1); |
|||
box-shadow: 0 2px 8px rgba(24, 144, 255, 0.2); |
|||
border-top-right-radius: 4px; |
|||
.msg-sender { color: rgba(255, 255, 255, 0.8); } |
|||
.msg-content { color: #fff; } |
|||
.msg-time { color: rgba(255, 255, 255, 0.6); } |
|||
} |
|||
|
|||
.msg-sender { |
|||
font-size: 12px; |
|||
color: #999; |
|||
margin-bottom: 4px; |
|||
} |
|||
|
|||
.msg-content { |
|||
font-size: 15px; |
|||
color: #333; |
|||
line-height: 1.5; |
|||
word-break: break-all; |
|||
} |
|||
|
|||
.msg-time { |
|||
font-size: 11px; |
|||
color: #bbb; |
|||
margin-top: 4px; |
|||
text-align: right; |
|||
} |
|||
|
|||
.input-area { |
|||
display: flex; |
|||
align-items: flex-end; |
|||
gap: 10px; |
|||
padding: 10px 12px; |
|||
background: #fff; |
|||
border-top: 1px solid #e4edf7; |
|||
flex-shrink: 0; |
|||
|
|||
.input-field { |
|||
flex: 1; |
|||
background: #f5f7fa; |
|||
border-radius: 20px; |
|||
padding: 8px 14px; |
|||
|
|||
:deep(.van-field__control) { |
|||
min-height: 24px; |
|||
} |
|||
} |
|||
|
|||
.send-btn { |
|||
height: 36px; |
|||
border-radius: 18px; |
|||
padding: 0 18px; |
|||
font-size: 14px; |
|||
background: linear-gradient(135deg, #1890ff, #0b63d1); |
|||
border: none; |
|||
} |
|||
} |
|||
</style> |
|||
@ -0,0 +1,423 @@ |
|||
<template> |
|||
<div class="notice-tab-content"> |
|||
<van-pull-refresh v-model="refreshing" @refresh="onRefresh"> |
|||
<van-list |
|||
v-model:loading="loading" |
|||
:finished="finished" |
|||
finished-text="没有更多了" |
|||
@load="onLoad" |
|||
> |
|||
<div class="notice-list"> |
|||
<section |
|||
v-for="item in noticeList" |
|||
:key="item.id" |
|||
class="notice-card" |
|||
> |
|||
<div class="card-header"> |
|||
<div class="status-tag" :class="'status-' + item.status"> |
|||
{{ getStatusText(item.status) }} |
|||
</div> |
|||
<div class="card-date">{{ formatDate(item.createTime) }}</div> |
|||
</div> |
|||
|
|||
<div class="notice-title">{{ item.enforceContent || '暂无执法事项' }}</div> |
|||
|
|||
<div class="info-row"> |
|||
<div class="label">执法日期:</div> |
|||
<div class="value">{{ item.enforceDate || '-' }} {{ getTimeText(item.enforceTime) }}</div> |
|||
</div> |
|||
|
|||
<div class="info-row"> |
|||
<div class="label">执法单位:</div> |
|||
<div class="value">{{ item.bureauDeptName || '-' }}</div> |
|||
</div> |
|||
|
|||
<div class="info-row"> |
|||
<div class="label">企业名称:</div> |
|||
<div class="value">{{ item.enterpriseName || '-' }}</div> |
|||
</div> |
|||
|
|||
<div class="info-row"> |
|||
<div class="label">执法人员:</div> |
|||
<div class="value">{{ item.enforceUserNames || '-' }}</div> |
|||
</div> |
|||
|
|||
<div class="info-row"> |
|||
<div class="label">联系人:</div> |
|||
<div class="value">{{ item.contactPerson || '-' }}</div> |
|||
</div> |
|||
|
|||
<div class="info-row"> |
|||
<div class="label">联系电话:</div> |
|||
<div class="value">{{ item.contactPhone || '-' }}</div> |
|||
</div> |
|||
|
|||
<!-- 操作按钮 --> |
|||
<div class="card-actions" v-if="canPublish || canEdit || canDelete || canMessage || canReinitiate || item.planId"> |
|||
<div v-if="item.planId" class="action-item blue-action" @click="onViewPlan(item.planId)"> |
|||
<van-icon name="orders-o" /> |
|||
<span>关联计划</span> |
|||
</div> |
|||
<div v-if="canPublish" class="action-item green-action" @click="$emit('publish', item)"> |
|||
<van-icon name="passed" /> |
|||
<span>发布</span> |
|||
</div> |
|||
<div v-if="canEdit" class="action-item blue-action" @click="$emit('edit', item)"> |
|||
<van-icon name="edit" /> |
|||
<span>编辑</span> |
|||
</div> |
|||
<div v-if="canDelete" class="action-item red-action" @click="$emit('delete', item)"> |
|||
<van-icon name="delete-o" /> |
|||
<span>删除</span> |
|||
</div> |
|||
<div v-if="canMessage" class="action-item blue-action" @click="$emit('message', item)"> |
|||
<van-icon name="chat-o" /> |
|||
<span>留言</span> |
|||
</div> |
|||
<div v-if="canReinitiate" class="action-item green-action" @click="$emit('reinitiate', item)"> |
|||
<van-icon name="replay" /> |
|||
<span>再次发起</span> |
|||
</div> |
|||
</div> |
|||
</section> |
|||
</div> |
|||
|
|||
<!-- 空状态 --> |
|||
<van-empty v-if="!loading && noticeList.length === 0" description="暂无预告" /> |
|||
</van-list> |
|||
</van-pull-refresh> |
|||
|
|||
<!-- 关联计划详情弹层 --> |
|||
<van-popup v-model:show="showPlanPopup" position="bottom" round :style="{ height: '65%' }"> |
|||
<div class="plan-detail-popup"> |
|||
<div class="plan-detail-header"> |
|||
<span class="plan-detail-title">关联计划详情</span> |
|||
<van-icon name="cross" @click="showPlanPopup = false" /> |
|||
</div> |
|||
<div class="plan-detail-body"> |
|||
<div v-if="planDetailLoading" class="plan-detail-loading"> |
|||
<van-loading size="18" /> 加载中... |
|||
</div> |
|||
<template v-else-if="planDetail"> |
|||
<div class="plan-info-row"> |
|||
<div class="plan-info-label">计划日期</div> |
|||
<div class="plan-info-value">{{ planDetail.planDate || '-' }} {{ getPlanTimeText(planDetail.planTime) }}</div> |
|||
</div> |
|||
<div class="plan-info-row"> |
|||
<div class="plan-info-label">计划类别</div> |
|||
<div class="plan-info-value">{{ planDetail.planType === 1 ? '日常计划' : '专项计划' }}</div> |
|||
</div> |
|||
<div class="plan-info-row"> |
|||
<div class="plan-info-label">企业名称</div> |
|||
<div class="plan-info-value">{{ planDetail.enterpriseName || '-' }}</div> |
|||
</div> |
|||
<div class="plan-info-row"> |
|||
<div class="plan-info-label">企业信用代码</div> |
|||
<div class="plan-info-value">{{ planDetail.enterpriseNumber || '-' }}</div> |
|||
</div> |
|||
<div class="plan-info-row"> |
|||
<div class="plan-info-label">企业联系人</div> |
|||
<div class="plan-info-value">{{ planDetail.contactPerson || '-' }}</div> |
|||
</div> |
|||
<div class="plan-info-row"> |
|||
<div class="plan-info-label">联系电话</div> |
|||
<div class="plan-info-value">{{ planDetail.contactPhone || '-' }}</div> |
|||
</div> |
|||
<div class="plan-info-row"> |
|||
<div class="plan-info-label">执法人员</div> |
|||
<div class="plan-info-value">{{ planDetail.enforceUserNames || '-' }}</div> |
|||
</div> |
|||
<div class="plan-info-row"> |
|||
<div class="plan-info-label">执法事项</div> |
|||
<div class="plan-info-value">{{ planDetail.enforceContent || '-' }}</div> |
|||
</div> |
|||
</template> |
|||
<van-empty v-else description="未查询到计划信息" image-size="80" /> |
|||
</div> |
|||
</div> |
|||
</van-popup> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup> |
|||
import { getMyNoticeList } from '@/api/enforcement/notice'; |
|||
import { getPlanDetail } from '@/api/enforcement/plan'; |
|||
|
|||
const props = defineProps({ |
|||
status: { type: Number, required: true }, |
|||
canPublish: { type: Boolean, default: false }, |
|||
canEdit: { type: Boolean, default: false }, |
|||
canDelete: { type: Boolean, default: false }, |
|||
canMessage: { type: Boolean, default: false }, |
|||
canReinitiate: { type: Boolean, default: false }, |
|||
messageReadonly: { type: Boolean, default: false }, |
|||
}); |
|||
|
|||
const emit = defineEmits(['publish', 'edit', 'delete', 'message', 'reinitiate']); |
|||
|
|||
const noticeList = ref([]); |
|||
const loading = ref(false); |
|||
const finished = ref(false); |
|||
const refreshing = ref(false); |
|||
const pageNum = ref(1); |
|||
const pageSize = 10; |
|||
|
|||
const getStatusText = (status) => { |
|||
const map = { 0: '草稿', 1: '已发布', 2: '已取消', 3: '已执行' }; |
|||
return map[status] || '未知'; |
|||
}; |
|||
|
|||
const getTimeText = (time) => { |
|||
const map = { 1: '上午', 2: '下午', 3: '其他' }; |
|||
return map[time] || ''; |
|||
}; |
|||
|
|||
const formatDate = (dateStr) => { |
|||
if (!dateStr) return ''; |
|||
return String(dateStr).substring(0, 16).replace('T', ' '); |
|||
}; |
|||
|
|||
const onLoad = () => { |
|||
fetchList(); |
|||
}; |
|||
|
|||
const onRefresh = () => { |
|||
pageNum.value = 1; |
|||
finished.value = false; |
|||
noticeList.value = []; |
|||
fetchList(); |
|||
}; |
|||
|
|||
const refresh = () => { |
|||
onRefresh(); |
|||
}; |
|||
|
|||
const fetchList = () => { |
|||
loading.value = true; |
|||
const params = { |
|||
status: props.status, |
|||
pageNum: pageNum.value, |
|||
pageSize, |
|||
}; |
|||
getMyNoticeList( |
|||
params, |
|||
(res) => { |
|||
refreshing.value = false; |
|||
loading.value = false; |
|||
if (res.code === 200) { |
|||
const rows = res.rows || []; |
|||
if (pageNum.value === 1) { |
|||
noticeList.value = rows; |
|||
} else { |
|||
noticeList.value.push(...rows); |
|||
} |
|||
if (rows.length < pageSize) { |
|||
finished.value = true; |
|||
} else { |
|||
pageNum.value++; |
|||
} |
|||
} else { |
|||
finished.value = true; |
|||
} |
|||
}, |
|||
() => { |
|||
refreshing.value = false; |
|||
loading.value = false; |
|||
finished.value = true; |
|||
} |
|||
); |
|||
}; |
|||
|
|||
// 关联计划弹层 |
|||
const showPlanPopup = ref(false); |
|||
const planDetail = ref(null); |
|||
const planDetailLoading = ref(false); |
|||
|
|||
const getPlanTimeText = (time) => { |
|||
const map = { 1: '上午', 2: '下午', 3: '其他' }; |
|||
return map[time] || ''; |
|||
}; |
|||
|
|||
const onViewPlan = (planId) => { |
|||
showPlanPopup.value = true; |
|||
planDetail.value = null; |
|||
planDetailLoading.value = true; |
|||
getPlanDetail(planId, (res) => { |
|||
planDetailLoading.value = false; |
|||
if (res.code === 200 && res.data) { |
|||
planDetail.value = res.data; |
|||
} |
|||
}, () => { |
|||
planDetailLoading.value = false; |
|||
}); |
|||
}; |
|||
|
|||
// 暴露 refresh 方法给父组件 |
|||
defineExpose({ refresh }); |
|||
</script> |
|||
|
|||
<style lang="less" scoped> |
|||
.notice-tab-content { |
|||
height: 100%; |
|||
overflow-y: auto; |
|||
padding: 12px; |
|||
box-sizing: border-box; |
|||
} |
|||
|
|||
.notice-list { |
|||
display: flex; |
|||
flex-direction: column; |
|||
gap: 14px; |
|||
} |
|||
|
|||
.notice-card { |
|||
background: #fff; |
|||
border-radius: 14px; |
|||
padding: 16px; |
|||
box-shadow: 0 4px 14px rgba(44, 116, 190, 0.08); |
|||
} |
|||
|
|||
.card-header { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
margin-bottom: 12px; |
|||
} |
|||
|
|||
.status-tag { |
|||
font-size: 12px; |
|||
font-weight: 500; |
|||
padding: 3px 10px; |
|||
border-radius: 10px; |
|||
} |
|||
|
|||
.status-0 { |
|||
background: #f0f0f0; |
|||
color: #666; |
|||
} |
|||
|
|||
.status-1 { |
|||
background: #e6f7ff; |
|||
color: #1890ff; |
|||
} |
|||
|
|||
.status-2 { |
|||
background: #fff2e8; |
|||
color: #fa8c16; |
|||
} |
|||
|
|||
.status-3 { |
|||
background: #f6ffed; |
|||
color: #52c41a; |
|||
} |
|||
|
|||
.card-date { |
|||
font-size: 12px; |
|||
color: #999; |
|||
} |
|||
|
|||
.notice-title { |
|||
font-size: 16px; |
|||
font-weight: 700; |
|||
color: #111; |
|||
line-height: 1.5; |
|||
margin-bottom: 12px; |
|||
white-space: nowrap; |
|||
overflow: hidden; |
|||
text-overflow: ellipsis; |
|||
} |
|||
|
|||
.info-row { |
|||
display: flex; |
|||
align-items: flex-start; |
|||
margin-bottom: 6px; |
|||
font-size: 14px; |
|||
line-height: 1.5; |
|||
} |
|||
|
|||
.label { |
|||
width: 72px; |
|||
flex-shrink: 0; |
|||
color: #333; |
|||
font-weight: 500; |
|||
} |
|||
|
|||
.value { |
|||
flex: 1; |
|||
color: #8a8a8a; |
|||
} |
|||
|
|||
.card-actions { |
|||
margin-top: 14px; |
|||
padding-top: 12px; |
|||
border-top: 1px solid rgba(0, 0, 0, 0.06); |
|||
display: flex; |
|||
justify-content: flex-end; |
|||
gap: 20px; |
|||
} |
|||
|
|||
.action-item { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 4px; |
|||
font-size: 14px; |
|||
cursor: pointer; |
|||
} |
|||
|
|||
.blue-action { |
|||
color: #2382cf; |
|||
} |
|||
|
|||
.red-action { |
|||
color: #f56c6c; |
|||
} |
|||
|
|||
.green-action { |
|||
color: #52c41a; |
|||
} |
|||
|
|||
.plan-detail-popup { |
|||
display: flex; |
|||
flex-direction: column; |
|||
height: 100%; |
|||
.plan-detail-header { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
padding: 14px 16px 8px; |
|||
.plan-detail-title { font-size: 16px; font-weight: 600; color: #333; } |
|||
:deep(.van-icon) { font-size: 18px; color: #999; cursor: pointer; } |
|||
} |
|||
.plan-detail-body { |
|||
flex: 1; |
|||
overflow-y: auto; |
|||
padding: 8px 16px 16px; |
|||
} |
|||
.plan-detail-loading { |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
padding: 30px 0; |
|||
color: #999; |
|||
font-size: 14px; |
|||
} |
|||
.plan-info-row { |
|||
display: flex; |
|||
padding: 12px 0; |
|||
border-bottom: 1px solid #f0f0f0; |
|||
&:last-child { border-bottom: none; } |
|||
.plan-info-label { |
|||
width: 90px; |
|||
flex-shrink: 0; |
|||
font-size: 14px; |
|||
color: #666; |
|||
} |
|||
.plan-info-value { |
|||
flex: 1; |
|||
font-size: 14px; |
|||
color: #333; |
|||
word-break: break-all; |
|||
} |
|||
} |
|||
} |
|||
</style> |
|||
Loading…
Reference in new issue