Browse Source

局长信箱(内网端) 信件签收、办结审批

master
wxc 2 years ago
parent
commit
4416682160
  1. 1
      .env
  2. 4
      src/api/org/department.ts
  3. 10
      src/assets/icons/lock-fill.svg
  4. 14
      src/assets/icons/logout.svg
  5. 39
      src/components/LeaderSelect.vue
  6. 30
      src/components/MailCategorySelect.vue
  7. 17
      src/components/MailLevel.vue
  8. 2
      src/components/Message.vue
  9. 2
      src/components/PoliceSelect.vue
  10. 5
      src/components/icon/index.vue
  11. 24
      src/layout/components/Header.vue
  12. 13
      src/style/element.scss
  13. 27
      src/style/public.scss
  14. 9
      src/utils/request.ts
  15. 7
      src/utils/util.ts
  16. 19
      src/views/Login.vue
  17. 12
      src/views/home/components/MailTable.vue
  18. 9
      src/views/work/Fav.vue
  19. 22
      src/views/work/Todo.vue
  20. 96
      src/views/work/components/ApplicationCompleted.vue
  21. 180
      src/views/work/components/ConfirmedCompletion.vue
  22. 163
      src/views/work/components/InitiateCountersign.vue
  23. 391
      src/views/work/components/MailDialog.vue
  24. 100
      src/views/work/components/ReviewComments.vue
  25. 58
      src/views/work/components/templates/CoHandlingAndThreeFlow.vue
  26. 0
      src/views/work/components/templates/CoHandlingPoliceEdit.vue
  27. 103
      src/views/work/components/templates/Comments.vue
  28. 0
      src/views/work/components/templates/ContactWriterFrom.vue
  29. 0
      src/views/work/components/templates/DeptSelectForm.vue
  30. 51
      src/views/work/components/templates/InterviewWriterFrom.vue
  31. 165
      src/views/work/components/templates/MailApprovalDetail.vue
  32. 80
      src/views/work/components/templates/MailTypeForm.vue
  33. 98
      src/views/work/components/templates/MainContactInfo.vue
  34. 21
      src/views/work/components/templates/VerifyForm.vue
  35. 7
      vite.config.ts

1
.env

@ -0,0 +1 @@
VITE_API_URL=/lan-api

4
src/api/org/department.ts

@ -37,3 +37,7 @@ export function listThree() {
export function listByThree() {
return request.get({ url: '/system/dept/three/listByThree'})
}
export function listCountersign() {
return request.get({ url: '/system/dept/countersign/list'})
}

10
src/assets/icons/lock-fill.svg

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="登录页" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g transform="translate(-1194.000000, -493.000000)" fill="#C0C5E2" fill-rule="nonzero" id="mima">
<g transform="translate(1194.000000, 493.000000)">
<path d="M24.7519225,13.3671273 C24.1635627,13.3671273 23.6803339,12.9320399 23.5802727,12.3496015 C23.0978443,9.53707218 20.6507478,7.39056953 17.7142854,7.39056953 C14.777823,7.39056953 12.3307266,9.53734009 11.8482982,12.3496015 C11.7485039,12.931772 11.265275,13.3671273 10.6766484,13.3671273 C9.95487366,13.3671273 9.3739851,12.7230694 9.49085657,12.008015 C10.1392531,8.03864616 13.5800239,5 17.7142854,5 C21.848547,5 25.289051,8.03864616 25.9377143,12.008015 C26.0545858,12.7230694 25.4736972,13.3671273 24.7519225,13.3671273 L24.7519225,13.3671273 Z M26.7083189,14.4998549 L8.83472199,14.4998549 C7.82130225,14.4998549 7,15.3247527 7,16.3420107 L7,27.1578442 C7,28.1753701 7.82156908,29 8.83472199,29 L26.5885123,29 C27.6016652,29 28.4229674,28.175638 28.4235011,27.15838 L28.4285712,16.2276127 C28.4291045,15.2735817 27.6587668,14.4998549 26.7083189,14.4998549 Z M18.9048802,23.7224219 C18.9048802,24.3825545 18.372021,24.9178406 17.7142854,24.9178406 C17.0568167,24.9178406 16.5236907,24.3828224 16.5236907,23.7224219 L16.5236907,21.3318524 C16.5236907,20.6717198 17.0565499,20.1364337 17.7142854,20.1364337 C18.3717542,20.1364337 18.9048802,20.6717198 18.9048802,21.3318524 L18.9048802,23.7224219 Z" id="形状"></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

14
src/assets/icons/logout.svg

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg viewBox="0 0 17 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>ic_tuichu</title>
<g id="web端" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="1.0首页" transform="translate(-1824.000000, -41.000000)">
<rect fill="#F0F0F0" x="0" y="0" width="1920" height="1704"></rect>
<rect id="矩形" x="0" y="0" width="1924" height="100"></rect>
<rect id="矩形" fill="#283AAC" x="1811" y="32" width="92" height="36" rx="3"></rect>
<g id="tuichu" transform="translate(1824.000000, 41.000000)" fill="#FFFFFF" fill-rule="nonzero">
<path d="M-1.77635684e-15,9.00108551 L-1.77635684e-15,16.9622482 C-1.77635684e-15,17.5353998 0.514019899,18 1.15105311,18 L6.90192531,18 C7.53676186,18 8.05297842,17.5353998 8.05297842,16.9622482 C8.05297842,16.3890966 7.53895852,15.9244964 6.90192531,15.9244964 L2.30210622,15.9244964 L2.30210622,2.07550356 L6.90412198,2.07550356 C7.53895852,2.07550356 8.05517509,1.61090339 8.05517509,1.03775178 C8.05517509,0.464600169 7.54115519,0 6.90412198,0 L1.15105311,0 C0.516216565,0 -1.77635684e-15,0.464600169 -1.77635684e-15,1.03775178 L-1.77635684e-15,9.00108551 Z M16.6397467,8.24990954 C16.86161,8.44096008 17,8.71668074 17,9.0227958 C17,9.3310819 16.86161,9.60463153 16.6397467,9.79568206 L13.0064608,12.9089374 C12.8131542,13.0739356 12.5561442,13.1759739 12.2771676,13.1759739 C11.675281,13.1759739 11.1876211,12.7092027 11.1876211,12.1360511 C11.1876211,11.8299361 11.3282078,11.5542154 11.5500711,11.3653359 L13.0723608,10.0605476 L6.4603954,10.0605476 C5.85850885,10.0605476 5.37084895,9.59594741 5.37084895,9.0227958 C5.37084895,8.44964419 5.85850885,7.98504402 6.4603954,7.98504402 L13.0679674,7.98504402 L11.5456777,6.6802557 C11.3238144,6.49137619 11.1832278,6.2134845 11.1832278,5.90736944 C11.1832278,5.33421783 11.6730844,4.86961766 12.2727743,4.86961766 C12.5539475,4.86961766 12.8087608,4.96948498 13.0020675,5.1366542 L16.6397467,8.24990954 Z" id="形状"></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

39
src/components/LeaderSelect.vue

@ -0,0 +1,39 @@
<template>
<el-select v-model="selectVal" @change="handleSelect">
<el-option
v-for="item in leaders"
:key="item.id"
:label="item.name"
:value="item.empNo"
>{{ item.name + " - " + item.postTitle }}</el-option
>
</el-select>
</template>
<script setup>
import { getLeaderList } from "@/api/perms/admin";
const selectVal = ref('');
defineProps({
value: {
type: String,
default: ''
}
})
const emit = defineEmits(['update:value', 'change']);
function handleSelect(val) {
emit('update:value', val);
const option = leaders.value.filter(
(item) => item.empNo === val
)[0];
emit('change', val, option);
}
const leaders = ref([]);
getLeaderList().then((data) => {
leaders.value = data;
});
</script>
<style lang="scss" scoped>
</style>

30
src/components/MailCategorySelect.vue

@ -0,0 +1,30 @@
<template>
<el-tree-select
v-model="val"
:data="mailStore.mailCategorys"
filterable
@current-change="handleCategoryChange"
/>
</template>
<script setup>
const mailStore = useMailStore();
//
mailStore.getMailCategorys();
const val = ref('');
defineProps({
value: {
type: String,
default: ''
}
})
const emit = defineEmits(['update:value', 'change'])
const handleCategoryChange = (data, node) => {
emit('change', data, node)
}
</script>
<style lang="scss" scoped>
</style>

17
src/components/MailLevel.vue

@ -30,5 +30,20 @@ watch(() => props.value, (val) => {
})
</script>
<style lang="scss" scoped>
.dot {
position: relative;
padding-left: 20px;
&::before {
display: block;
content: '';
width: 10px;
height: 10px;
border-radius: 50%;
background-color: #6CFF5E;
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
}
}
</style>

2
src/components/Message.vue

@ -1,5 +1,5 @@
<template>
<div class="message-wrapper" v-if="show">
<div class="message-wrapper" v-if="show" @click="show = false">
<div class="message flex v-center gap">
<icon name="el-icon-SuccessFilled" :size="32" />
<span>{{ message }}</span>

2
src/components/PoliceSelect.vue

@ -6,7 +6,7 @@
placeholder="请输入姓名搜索"
:loading="loading"
remote-show-suffix
@change="handleChange">
@change="handleChange" clearable>
<el-option
v-for="item in options"
:key="item.id"

5
src/components/icon/index.vue

@ -46,3 +46,8 @@ export default defineComponent({
}
})
</script>
<style scoped>
.local-icon {
display: flex;
}
</style>

24
src/layout/components/Header.vue

@ -9,11 +9,14 @@
</div>
<div class="message"></div>
</div>
<div class="flex v-center gap">
<section>
<span>市局专班</span>
<div class="flex v-center gap-16">
<section class="flex gap">
<icon name="el-icon-UserFilled" :size="29" color="#586EFF" />
<span>{{ userStore.userInfo.roleName }}</span>
</section>
<el-button type="primary" @click="logout">退出</el-button>
<el-button type="primary" @click="logout" style="">退出
<template #icon><icon name="local-icon-logout" color="#586EFF" /></template>
</el-button>
</div>
</header>
</template>
@ -33,15 +36,28 @@ header {
color: #fff;
height: var(--header-height);
padding: 0 8px;
font-size: 20px;
.logo {
font-size: 3.89vh;
font-weight: 700;
a {
text-decoration: none;
color: inherit;
letter-spacing: 5.25px;
img {
width: 4vw;
}
}
}
.el-button {
--el-font-size-base: 18px;
--el-button-bg-color: #283AAC;
--el-button-border-color: #586EFF;
height: 36px;
&:hover {
--el-button-hover-bg-color: var(--el-button-bg-color);
--el-button-hover-border-color: #fff;
}
}
}
</style>

13
src/style/element.scss

@ -8,6 +8,9 @@
.el-select {
min-width: 80px;
}
.el-radio {
color: #333;
}
.el-button--primary.is-link>span {
font-weight: bold;
@ -28,12 +31,20 @@
--el-color-info: #fff;
background-color: var(--primary-color);
margin-right: 0;
.el-dialog__headerbtn:hover .el-dialog__close {
color: #aaa;
}
}
.el-dialog.dialog-header-nopadding .el-dialog__header {
.el-dialog.dialog-header-nopadding {
& > .el-dialog__header {
padding: 0;
margin-right: 0;
}
& > .el-dialog__body {
padding-left: 0;
padding-right: 0;
}
}

27
src/style/public.scss

@ -83,6 +83,9 @@ body {
.text-center {
text-align: center;
}
.text-right {
text-align: right;
}
.text-nowrap {
white-space: nowrap;
@ -164,6 +167,9 @@ body {
margin-top: 40px;
}
.mb-4 {
margin-bottom: 4px;
}
.mb-8 {
margin-bottom: 8px;
}
@ -189,7 +195,6 @@ body {
--setp-font-color: #666;
--setp-border-color: rgba(195, 202, 245, 1);
--setp-font-size: 20px;
width: 33.33%;
height: 45px;
padding-left: 6px;
background-color: var(--setp-background-color);
@ -207,8 +212,8 @@ body {
position: absolute;
right: -17px;
top: 50%;
width: 32px;
height: 32px;
width: 31px;
height: 31px;
background-color: var(--setp-background-color);
border-top: 1px solid var(--setp-border-color);
border-right: 1px solid var(--setp-border-color);
@ -219,7 +224,7 @@ body {
display: block;
content: "";
position: absolute;
left: 24%;
left: 26%;
top: 50%;
width: 16px;
height: 16px;
@ -229,3 +234,17 @@ body {
transform: translateY(-50%);
}
}
.col {
display: flex;
width: 280px;
&.short {
width: 140px;
}
label {
margin-right: 18px;
}
span {
color: #333;
}
}

9
src/utils/request.ts

@ -1,8 +1,13 @@
const basePath = '/lan-api/api'
import { getToken } from './auth'
import feedback from './feedback'
const {
VITE_API_URL
} = process.env
const BASE_PATH = VITE_API_URL + '/api'
type Options = {
url: string,
method?: 'GET' | 'POST' | 'PUT' | 'DELETE',
@ -59,7 +64,7 @@ function ajax(url: string, options:Options) {
}
}
return new Promise((resolve, reject) => {
fetch(`${basePath}${url}`, {
fetch(`${BASE_PATH}${url}`, {
method: options.method,
body: body,
headers: { ...headers, ...options.headers }

7
src/utils/util.ts

@ -201,3 +201,10 @@ export const getNonDuplicateID = (length = 8) => {
export const firstToUpperCase = (str = '') => {
return str.toLowerCase().replace(/( |^)[a-z]/g, ($1) => $1.toUpperCase())
}
export const getDictLable = (dictArr: any[], value: string) => {
if (!value) {
return ''
}
return dictArr.filter(item => item.value === value)[0].name
}

19
src/views/Login.vue

@ -4,10 +4,10 @@
<img src="/imgs/login_logo.png" alt="">
</header>
<div class="box flex v-center">
<div class="left">
<div class="left text-center">
<img src="/imgs/pic.png" alt="" />
</div>
<div class="right">
<div class="right mb-40">
<el-form
ref="formRef"
:model="formData"
@ -23,6 +23,9 @@
@keyup.enter="handleEnter"
style="--el-input-height: 50px"
>
<template #prefix>
<icon name="el-icon-UserFilled" :size="33" color="#c0c5e2" />
</template>
</el-input>
</el-form-item>
<el-form-item prop="password">
@ -34,15 +37,18 @@
@keyup.enter="handleLogin"
style="--el-input-height: 50px"
>
<template #prefix>
<icon name="local-icon-lock-fill" :size="36" />
</template>
</el-input>
</el-form-item>
<el-form-item>
<!-- <el-form-item>
<el-input
placeholder="请输入验证码"
style="--el-input-height: 50px"
>
</el-input>
</el-form-item>
</el-form-item> -->
<div class="mt-30 mb-10">
<el-button
type="primary"
@ -51,7 +57,7 @@
@click="lockLogin"
style="
width: 100%;
--el-button-size: 60px;
--el-button-size: 54px;
--el-font-size-base: 18px;
"
>
@ -63,7 +69,7 @@
size="large"
style="
width: 100%;
--el-button-size: 60px;
--el-button-size: 54px;
--el-font-size-base: 18px;
"
>
@ -146,7 +152,6 @@ function lockLogin() {
background-image: url("/imgs/bg.png");
background-position: center;
background-size: cover;
font-size: 15px;
header {
padding-top: 3.2vh;
margin-bottom: 8vh;

12
src/views/home/components/MailTable.vue

@ -137,8 +137,12 @@
</el-table>
</el-tab-pane>
</el-tabs>
<MailDialog v-model:show="showModel" :mail-id="activeMailId" @update="getList" />
</template>
<script setup>
import MailDialog from "@/views/work/components/MailDialog.vue";
import { getTodos } from "@/api/work";
const activeName = ref("todo");
@ -157,6 +161,7 @@ watch(activeName, (val) => {
const todos = ref([]);
const superviseTodos = ref([]);
const highTodos = ref([]);
const showModel = ref(false);
todoList();
function todoList() {
getTodos({
@ -186,6 +191,13 @@ function highTodoList() {
highTodos.value = data.records;
});
}
const activeMailId = ref("");
function handleMail(mailId) {
showModel.value = true;
activeMailId.value = mailId;
}
</script>
<style lang="scss" scoped>
.el-tabs {

9
src/views/work/Fav.vue

@ -0,0 +1,9 @@
<template>
</template>
<script setup>
</script>
<style lang="scss" scoped>
</style>

22
src/views/work/Todo.vue

@ -3,7 +3,6 @@
<header>
<el-form
:inline="true"
:model="form"
class="demo-form-inline"
:label-width="120"
>
@ -141,12 +140,12 @@
prop="mailTime"
label="来信时间"
align="center"
width="180"
width="170"
/>
<el-table-column
label="信件来源"
align="center"
width="120"
width="94"
>
<template #default="{ row }">
<span>{{
@ -160,14 +159,14 @@
prop="contactName"
label="姓名"
align="center"
width="120"
width="70"
/>
<el-table-column
prop="contactPhone"
label="联系电话"
width="120"
/>
<el-table-column label="信件等级" width="120">
<el-table-column label="信件等级" width="100">
<template #default="{ row }">
<mail-level
:value="row.mailLevel"
@ -175,16 +174,19 @@
/>
</template>
</el-table-column>
<el-table-column prop="mailCategory" label="信件分类" />
<el-table-column prop="mailCategory" label="信件分类" width="160" />
<el-table-column
prop="content"
label="信件内容"
show-overflow-tooltip
/>
<el-table-column prop="mailState" label="信件状态" />
<el-table-column prop="threeDeptName" label="办理单位" />
<el-table-column label="信件状态" width="90">
<template #default="{ row }">
<span>{{ getDictLable(dictData.mail_state, row.mailState) }}</span>
</template>
</el-table-column>
<el-table-column prop="threeDeptName" label="办理单位" width="160" />
<el-table-column label="流程节点">
<template #default="{ row }">
<el-tag :type="getFlowTagType(row.flowKey)" v-if="row.flowBeforeName">{{ row.flowBeforeName }}</el-tag>
</template>
@ -221,7 +223,7 @@ import { getTodos } from "@/api/work";
import { getMailFlowDetail } from "@/api/mail";
import { useDictData } from "@/hooks/useDictOptions";
import useMailStore from "@/stores/modules/mail";
import { formatTimeText } from "@/utils/util";
import { formatTimeText, getDictLable } from "@/utils/util";
const mailStore = useMailStore();
mailStore.getMailCategorys();

96
src/views/work/components/ApplicationCompleted.vue

@ -1,9 +1,5 @@
<template>
<el-dialog
v-model="visible"
width="50vw"
title="申请办结"
>
<el-dialog v-model="visible" width="50vw" title="申请办结">
<div>
<h2>选择办结方式</h2>
<el-form
@ -28,47 +24,36 @@
</el-row>
</el-radio-group>
</el-form-item>
<p>线上呈批时由三级机构专班发起办结线上呈批流程经由</p>
<div style="margin-bottom: 60px">
<template v-if="form.completeMethod === 'offline'">
<p>
线下审批时需要将信件办结的情况以纸制形式呈报<span
style="color: red"
>三长审批</span
>办结材料步骤中将其扫描成电子档作为附件上传至系统经由
</p>
<h3>三级机构专班 二级机构专班 市局专班</h3>
<p style="margin-bottom: 40px">逐级上报即可</p>
<el-form-item label="呈报上级,选择对应的二级机构专班">
<span>{{ secondDeptName }}</span>
</el-form-item>
</template>
<template v-else>
<p>
线上呈批时由三级机构专班发起办结线上呈批流程经由
</p>
<h3>
所队领导 二级机构专班 二级机构分管领导
二级机构正职领导 二级机构专班 市局专班
</h3>
<p style="margin-bottom: 40px">逐步上报审批</p>
<div style="margin-bottom: 60px">
<el-form-item
label="呈报上级,选择对应的二级机构专班"
prop="secondDeptId"
v-if="form.completeMethod === 'online'"
>
<el-select
v-model="form.secondDeptId"
@change="handleSelect"
>
<el-option
v-for="item in depts"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item
label="请选择上级领导"
prop="leaderEmpNo"
v-else
>
<el-select
<el-form-item label="请选择上级领导" prop="leaderEmpNo">
<LeaderSelect
v-model="form.leaderEmpNo"
@change="handleSelect"
>
<el-option
v-for="item in leaders"
:key="item.id"
:label="item.name"
:value="item.empNo"
/>
</el-select>
</el-form-item>
</template>
</div>
</el-form>
</div>
@ -81,8 +66,6 @@
</el-dialog>
</template>
<script setup>
import { listSecond } from "@/api/org/department";
import { getLeaderList } from "@/api/perms/admin";
const visible = ref(false);
const props = defineProps({
@ -92,7 +75,11 @@ const props = defineProps({
},
data: {
type: Object,
default: {}
default: {},
},
secondDeptName: {
type: String,
default: "",
},
});
const emits = defineEmits(["update:show", "update:data", "submit"]);
@ -108,7 +95,9 @@ watch(
}
);
const form = ref({});
const form = ref({
completeMethod: "offline",
});
const formRef = ref(null);
const rules = {
completeMethod: [
@ -117,12 +106,6 @@ const rules = {
message: "请选择办结方式",
},
],
secondDeptId: [
{
required: true,
message: "请选择二级机构",
},
],
leaderEmpNo: [
{
required: true,
@ -130,23 +113,14 @@ const rules = {
},
],
};
const depts = ref([]);
listSecond().then((data) => {
depts.value = data;
});
const leaders = ref([]);
getLeaderList().then((data) => {
leaders.value = data;
});
function submit() {
formRef.value.validate((valid) => {
if (valid) {
const data = { ...props.data, ...form.value}
emits('update:data', data)
emits('submit', form.value.completeMethod)
visible.value = false
const data = { ...props.data, ...form.value };
emits("update:data", data);
emits("submit", form.value.completeMethod);
visible.value = false;
}
});
}

180
src/views/work/components/ConfirmedCompletion.vue

@ -0,0 +1,180 @@
<template>
<el-dialog v-model="visible" width="55vw" title="认定办结">
<h2>办理反馈情况</h2>
<div class="flex mb-20">
<div class="col">
<label>群众反应事项解决情况</label>
<span></span>
</div>
<div class="col">
<label>办理反馈情况</label>
<span></span>
</div>
</div>
<div class="flex mb-20">
<div class="col">
<label>回访人姓名</label>
<span></span>
</div>
<div class="col">
<label>回访人警号</label>
<span></span>
</div>
</div>
<div class="flex mb-40">
<div class="col">
<label>回访人电话</label>
<span></span>
</div>
</div>
<el-form
:model="form"
:rules="rules"
ref="formRef"
:label-width="244"
>
<h2>综合认定情况</h2>
<el-row>
<el-col :span="12">
<el-form-item
label="办理合格情况"
prop="qualifiedProcessingStatus"
>
<el-radio-group v-model="form.qualifiedProcessingStatus">
<el-radio v-for="item in dictData.qualified_processing_status" :key="item.id" :label="item.value" size="large" >{{ item.name }}</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item
label="问题解决情况"
prop="problemSolvingStatus"
>
<el-radio-group v-model="form.problemSolvingStatus">
<el-radio :label="true" size="large">已解决</el-radio>
<el-radio :label="false" size="large">未解决</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-form-item
label="群众回复情况"
prop="satisfactionStatus"
>
<el-radio-group v-model="form.satisfactionStatus">
<el-radio v-for="item in dictData.satisfaction_status" :key="item.id" :label="item.value" size="large" >{{ item.name }}</el-radio>
</el-radio-group>
</el-form-item>
<h2>认定办结意见</h2>
<el-form-item prop="completionComment" class="mb-40">
<el-input
v-model="form.completionComment"
type="textarea"
:rows="4"
placeholder="请输入认定办结意见"
></el-input>
</el-form-item>
</el-form>
<footer class="flex end">
<el-button type="primary" size="large" @click="submit"
>审批通过</el-button
>
</footer>
</el-dialog>
</template>
<script setup>
import { useDictData } from "@/hooks/useDictOptions";
const { dictData } = useDictData(["satisfaction_status", "qualified_processing_status"]);
const visible = ref(false);
const form = reactive({});
const rules = {
qualifiedProcessingStatus: [
{
required: true,
message: "请选择办理合格情况",
},
],
problemSolvingStatus: [
{
required: true,
message: "请选择问题解决情况",
},
],
satisfactionStatus: [
{
required: true,
message: "请选择群众回复情况",
},
],
completionComment: [
{
required: true,
message: "请填写认定办结意见",
},
],
};
const formRef = ref();
const props = defineProps({
show: {
type: Boolean,
default: false,
},
data: {
type: Object,
default: {},
},
flowKey: {
type: String,
default: "",
},
});
const selectLeaderVisible = ref(false);
watch(
() => props.flowKey,
(val) => {
if (val === "second_approval" || val === "second_deputy_approval") {
selectLeaderVisible.value = true;
}
}
);
const emits = defineEmits(["update:show", "update:data", "submit"]);
watch(visible, (val) => {
emits("update:show", val);
});
watch(
() => props.show,
(val) => {
visible.value = val;
if (val) {
}
}
);
function submit() {
formRef.value.validate((valid) => {
if (valid) {
//
const data = { ...props.data, ...form };
emits("update:data", data);
emits("submit", "confirmedCompletion");
visible.value = false;
}
});
}
</script>
<style lang="scss" scoped>
h2 {
margin-top: 0;
font-size: 20px;
color: #333;
}
.col {
label {
width: 244px;
text-align: right;
}
}
</style>

163
src/views/work/components/InitiateCountersign.vue

@ -0,0 +1,163 @@
<template>
<el-dialog v-model="visible" width="55vw" title="发起部门会签">
<el-form
:model="form"
:rules="rules"
ref="formRef"
label-position="top"
style="min-height: 50vh"
>
<h2>会签具体要求</h2>
<el-form-item prop="countersignRequirement" class="mb-40">
<el-input
v-model="form.countersignRequirement"
type="textarea"
:rows="4"
placeholder="请输入部门会签提议,如对信件处理结果存疑的地方,希望哪些部门提供专业意见等。"
></el-input>
</el-form-item>
<h2>参与会签的部门</h2>
<el-form-item prop="countersignDeptIds">
<div>
<div
v-for="(item, index) in countersignDeptIds"
:key="index"
class="flex gap mb-10"
>
<el-select
v-model="countersignDeptIds[index]"
style="width: 300px"
@change="handleChange"
>
<el-option
v-for="item in depts"
:key="item.id"
:label="item.name"
:value="item.id"
:disabled="
mail.secondDeptId === item.id ||
mail.threeDeptId === item.id ||
countersignDeptIds.indexOf(item.id) > -1
"
></el-option>
</el-select>
<el-button
type="primary"
plain
@click="countersignDeptIds.splice(index, 1)"
>删除</el-button
>
</div>
</div>
</el-form-item>
<p style="color: #999" class="mb-10">会签部门数量最多3个</p>
<el-button type="primary" plain @click="handleAddDept"
>添加部门</el-button
>
</el-form>
<footer class="flex end">
<el-button type="primary" size="large" @click="submit"
>发起会签</el-button
>
</footer>
</el-dialog>
</template>
<script setup>
import { listCountersign } from "@/api/org/department";
const visible = ref(false);
const form = reactive({});
const rules = {
countersignRequirement: [
{
required: true,
message: "请填写部门会签提议",
},
],
countersignDeptIds: [
{
required: true,
message: "请添加会签部门",
},
],
};
const formRef = ref();
const depts = ref([]);
listCountersign().then((data) => {
depts.value = data;
});
const countersignDeptIds = ref([]);
const props = defineProps({
show: {
type: Boolean,
default: false,
},
data: {
type: Object,
default: {},
},
mail: {
type: Object,
default: {},
},
});
const selectLeaderVisible = ref(false);
watch(
() => props.flowKey,
(val) => {
if (val === "second_approval" || val === "second_deputy_approval") {
selectLeaderVisible.value = true;
}
}
);
const emits = defineEmits(["update:show", "update:data", "submit"]);
watch(visible, (val) => {
emits("update:show", val);
});
watch(
() => props.show,
(val) => {
visible.value = val;
if (val) {
}
}
);
function handleChange() {
const arr = countersignDeptIds.value.filter(item => item);
if (arr.length) {
form.countersignDeptIds = arr
}
}
function handleAddDept() {
if (countersignDeptIds.value.length >= 3) {
return
}
countersignDeptIds.value.push('')
}
function submit() {
formRef.value.validate((valid) => {
if (valid) {
//
const data = { ...props.data, ...form };
emits("update:data", data);
emits("submit", "confirmedCompletion");
visible.value = false;
}
});
}
</script>
<style lang="scss" scoped>
h2 {
margin-top: 0;
font-size: 20px;
color: #333;
}
</style>

391
src/views/work/components/MailDialog.vue

@ -5,6 +5,7 @@
width="84vw"
top="4vh"
class="dialog-header-nopadding"
style="--el-dialog-padding-primary: 10px"
>
<template #header="{ close, titleId, titleClass }">
<header class="flex between v-center dialog-header">
@ -17,6 +18,8 @@
class="step flex center v-center"
v-for="(label, index) in steps"
:key="index"
:active="activeStep === index"
:completed="index < activeStep"
>
<span class="bloder">{{ index + 1 }}</span>
<span>{{ label }}</span>
@ -55,30 +58,66 @@
</template>
<main>
<el-row :gutter="20" style="height: 100%">
<el-col :span="5" style="padding: 0 20px">
<el-col :span="5" style="height: 100%">
<div
style="height: 100%; padding: 0 20px 0 40px"
ref="leftContainerRef"
>
<el-progress
type="dashboard"
:percentage="percentage"
:stroke-width="16"
:width="250"
:width="240"
color="#2B45E5"
>
<div class="mb-20" v-html="percentageLableHtml"></div>
<div
class="mb-20"
v-html="percentageLableHtml"
></div>
<p v-if="flowRemainingTime > 0">剩余处理时间</p>
</el-progress>
<div>
<div ref="mailInfoRef">
<div class="col mb-10" v-if="mail.mailCategory">
<label style="width: 56px">信件分类</label>
<span style="width: calc(100% - 74px)">
<span v-if="mail.mailFirstCategory">{{
mail.mailFirstCategory
}}</span>
<span v-if="mail.mailSecondCategory">{{
" > " + mail.mailSecondCategory
}}</span>
<span v-if="mail.mailThreeCategory">{{
" > " + mail.mailThreeCategory
}}</span>
</span>
</div>
<div
class="col"
v-if="mail.mailLevel"
style="padding-bottom: 20px"
>
<label>信件等级</label>
<span>{{ getMailLevel(dictData.mail_level, mail.mailLevel) }}</span>
</div>
</div>
<div class="flow">
<div class="flex between v-center flow-header">
<header
class="flex between v-center flow-header"
ref="flowHeaderRef"
>
<span class="second">总耗时</span>
<el-button type="primary" size="small" plain
<el-button
type="primary"
size="small"
plain
:disabled="true"
>节点流程图</el-button
>
</div>
</header>
<el-scrollbar :max-height="flowMaxHeight">
<div class="flow-container">
<div v-for="item in flows" :key="item.id">
<div class="mb-8 mt-8">
<div class="mb-4 mt-4 flow-info">
<span class="second mr-8">{{
item.createTime
}}</span>
@ -89,62 +128,39 @@
item.flowAfterName
}}</span>
</div>
<div v-if="item.flowKey !== 'first_sign'">
<span class="second mr-8">用时</span>
<div
v-if="item.flowKey !== 'first_sign'"
class="flow-time"
>
<span class="second mr-8"
>用时</span
>
<span class="primary">{{
formatTimeText(
item.consumingTime
)
}}</span>
</div>
</div>
<div
class="text-center mt-20"
v-if="!flows.length"
style="color: #999"
>
</div>
</div>
</el-col>
<el-col :span="19" style="height: 100%">
<el-scrollbar max-height="100%">
<div class="card">
<div class="flex">
<div class="col">
<label>来信时间</label>
<span>{{ mail.mailTime }}</span>
</div>
<div class="col">
<label>信件来源</label>
<span>{{ mail.source }}</span>
</div>
</div>
<h2>来信人信息</h2>
<div class="flex">
<div class="col short">
<label>姓名</label>
<span>{{ mail.contactName }}</span>
</div>
<div class="col short">
<label>性别</label>
<span>{{ mail.contactSex }}</span>
</div>
<div class="col">
<label>证件号码</label>
<span>{{ mail.contactIdCard }}</span>
</div>
<div class="col">
<label>联系电话</label>
<span>{{ mail.contactPhone }}</span>
</div>
</div>
<h2>信件内容</h2>
<div class="flex">
<div class="col">
<label>案件编号</label>
<span>{{ mail.caseNumber }}</span>
</div>
<div class="col">
<label>被投诉/涉及单位</label>
<span>{{ mail.involvedDeptName }}</span>
</div>
</el-scrollbar>
</div>
<p class="content">{{ mail.content }}</p>
<h2>附件</h2>
</div>
</el-col>
<el-col :span="19" style="height: 100%">
<el-scrollbar max-height="100%" class="main-container">
<template
v-if="webComponents.indexOf('MainContactInfo') > -1"
>
<MainContactInfo :mail="mail" />
</template>
<template
v-if="webComponents.indexOf('MailTypeForm') > -1"
>
@ -171,6 +187,7 @@
>
<CoHandlingAndThreeFlow
v-model:data="requestData"
:flowKey="flowNode.key"
/>
</template>
<template
@ -197,16 +214,23 @@
/>
</template>
<template
v-if="
webComponents.indexOf('VerifyForm') >
-1
"
v-if="webComponents.indexOf('VerifyForm') > -1"
>
<VerifyForm
v-model:data="requestData"
ref="verifyFormRef"
/>
</template>
<template
v-if="
webComponents.indexOf('MailApprovalDetail') > -1
"
>
<MailApprovalDetail :mail="mail" />
</template>
<template v-if="webComponents.indexOf('Comments') > -1">
<Comments :approvals="approvals" />
</template>
</el-scrollbar>
</el-col>
</el-row>
@ -229,29 +253,69 @@
<Message ref="messageRef" />
<ApplicationCompleted v-model:show="completeShow" v-model:data="requestData" @submit="handleAction" />
<ApplicationCompleted
v-model:show="completeShow"
v-model:data="requestData"
@submit="(key) => handleAction(key)"
/>
<ReviewComments
v-model:show="approvedShow"
v-model:data="requestData"
v-model:flowKey="flowNode.key"
@submit="(key) => handleAction(key)"
/>
<ConfirmedCompletion
v-model:show="completionShow"
v-model:data="requestData"
@submit="(key) => handleAction(key)"
/>
<InitiateCountersign
v-model:show="countersignShow"
v-model:data="requestData"
:mail="mail"
@submit="(key) => handleAction(key)"
/>
</template>
<script setup>
import MailTypeForm from "./MailTypeForm.vue";
import DeptSelectForm from "./DeptSelectForm.vue";
import CoHandlingAndThreeFlow from "./CoHandlingAndThreeFlow.vue";
import ContactWriterFrom from "./ContactWriterFrom.vue";
import InterviewWriterFrom from "./InterviewWriterFrom.vue";
import VerifyForm from "./VerifyForm.vue";
import MainContactInfo from "./templates/MainContactInfo.vue";
import MailTypeForm from "./templates/MailTypeForm.vue";
import DeptSelectForm from "./templates/DeptSelectForm.vue";
import CoHandlingAndThreeFlow from "./templates/CoHandlingAndThreeFlow.vue";
import ContactWriterFrom from "./templates/ContactWriterFrom.vue";
import InterviewWriterFrom from "./templates/InterviewWriterFrom.vue";
import VerifyForm from "./templates/VerifyForm.vue";
import MailApprovalDetail from "./templates/MailApprovalDetail.vue";
import Comments from "./templates/Comments.vue";
import ApplicationCompleted from "./ApplicationCompleted.vue";
import ReviewComments from "./ReviewComments.vue";
import ConfirmedCompletion from "./ConfirmedCompletion.vue";
import InitiateCountersign from "./InitiateCountersign.vue";
import { getMailFlowDetail, flowNext } from "@/api/mail";
import { addFav, delFav } from "@/api/work/fav";
import { useDictData } from "@/hooks/useDictOptions";
import feedback from "@/utils/feedback";
import { timeDiffSeconds } from "@/utils/util";
import { timeDiffSeconds, formatTimeText } from "@/utils/util";
const { dictData } = useDictData(["mail_level"]);
const mailTypeFormRef = ref();
const deptSelectFormRef = ref();
const contactWriterFromRef = ref();
const interviewWriterFromRef = ref();
const verifyFormRef = ref()
const verifyFormRef = ref();
const mail = ref({});
const flows = ref([]);
const flowNode = ref({});
const webComponents = ref([]);
const actions = ref([]);
const percentage = ref(100);
const percentageLableHtml = ref("");
//
const flowRemainingTime = ref(0);
const approvals = ref([]);
const isFav = ref(false);
const steps = ref([
@ -262,6 +326,29 @@ const steps = ref([
"办结审批",
"认定办结",
]);
const activeStep = ref(0);
watch(flowNode, (val) => {
if (val.key === "contact_writer") {
activeStep.value = 1;
}
if (val.key === "interview_writer") {
activeStep.value = 2;
}
if (val.key === "verify") {
activeStep.value = 3;
}
if (
val.key === "three_leader_approval" ||
val.key === "second_approval" ||
val.key === "second_deputy_approval" ||
val.key === "second_leader_approval" ||
val.key === "second_reporting" ||
val.key === "first_approval" ||
val.key === "countersign"
) {
activeStep.value = 4;
}
});
const props = defineProps({
show: {
@ -275,7 +362,7 @@ const props = defineProps({
disabled: {
type: Boolean,
default: false,
}
},
});
const emits = defineEmits(["update:show", "update"]);
@ -292,16 +379,7 @@ watch(
}
);
const mail = ref({});
const flows = ref([]);
const flowNode = ref({});
const webComponents = ref([]);
const actions = ref([]);
const percentage = ref(100);
const percentageLableHtml = ref("");
//
const flowRemainingTime = ref(0);
let timer;
function getDetail() {
//
getMailFlowDetail(props.mailId).then((data) => {
@ -309,6 +387,7 @@ function getDetail() {
flows.value = data.flows;
isFav.value = data.isFav;
flowNode.value = data.flowNode;
approvals.value = data.approvals;
if (data.flowNode.webComponents) {
webComponents.value = data.flowNode.webComponents
.split(",")
@ -318,13 +397,22 @@ function getDetail() {
percentage.value = data.mail.flowRemainingTimePercentage;
flowRemainingTime.value = data.mail.flowRemainingTime;
if (flowRemainingTime.value > 0 && flowRemainingTime.value < 3600) {
let timer = setInterval(() => {
clearInterval(timer);
timer = setInterval(() => {
flowRemainingTime.value -= 1;
if (flowRemainingTime.value <= 0) {
clearInterval(timer);
}
}, 1000);
}
nextTick(() => {
flowMaxHeight.value =
leftContainerRef.value.offsetHeight -
(mailInfoRef.value?.offsetHeight || 0) -
flowHeaderRef.value.offsetHeight -
240;
});
});
}
@ -360,19 +448,44 @@ watch(flowRemainingTime, (val) => {
)}</span><span></span>`;
return;
}
const remainder = val % 86400;
if (remainder === 0) {
percentageLableHtml.value = `<span class="large">${parseInt(
val / 86400
)}</span><span></span>`;
return;
}
percentageLableHtml.value = `<span class="large">${parseInt(
val / 86400
)}</span><span></span><span class="large">${parseInt(
(val % 86400) / 3600
remainder / 3600
)}</span><span>小时</span>`;
});
const requestBody = ref({});
const requestData = ref({});
const messageRef = ref();
const approvedShow = ref(false);
const completionShow = ref(false);
const countersignShow = ref(false);
async function handleAction(key) {
if (!key) {
return
return;
}
//
if (key === "approved") {
approvedShow.value = true;
return;
}
//
if (key === "approvedCompletion") {
completionShow.value = true;
return;
}
//
if (key === "initiateCountersign") {
countersignShow.value = true;
return;
}
if (mailTypeFormRef.value) {
await mailTypeFormRef.value.validate();
@ -383,13 +496,16 @@ async function handleAction(key) {
if (contactWriterFromRef.value) {
await contactWriterFromRef.value.validate();
}
if (interviewWriterFromRef.value) {
await interviewWriterFromRef.value.validate();
}
if (verifyFormRef.value) {
await verifyFormRef.value.validate();
}
//
if (key === 'applicationCompleted') {
if (key === "applicationCompleted") {
completeShow.value = true;
return
return;
}
requestBody.value = {
mailId: props.mailId,
@ -418,8 +534,6 @@ async function handleAction(key) {
});
}
const coHandlingPoliceShow = ref(false);
//
function handleFav() {
if (!isFav.value) {
@ -434,6 +548,11 @@ function handleFav() {
}
const completeShow = ref(false);
const flowMaxHeight = ref(200);
const leftContainerRef = ref();
const mailInfoRef = ref();
const flowHeaderRef = ref();
</script>
<style lang="scss" scoped>
@ -448,20 +567,31 @@ const completeShow = ref(false);
color: var(--dialog-header-font-color);
}
.step-box {
width: 52%;
margin-right: 4%;
.step {
--setp-background-color: #2234a7;
--setp-background-color: #3a4dc1;
--setp-border-color: #4b60e4;
--setp-font-color: var(--dialog-header-font-color);
--setp-font-size: var(--dialog-header-font-size);
width: 16.66%;
padding-left: 30px;
padding-right: 16px;
&::after {
display: none;
}
&:first-child {
padding-left: 16px;
}
&[active="true"] {
--setp-background-color: #ff4242;
--setp-border-color: #ff7474;
--setp-font-color: #fff;
}
.bloder {
font-size: 24px;
font-weight: 700;
margin-right: 8px;
}
span {
z-index: 2;
}
}
}
@ -483,11 +613,11 @@ const completeShow = ref(false);
color: #999;
:deep() {
.error {
font-size: 60px;
color: var(--danger-color);
font-size: 50px;
color: #ff4242;
}
.large {
font-size: 60px;
font-size: 56px;
color: var(--primary-color);
}
}
@ -506,8 +636,8 @@ const completeShow = ref(false);
.flow-container {
background-color: #eff0f5;
padding: 12px 8px;
min-height: 120px;
font-size: 12px;
}
.second {
color: #999;
}
@ -515,54 +645,51 @@ const completeShow = ref(false);
color: var(--primary-color);
font-weight: 500;
}
.flow-time {
display: inline-flex;
padding: 6px;
margin-left: 6px;
background-color: #f5f6ff;
border-left: 2px solid #00d050;
padding-left: 20px;
}
.flow-info {
position: relative;
padding-left: 20px;
&::before {
display: block;
content: "";
width: 10px;
height: 10px;
border-radius: 50%;
background-color: #bfbfbf;
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
}
}
&>div:first-child .flow-info::before {
width: 12px;
height: 12px;
background-color: var(--primary-color);
}
.card {
background: #f9faff;
border-top: 8px solid var(--primary-color);
box-shadow: inset 0px 8px 0px 0px rgba(22, 37, 130, 1);
box-shadow: 0px 2px 4px 0px rgba(129, 150, 255, 0.47);
padding: 16px;
margin-bottom: 24px;
.col {
display: flex;
width: 280px;
&.short {
width: 140px;
}
label {
margin-right: 18px;
}
span {
color: #333;
}
}
.content {
margin-top: 16px;
min-height: 80px;
padding: 12px;
background-color: #fff;
color: #333;
font-size: 16px;
}
}
h1 {
font-size: 24px;
font-weight: 500;
color: #162582;
}
h2 {
font-size: 20px;
font-weight: 500;
color: #162582;
}
main {
height: calc(92vh - 192px);
}
:deep() {
.el-dialog__header {
padding: 0;
.main-container {
padding: 0 20px;
}
footer {
padding: 0 20px;
}
</style>

100
src/views/work/components/ReviewComments.vue

@ -0,0 +1,100 @@
<template>
<el-dialog v-model="visible" width="50vw" title="提交审批">
<el-form
label-position="top"
:model="form"
:rules="rules"
ref="formRef"
style="height: 50vh"
>
<el-form-item label="请选择上级领导" prop="leaderEmpNo" v-if="selectLeaderVisible">
<LeaderSelect v-model="form.leaderEmpNo" />
</el-form-item>
<el-form-item label="审批意见" prop="approvalComment">
<el-input
v-model="form.approvalComment"
type="textarea"
:rows="3"
placeholder="请输入审批意见"
></el-input>
</el-form-item>
</el-form>
<footer class="flex end">
<el-button type="primary" size="large" @click="submit"
>审批通过</el-button
>
</footer>
</el-dialog>
</template>
<script setup>
const visible = ref(false);
const form = reactive({});
const rules = {
leaderEmpNo: [
{
required: true,
message: "请选择上级领导",
},
],
approvalComment: [
{
required: true,
message: "请填写审批意见",
},
],
};
const formRef = ref();
const props = defineProps({
show: {
type: Boolean,
default: false,
},
data: {
type: Object,
default: {},
},
flowKey: {
type: String,
default: "",
},
});
const selectLeaderVisible = ref(false)
watch(() => props.flowKey, (val) => {
if (val === 'second_approval' || val === 'second_deputy_approval') {
selectLeaderVisible.value = true
}
})
onMounted(() => {
if (props.data) {
Object.assign(form, props.data);
}
});
const emits = defineEmits(["update:show", "update:data", "submit"]);
watch(visible, (val) => {
emits("update:show", val);
});
watch(
() => props.show,
(val) => {
visible.value = val;
if (val) {
}
}
);
function submit() {
formRef.value.validate((valid) => {
if (valid) {
//
const data = { ...props.data, ...form };
emits("update:data", data);
emits("submit", "approvedSubmit");
visible.value = false;
}
});
}
</script>
<style lang="scss" scoped>
</style>

58
src/views/work/components/CoHandlingAndThreeFlow.vue → src/views/work/components/templates/CoHandlingAndThreeFlow.vue

@ -23,23 +23,51 @@
class="step flex center v-center"
v-for="(label, index) in threeSteps"
:key="index"
:active="activeStep === index"
:completed="index < activeStep"
>
<span class="mr-8">{{ index + 1 }}</span>
<span>{{ label }}</span>
</div>
</header>
<CoHandlingPoliceEdit v-model:show="coHandlingPoliceShow" v-model:data="contactWriteData.coHandingPolices" />
<CoHandlingPoliceEdit
v-model:show="coHandlingPoliceShow"
v-model:data="contactWriteData.coHandingPolices"
/>
</template>
<script setup>
import CoHandlingPoliceEdit from './CoHandlingPoliceEdit.vue'
import CoHandlingPoliceEdit from "./CoHandlingPoliceEdit.vue";
const threeSteps = ref(["联系群众", "群众接访", "核查办理"]);
const activeStep = ref(0);
const contactWriteData = ref({
coHandingPolices: []
})
coHandingPolices: [],
});
const coHandlingPoliceShow = ref(false);
const props = defineProps({
flowKey: {
type: String,
default: "",
},
});
watch(
() => props.flowKey,
() => {
getActiveStep()
}
);
getActiveStep()
function getActiveStep() {
if (props.flowKey === "interview_writer") {
activeStep.value = 1;
}
if (props.flowKey === "verify") {
activeStep.value = 2;
}
}
</script>
<style lang="scss" scoped>
.co-box {
@ -47,4 +75,22 @@ const contactWriteData = ref({
border: 1px solid rgba(195, 202, 245, 1);
padding: 10px 16px;
}
.step {
width: 33.33%;
&[active="true"] {
--setp-background-color: #edf0ff;
--setp-border-color: rgba(195, 202, 245, 1);
--setp-font-color: #162582;
}
&[completed="true"] {
--setp-font-color: #666;
&::after {
--setp-border-color: #8191F1;
background-color: var(--primary-color);
}
&:hover {
cursor: pointer;
}
}
}
</style>

0
src/views/work/components/CoHandlingPoliceEdit.vue → src/views/work/components/templates/CoHandlingPoliceEdit.vue

103
src/views/work/components/templates/Comments.vue

@ -0,0 +1,103 @@
<template>
<div>
<header>审批意见</header>
<div class="comments-container flex gap-20">
<div
class="item"
v-for="(item, index) in approvals"
:key="index"
:approved="item.approved"
>
<div class="flex center mb-20 relative comments-header">
<icon
name="el-icon-CircleCheck"
:size="41"
color="var(--primary-color)"
v-if="item.approved"
/>
<div class="icon" v-else></div>
</div>
<h1 class="text-center mb-20">
{{ item.approved ? "审批通过" : "待审批" }}
</h1>
<h2 class="text-center mb-20">
{{ item.handlerDeptName }} {{ item.handlerName }}
</h2>
<div v-if="item.approved">
<h3>审批意见</h3>
<p>{{ item.comment }}</p>
<h4 class="text-right">{{ item.createTime }}</h4>
</div>
</div>
</div>
</div>
</template>
<script setup>
defineProps({
approvals: {
type: Array,
default: () => [],
},
});
</script>
<style lang="scss" scoped>
header {
font-size: 20px;
color: var(--primary-color);
padding: 20px 0;
}
.item {
width: 100%;
padding: 8px;
--base-color: #999;
--second-color: #D8D8D8;
.comments-header {
&::before {
display: block;
content: "";
position: absolute;
top: 50%;
right: calc(50% + 30px);
width: calc(100% - 30px);
border-top: 2px solid var(--second-color);
}
}
&:first-child .comments-header::before {
display: none;
}
&[approved="true"] {
--base-color: var(--primary-color);
--second-color: #8595FB;
}
h1 {
color: var(--base-color);
font-size: 18px;
font-weight: 700;
}
h2 {
font-size: 16px;
font-weight: 500;
}
h3 {
font-size: 14px;
color: #666;
font-weight: 500;
margin-bottom: 8px;
}
h4 {
font-size: 12px;
color: #666;
font-weight: 500;
}
p {
color: #333;
}
.icon {
width: 22px;
height: 22px;
border: 3px solid #e0e0e0;
border-radius: 50%;
margin: 6.5px;
}
}
</style>

0
src/views/work/components/ContactWriterFrom.vue → src/views/work/components/templates/ContactWriterFrom.vue

0
src/views/work/components/DeptSelectForm.vue → src/views/work/components/templates/DeptSelectForm.vue

51
src/views/work/components/InterviewWriterFrom.vue → src/views/work/components/templates/InterviewWriterFrom.vue

@ -23,14 +23,7 @@
</el-col>
<el-col :span="12" prop="interviewPoliceEmpNo">
<el-form-item label="接访领导" prop="interviewPoliceEmpNo">
<el-select v-model="form.interviewPoliceEmpNo" @change="handleSelect">
<el-option
v-for="item in leaders"
:key="item.id"
:label="item.name"
:value="item.empNo"
/>
</el-select>
<LeaderSelect v-model="form.interviewPoliceEmpNo" @change="handleSelect" />
</el-form-item>
</el-col>
</el-row>
@ -51,6 +44,20 @@
<el-row>
<el-col :span="12">
<el-form-item label="上传佐证" prop="interviewAttachments">
<el-upload
v-model:file-list="fileList"
:action="`${VITE_API_URL}/api/file/upload`"
:headers="{'Admin': getToken()}"
multiple
>
<el-button
>上传
<template #icon>
<icon name="el-icon-Upload" />
</template>
</el-button>
</el-upload>
</el-form-item>
</el-col>
<el-col :span="12">
@ -65,9 +72,11 @@
</el-form>
</template>
<script setup>
import { getToken } from "@/utils/auth";
import { useDictData } from "@/hooks/useDictOptions";
import { getLeaderList } from "@/api/perms/admin";
const { VITE_API_URL } = process.env;
const { dictData } = useDictData(["interview_type"]);
const form = ref({});
@ -97,23 +106,18 @@ const rules = {
message: "请填写接访情况",
},
],
interviewAttachments: [
{
required: true,
message: "请上传佐证",
},
],
// interviewAttachments: [
// {
// required: true,
// message: "",
// },
// ],
};
const leaders = ref([]);
getLeaderList().then((data) => {
console.log(data);
leaders.value = data;
});
function handleSelect(empNo) {
form.value.interviewPoliceName = leaders.value.filter(item => item.empNo === empNo)[0].name;
function handleSelect(empNo, option) {
form.value.interviewPoliceName = option.name;
}
const props = defineProps({
@ -143,4 +147,7 @@ defineExpose({
});
</script>
<style lang="scss" scoped>
p {
margin: 0;
}
</style>

165
src/views/work/components/templates/MailApprovalDetail.vue

@ -0,0 +1,165 @@
<template>
<el-collapse>
<el-collapse-item title="举报人/投诉人基本情况">
<div class="flex">
<div class="col">
<label>姓名</label>
<span>{{ mail.contactName }}</span>
</div>
<div class="col">
<label>性别</label>
<span>{{ mail.contactSex === 'M' ? '男' : '女' }}</span>
</div>
<div class="col">
<label>证件号码</label>
<span>{{ mail.contactIdCard }}</span>
</div>
<div class="col">
<label>联系电话</label>
<span>{{ mail.contactPhone }}</span>
</div>
</div>
</el-collapse-item>
<el-collapse-item title="举报/投诉对象基本情况">
<div class="flex" v-for="item in mail.verifyReportedPolices" :key="item.empNo">
<div class="col">
<label>被举报对象1</label>
<span>{{ item.name }}</span>
</div>
<div class="col">
<label>警号</label>
<span>{{ item.empNo }}</span>
</div>
<div class="col">
<label>性别</label>
<span>{{ item.gender }}</span>
</div>
<div class="col">
<label>出生年月</label>
<span>{{ item.birthday }}</span>
</div>
</div>
</el-collapse-item>
<el-collapse-item title="举报/投诉反应主要问题">
<div class="content">{{ mail.content }}</div>
</el-collapse-item>
<el-collapse-item title="领导接访情况">
<div class="flex mb-10">
<div class="col">
<label>主单位签收时长</label>
<span>{{ mail.mailTime }}</span>
</div>
<div class="col">
<label>主单位联系群众时长</label>
<span>{{ mail.source }}</span>
</div>
</div>
<div class="flex mb-10">
<div class="col">
<label>接访形式</label>
<span>{{ mail.interviewType }}</span>
</div>
<div class="col">
<label>是否一把手接访</label>
<span>{{ mail.interviewIsLeader }}</span>
</div>
<div class="col">
<label>接访领导</label>
<span>{{ mail.interviewPoliceName }}</span>
</div>
</div>
<div class="flex">
<div class="col">
<label>接访情况</label>
<span>{{ mail.interviewDetails }}</span>
</div>
</div>
</el-collapse-item>
<el-collapse-item title="核查办理情况">
<div class="flex mb-10">
<div class="col">
<label>是否属实</label>
<span>{{ mail.verifyIsTrue }}</span>
</div>
<div class="col">
<label>是否需要问责</label>
<span>{{ mail.verifyNeedAccountability }}</span>
</div>
</div>
<div class="flex mb-10">
<div class="col">
<label>查证属实问题</label>
<span v-for="(item, index) in mail.verifyProblem" :key="index">{{ item }}</span>
</div>
</div>
<div class="flex mb-10">
<div class="col">
<label>核办结果</label>
<span>{{ mail.verifyFeedback }}</span>
</div>
</div>
</el-collapse-item>
<el-collapse-item title="整改问责情况">
<div class="flex">
<div class="col">
<label>责任追究</label>
<span v-for="(item, index) in mail.verifyPunish" :key="index">{{ item }}</span>
</div>
</div>
</el-collapse-item>
<el-collapse-item title="结果反馈情况">
<div class="flex mb-10">
<div class="col">
<label>群众反应事项解决情况</label>
<span>{{ mail.verifyIsResolved }}</span>
</div>
<div class="col">
<label>办理反馈情况</label>
<span>{{ mail.source }}</span>
</div>
</div>
<div class="flex">
<div class="col">
<label>回访人姓名</label>
<span>{{ mail.source }}</span>
</div>
<div class="col">
<label>回访人电话</label>
<span>{{ mail.source }}</span>
</div>
<div class="col">
<label>回访人警号</label>
<span>{{ mail.source }}</span>
</div>
</div>
</el-collapse-item>
<el-collapse-item title="办结佐证材料">
<div></div>
</el-collapse-item>
</el-collapse>
</template>
<script setup>
defineProps({
mail: {
type: Object,
default: () => ({}),
},
});
</script>
<style lang="scss" scoped>
.el-collapse-item {
--el-collapse-header-text-color: var(--primary-color);
--el-collapse-header-font-size: 16px;
}
.col {
label {
width: 144px;
text-align: right;
}
}
.content {
font-size: 16px;
padding: 12px;
color: #333;
}
</style>

80
src/views/work/components/MailTypeForm.vue → src/views/work/components/templates/MailTypeForm.vue

@ -2,19 +2,18 @@
<div>
<h1>信件类型</h1>
<el-form :label-width="150" :model="form" :rules="rules" ref="formRef">
<el-row :gutter="20">
<el-col :span="12">
<div class="flex gap-20">
<div style="width: 50%">
<el-form-item label="信件分类" prop="mailCategoryId">
<el-tree-select
v-model="form.mailCategoryName"
:data="mailStore.mailCategorys"
filterable
@select="handleCategorySelect"
@current-change="handleCategoryChange"
/>
</el-form-item>
</el-col>
<el-col :span="12">
</div>
<div style="width: 50%">
<el-form-item label="信件等级" prop="mailLevel">
<el-select
v-model="form.mailLevel"
@ -29,52 +28,66 @@
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
</div>
</div>
<el-form-item label="处理流程" prop="flowType" v-if="simpleFlowFlag">
<el-radio-group v-model="form.simpleFlowFlag" class="ml-4">
<div>
<el-radio :label="false" size="large">正常流程</el-radio>
<p class="radio-tips">正常流程包含接访群众步骤</p>
</div>
<div>
<el-radio :label="true" size="largdie">简易流程</el-radio>
<p class="radio-tips">简化流程不包含接访群众步骤</p>
</div>
</el-radio-group>
</el-form-item>
</el-form>
</div>
</template>
<script setup>
import useMailStore from "@/stores/modules/mail";
import { useDictData } from "@/hooks/useDictOptions";
const mailStore = useMailStore();
//
mailStore.getMailCategorys();
const { dictData } = useDictData(["mail_level"]);
const form = ref({});
const formRef = ref()
const formRef = ref();
const flowTypeShow = ref(false);
const rules = {
mailCategoryName: [
mailCategory: [
{
required: true,
message: "请选择信件分类"
}
message: "请选择信件分类",
},
],
mailLevel: [
{
required: true,
message: "请选择信件等级"
}
message: "请选择信件等级",
},
],
simpleFlowFlag: [
{
required: true,
message: "请选择处理流程",
},
],
};
defineProps({
data: {
type: Object,
default: {}
}
})
default: {},
},
});
const emits = defineEmits(['update:data'])
function handleCategorySelect(item) {
form.value.mailCategory = item.label;
}
const emits = defineEmits(["update:data"]);
function handleCategoryChange(data, node) {
form.value.mailCategory = data.label;
if (node.level === 1) {
form.value.mailFirstCategory = data.label;
form.value.mailSecondCategory = null;
@ -90,13 +103,16 @@ function handleCategoryChange(data, node) {
form.value.mailSecondCategory = node.parent.data.label;
form.value.mailThreeCategory = data.label;
}
if (form.value.mailFirstCategory === "咨询类" || form.value.mailFirstCategory === "简易类") {
flowTypeShow.value = true
}
}
function validate() {
return new Promise((resolve, reject) => {
formRef.value.validate((valid) => {
if (valid) {
emits('update:data', form.value)
emits("update:data", form.value);
resolve(true);
} else {
reject();
@ -106,8 +122,18 @@ function validate() {
}
defineExpose({
validate
})
validate,
});
</script>
<style lang="scss" scoped>
.el-form {
color: #333;
}
.radio-tips {
font-size: 14px;
color: #666;
margin-top: 0;
margin-left: 20px;
margin-right: 20px;
}
</style>

98
src/views/work/components/templates/MainContactInfo.vue

@ -0,0 +1,98 @@
<template>
<div class="card">
<div class="flex">
<div class="col">
<label>来信时间</label>
<span>{{ mail.mailTime }}</span>
</div>
<div class="col">
<label>信件来源</label>
<span>{{ mail.source }}</span>
</div>
</div>
<h2>来信人信息</h2>
<div class="flex">
<div class="col short">
<label>姓名</label>
<span>{{ mail.contactName }}</span>
</div>
<div class="col short">
<label>性别</label>
<span>{{ mail.contactSex === 'M' ? '男':'女' }}</span>
</div>
<div class="col">
<label>证件号码</label>
<span>{{ mail.contactIdCard }}</span>
</div>
<div class="col">
<label>联系电话</label>
<span>{{ mail.contactPhone }}</span>
</div>
</div>
<h2>信件内容</h2>
<div class="flex">
<div class="col">
<label>案件编号</label>
<span>{{ mail.caseNumber }}</span>
</div>
<div class="col">
<label>被投诉/涉及单位</label>
<span>{{ mail.involvedDeptName }}</span>
</div>
</div>
<p class="content">{{ mail.content }}</p>
<h2>附件</h2>
<div class="flex gap-16 wrap img-container">
<div class="img-box" v-for="(item, index) in mail.attachments" :key="index" :style="{ backgroundImage: `url(${VITE_API_URL}/api/file/stream/${item.filepath})`}"></div>
</div>
<div>
<el-empty :image-size="40" style="--el-empty-padding: 0;" v-if="mail.attachments.length === 0" />
</div>
</div>
</template>
<script setup>
const {
VITE_API_URL
} = process.env
defineProps({
mail: {
type: Object,
default: () => ({})
}
})
</script>
<style lang="scss" scoped>
h2 {
font-size: 20px;
font-weight: 500;
color: #162582;
}
.card {
background: #f9faff;
border-top: 8px solid var(--primary-color);
box-shadow: 0px 2px 4px 0px rgba(129, 150, 255, 0.47);
padding: 16px;
margin-bottom: 24px;
margin-left: 2px;
margin-right: 2px;
.content {
margin-top: 16px;
min-height: 40px;
padding: 12px;
background-color: #fff;
color: #333;
font-size: 16px;
}
.img-container {
.img-box {
width: 80px;
height: 80px;
background-size: cover;
background-position: center;
box-shadow: 1px 1px 10px rgba(0, 0, 0, 0.1);
}
}
}
</style>

21
src/views/work/components/VerifyForm.vue → src/views/work/components/templates/VerifyForm.vue

@ -150,8 +150,19 @@
<el-row>
<el-col :span="12">
<el-form-item label="上传佐证">
<el-upload multiple>
<el-button>上传</el-button>
<el-upload
v-model:file-list="fileList"
:action="`${VITE_API_URL}/api/file/upload`"
:headers="{'Admin': getToken()}"
multiple
>
<el-button
>上传
<template #icon>
<icon name="el-icon-Upload" />
</template>
</el-button>
</el-upload>
</el-form-item>
</el-col>
@ -177,7 +188,8 @@
import { useDictData } from "@/hooks/useDictOptions";
import { listByThree } from "@/api/org/department";
import { allLists } from "@/api/perms/admin";
import { getToken } from "@/utils/auth";
const { VITE_API_URL } = process.env;
const { dictData } = useDictData(["verify_problem", "verify_punish"]);
const threeDepts = ref([]);
const polices = ref([]);
@ -265,7 +277,8 @@ function validate() {
const verifyReportedPolices = reportedPolices.value.filter(item => item.empNo)
form.value.verifyReportedPolices = JSON.stringify(verifyReportedPolices)
form.value.verifyFollowupPolice = JSON.stringify(form.value.verifyFollowupPolice)
emits("update:data", form.value);
const data = { ...props.data, ...form.value };
emits("update:data", data);
resolve(true);
} else {
reject();

7
vite.config.ts

@ -1,6 +1,6 @@
import { fileURLToPath } from 'url'
import { defineConfig } from 'vite'
import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
@ -10,8 +10,11 @@ import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
// https://vitejs.dev/config/
export default defineConfig({
export default ({ mode }) => defineConfig({
base: '/lan/',
define: {
'process.env': loadEnv(mode, process.cwd())
},
server: {
host: '0.0.0.0',
proxy: {

Loading…
Cancel
Save