@ -0,0 +1,9 @@
|
||||
.history/ |
||||
/node_modules/ |
||||
/v2/ |
||||
*.zip |
||||
|
||||
package-lock.json |
||||
pnpm-lock.yaml |
||||
auto-imports.d.ts |
||||
components.d.ts |
||||
@ -0,0 +1,16 @@
|
||||
<!doctype html> |
||||
<html lang="en"> |
||||
|
||||
<head> |
||||
<meta charset="UTF-8" /> |
||||
<link rel="icon" type="image/svg+xml" href="/favicon.png" /> |
||||
<meta name="viewport" |
||||
content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no"> |
||||
<title>数字督察一体化平台</title> |
||||
</head> |
||||
|
||||
<body> |
||||
<div id="app"></div> |
||||
<script type="module" src="/src/main.ts"></script> |
||||
</body> |
||||
</html> |
||||
@ -0,0 +1,61 @@
|
||||
{ |
||||
"name": "supervision-vue", |
||||
"private": true, |
||||
"version": "1.0", |
||||
"type": "module", |
||||
"scripts": { |
||||
"dev": "vite", |
||||
"build": "vite build --mode dev", |
||||
"build:prod": "vite build --mode prod && WinRAR a -r v2.zip ./v2/", |
||||
"preview": "vite preview" |
||||
}, |
||||
"engines": { |
||||
"nodejs": ">=16.0.0", |
||||
"npm": ">=7.0.0" |
||||
}, |
||||
"dependencies": { |
||||
"@element-plus/icons-vue": "^2.3.1", |
||||
"@univerjs/core": "^0.2.5", |
||||
"@univerjs/data-validation": "^0.2.5", |
||||
"@univerjs/design": "^0.2.5", |
||||
"@univerjs/docs": "^0.2.5", |
||||
"@univerjs/docs-ui": "^0.2.5", |
||||
"@univerjs/engine-formula": "^0.2.5", |
||||
"@univerjs/engine-numfmt": "^0.2.5", |
||||
"@univerjs/engine-render": "^0.2.5", |
||||
"@univerjs/facade": "^0.2.5", |
||||
"@univerjs/sheets": "^0.2.5", |
||||
"@univerjs/sheets-data-validation": "^0.2.5", |
||||
"@univerjs/sheets-formula": "^0.2.5", |
||||
"@univerjs/sheets-ui": "^0.2.5", |
||||
"@univerjs/ui": "^0.2.5", |
||||
"@vue-office/docx": "^1.6.0", |
||||
"@vue-office/excel": "^1.7.11", |
||||
"amfe-flexible": "^2.2.1", |
||||
"baidu-map-vue3": "^0.4.9", |
||||
"crypto-js": "^4.2.0", |
||||
"echarts": "^5.4.3", |
||||
"element-plus": "^2.5.1", |
||||
"install": "^0.13.0", |
||||
"lodash": "^4.17.21", |
||||
"moment": "^2.30.1", |
||||
"nprogress": "^0.2.0", |
||||
"pinia": "^2.1.7", |
||||
"typescript": "^5.3.3", |
||||
"vue": "^3.3.11", |
||||
"vue-echarts": "^6.6.8", |
||||
"vue-router": "^4.2.5" |
||||
}, |
||||
"devDependencies": { |
||||
"@univerjs/vite-plugin": "^0.5.0", |
||||
"@vitejs/plugin-vue": "^4.5.2", |
||||
"amfe-flexible": "^2.2.1", |
||||
"mitt": "^3.0.1", |
||||
"postcss-pxtorem": "^6.1.0", |
||||
"sass": "^1.69.7", |
||||
"unplugin-auto-import": "^0.17.3", |
||||
"unplugin-vue-components": "^0.26.0", |
||||
"vite": "^5.0.8", |
||||
"vite-plugin-svg-icons": "^2.0.1" |
||||
} |
||||
} |
||||
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 225 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 115 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 954 KiB |
|
After Width: | Height: | Size: 268 KiB |
|
After Width: | Height: | Size: 587 KiB |
|
After Width: | Height: | Size: 522 KiB |
|
After Width: | Height: | Size: 398 KiB |
|
After Width: | Height: | Size: 269 KiB |
|
After Width: | Height: | Size: 210 KiB |
|
After Width: | Height: | Size: 266 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 142 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 37 KiB |
@ -0,0 +1,13 @@
|
||||
<template> |
||||
<el-config-provider :locale="elConfig.locale" :z-index="elConfig.zIndex" > |
||||
<router-view /> |
||||
</el-config-provider> |
||||
</template> |
||||
<script setup> |
||||
import zhCn from 'element-plus/es/locale/lang/zh-cn' |
||||
|
||||
const elConfig = { |
||||
zIndex: 3000, |
||||
locale: zhCn |
||||
} |
||||
</script> |
||||
@ -0,0 +1,12 @@
|
||||
# 数字督察 |
||||
|
||||
## 安装 |
||||
```bash |
||||
# 使用淘宝源 |
||||
npm config set registry https://registry.npmmirror.com |
||||
npm install |
||||
``` |
||||
## 运行 |
||||
```bash |
||||
npm run start |
||||
``` |
||||
@ -0,0 +1,21 @@
|
||||
import request from "./request"; |
||||
|
||||
export function login(body) { |
||||
return request.post({ |
||||
url: '/login', |
||||
body |
||||
}); |
||||
} |
||||
|
||||
export function logout() { |
||||
return request.post({ |
||||
url: '/logout' |
||||
}); |
||||
} |
||||
|
||||
export function getSelf(body) { |
||||
return request.get({ |
||||
url: '/auth/self', |
||||
body |
||||
}); |
||||
} |
||||
@ -0,0 +1,111 @@
|
||||
import { ElMessageBox } from 'element-plus' |
||||
import { getToken, deleteToken } from '@/utils/token' |
||||
import feedback from '@/utils/feedback' |
||||
|
||||
|
||||
export const BASE_PATH = '/api/v2' |
||||
|
||||
type Options = { |
||||
url: string, |
||||
method?: 'GET' | 'POST' | 'PUT' | 'DELETE', |
||||
headers?: Record<string, string>, |
||||
query?: Record<string, any>, |
||||
params?: Record<string, any>, |
||||
body?: string | FormData | Record<string, any>, |
||||
showErrorMsg: bool |
||||
}; |
||||
|
||||
function get(options: Options) { |
||||
options.method = 'GET'; |
||||
return ajax(options.url, options) |
||||
} |
||||
|
||||
function post(options: Options) { |
||||
options.method = 'POST'; |
||||
return ajax(options.url, options) |
||||
} |
||||
|
||||
function put(options: Options) { |
||||
options.method = 'PUT'; |
||||
return ajax(options.url, options) |
||||
} |
||||
|
||||
function del(options: Options) { |
||||
options.method = 'DELETE'; |
||||
return ajax(options.url, options) |
||||
} |
||||
|
||||
let isRelogin = false; |
||||
function ajax(url: string, options: Options) { |
||||
const headers: Record<string, string> = { |
||||
"Content-Type": "application/json", |
||||
"Authorization": getToken() |
||||
}; |
||||
let body: string | FormData; |
||||
if (options?.params && Object.keys(options.params).length > 0) { |
||||
if (options.method === 'GET') { |
||||
options.query = options.params; |
||||
} else { |
||||
body = JSON.stringify(options.params); |
||||
} |
||||
} |
||||
if (options?.query) { |
||||
// 解决 query 的值为 undefined 的情况
|
||||
if (options.query instanceof Object) { |
||||
const params = {} |
||||
for (let k of Object.keys(options.query)) { |
||||
if (options.query[k] === undefined || options.query[k] === null) { |
||||
continue; |
||||
} |
||||
params[k] = options.query[k] |
||||
} |
||||
url += (url.indexOf('?') > -1 ? '&' : '?') + new URLSearchParams(params).toString(); |
||||
} else { |
||||
url += (url.indexOf('?') > -1 ? '&' : '?') + new URLSearchParams(options.query).toString(); |
||||
} |
||||
} |
||||
if (options?.body) { |
||||
if (options.body instanceof String || options.body instanceof FormData) { |
||||
body = options.body; |
||||
} else { |
||||
if (options.body instanceof Array || (options.body instanceof Object && Object.keys(options.body).length > 0)) { |
||||
body = JSON.stringify(options.body); |
||||
} |
||||
} |
||||
} |
||||
return new Promise((resolve, reject) => { |
||||
fetch(`${BASE_PATH}${url}`, { |
||||
method: options.method, |
||||
body: body, |
||||
headers: { ...headers, ...options.headers } |
||||
}).then(response => { |
||||
if (response.status === 413) { |
||||
return; |
||||
} |
||||
return response.json(); |
||||
}).then(res => { |
||||
if (res.code === 200) { |
||||
resolve(res.data) |
||||
} else { |
||||
let message = res.message; |
||||
if (res.code === 401) { |
||||
// deleteToken()
|
||||
location.href = '/' |
||||
} |
||||
feedback.msgError(message) |
||||
reject(res) |
||||
} |
||||
|
||||
}) |
||||
}) |
||||
} |
||||
|
||||
|
||||
const request = { |
||||
get, |
||||
post, |
||||
put, |
||||
del |
||||
} |
||||
|
||||
export default request; |
||||
@ -0,0 +1,15 @@
|
||||
import request from "@/api/request"; |
||||
|
||||
export function listModel(query) { |
||||
return request.get({ |
||||
url: '/model', |
||||
query |
||||
}); |
||||
} |
||||
|
||||
export function addModel(body) { |
||||
return request.post({ |
||||
url: '/model', |
||||
body |
||||
}); |
||||
} |
||||
@ -0,0 +1,7 @@
|
||||
import request from "@/api/request"; |
||||
|
||||
export function listModelClass() { |
||||
return request.get({ |
||||
url: '/modelClass' |
||||
}); |
||||
} |
||||
@ -0,0 +1,7 @@
|
||||
import request from "./request"; |
||||
|
||||
export function flowNumberAndTodayNumber() { |
||||
return request.get({ |
||||
url: '/statistics/flowNumberAndTodayNumber' |
||||
}); |
||||
} |
||||
@ -0,0 +1,40 @@
|
||||
import request from "@/api/request"; |
||||
|
||||
export function departTreeList(query) { |
||||
return request.get({ |
||||
url: '/depart', |
||||
query |
||||
}); |
||||
} |
||||
|
||||
export function departTree() { |
||||
return request.get({ |
||||
url: '/depart/tree' |
||||
}); |
||||
} |
||||
|
||||
export function secondList() { |
||||
return request.get({ |
||||
url: '/depart/second' |
||||
}); |
||||
} |
||||
|
||||
export function listChildren(departId) { |
||||
return request.get({ |
||||
url: `/depart/${departId}/children` |
||||
}); |
||||
} |
||||
|
||||
export function addDepart(body) { |
||||
return request.post({ |
||||
url: '/depart', |
||||
body |
||||
}); |
||||
} |
||||
|
||||
export function updateDepart(body) { |
||||
return request.put({ |
||||
url: '/depart', |
||||
body |
||||
}); |
||||
} |
||||
@ -0,0 +1,49 @@
|
||||
import request from "@/api/request"; |
||||
|
||||
export function listDictTypes(query) { |
||||
return request.get({ |
||||
url: '/dict', |
||||
query |
||||
}); |
||||
} |
||||
|
||||
export function addDictType(body) { |
||||
return request.post({ |
||||
url: '/dict', |
||||
body |
||||
}); |
||||
} |
||||
|
||||
export function listDictDatas(query, dictType) { |
||||
return request.get({ |
||||
url: `/dict/${dictType}/dictData`, |
||||
query |
||||
}); |
||||
} |
||||
|
||||
export function addDictData(body, dictType) { |
||||
return request.post({ |
||||
url: `/dict/${dictType}/dictData`, |
||||
body |
||||
}); |
||||
} |
||||
|
||||
export function updateDictData(body, dictType) { |
||||
return request.put({ |
||||
url: `/dict/${dictType}/dictData`, |
||||
body |
||||
}); |
||||
} |
||||
|
||||
export function delDictData(dictCode, dictType) { |
||||
return request.del({ |
||||
url: `/dict/${dictType}/dictData/${dictCode}` |
||||
}); |
||||
} |
||||
|
||||
export function listDictDataAll(dictType) { |
||||
return request.get({ |
||||
url: `/dict/data/${dictType}` |
||||
}); |
||||
} |
||||
|
||||
@ -0,0 +1,27 @@
|
||||
import request from "@/api/request"; |
||||
|
||||
export function listDictContentTree() { |
||||
return request.get({ |
||||
url: `/dict/content/tree` |
||||
}); |
||||
} |
||||
|
||||
export function addDictContent(body) { |
||||
return request.post({ |
||||
url: `/dict/content`, |
||||
body |
||||
}); |
||||
} |
||||
|
||||
export function updateDictContent(body) { |
||||
return request.put({ |
||||
url: `/dict/content`, |
||||
body |
||||
}); |
||||
} |
||||
|
||||
export function delDictContent(id) { |
||||
return request.del({ |
||||
url: `/dict/content/${id}` |
||||
}); |
||||
} |
||||
@ -0,0 +1,27 @@
|
||||
import request from "@/api/request"; |
||||
|
||||
export function listMenu() { |
||||
return request.get({ |
||||
url: '/menu' |
||||
}); |
||||
} |
||||
|
||||
export function addMenu(body) { |
||||
return request.post({ |
||||
url: '/menu', |
||||
body |
||||
}); |
||||
} |
||||
|
||||
export function updateMenu(body) { |
||||
return request.put({ |
||||
url: '/menu', |
||||
body |
||||
}); |
||||
} |
||||
|
||||
export function delMenu(id) { |
||||
return request.del({ |
||||
url: '/menu/' + id |
||||
}); |
||||
} |
||||
@ -0,0 +1,41 @@
|
||||
import request from "@/api/request"; |
||||
|
||||
export function listPolice(query) { |
||||
return request.get({ |
||||
url: '/police', |
||||
query |
||||
}); |
||||
} |
||||
|
||||
export function addPolice(body) { |
||||
return request.post({ |
||||
url: '/police', |
||||
body |
||||
}); |
||||
} |
||||
|
||||
export function updatePolice(body) { |
||||
return request.put({ |
||||
url: '/police', |
||||
body |
||||
}); |
||||
} |
||||
|
||||
export function listPoliceAll(departId) { |
||||
return request.get({ |
||||
url: `/police/${departId}/all` |
||||
}); |
||||
} |
||||
|
||||
export function getPoliceAuths(idCode) { |
||||
return request.get({ |
||||
url: `/police/auth/${idCode}` |
||||
}); |
||||
} |
||||
|
||||
export function updatePoliceAuths(idCode, body) { |
||||
return request.post({ |
||||
url: `/police/auth/${idCode}`, |
||||
body |
||||
}); |
||||
} |
||||
@ -0,0 +1,41 @@
|
||||
import request from "@/api/request"; |
||||
|
||||
export function listRole(query) { |
||||
return request.get({ |
||||
url: '/role', |
||||
query |
||||
}); |
||||
} |
||||
|
||||
export function addRole(body) { |
||||
return request.post({ |
||||
url: '/role', |
||||
body |
||||
}); |
||||
} |
||||
|
||||
export function updateRole(body) { |
||||
return request.put({ |
||||
url: '/role', |
||||
body |
||||
}); |
||||
} |
||||
|
||||
export function delRole(id) { |
||||
return request.del({ |
||||
url: `/role/${id}` |
||||
}); |
||||
} |
||||
|
||||
export function getRoleMenu(roleCode) { |
||||
return request.get({ |
||||
url: `/role/${roleCode}/menu` |
||||
}); |
||||
} |
||||
|
||||
export function updateRoleMenu(roleCode, menuIds) { |
||||
return request.post({ |
||||
url: `/role/${roleCode}/menu`, |
||||
body: menuIds |
||||
}); |
||||
} |
||||
@ -0,0 +1,15 @@
|
||||
import request from "@/api/request"; |
||||
|
||||
export function listUser(query) { |
||||
return request.get({ |
||||
url: '/user', |
||||
query |
||||
}); |
||||
} |
||||
|
||||
export function updateUser(body) { |
||||
return request.post({ |
||||
url: '/user', |
||||
body |
||||
}); |
||||
} |
||||
@ -0,0 +1,21 @@
|
||||
import request from "@/api/request"; |
||||
|
||||
export function listFav(query) { |
||||
return request.get({ |
||||
url: '/negative/fav', |
||||
query |
||||
}); |
||||
} |
||||
|
||||
export function addFav(negativeId) { |
||||
return request.post({ |
||||
url: '/negative/fav', |
||||
body: {negativeId} |
||||
}); |
||||
} |
||||
|
||||
export function delFav(id) { |
||||
return request.del({ |
||||
url: `/negative/fav/${id}` |
||||
}); |
||||
} |
||||
@ -0,0 +1,7 @@
|
||||
import request from "@/api/request"; |
||||
|
||||
export function listFlowNode() { |
||||
return request.get({ |
||||
url: '/negative/flowNode' |
||||
}); |
||||
} |
||||
@ -0,0 +1,15 @@
|
||||
import request from "@/api/request"; |
||||
|
||||
export function listTodos(query) { |
||||
return request.get({ |
||||
url: '/work/todo', |
||||
query |
||||
}); |
||||
} |
||||
|
||||
export function listDones(query) { |
||||
return request.get({ |
||||
url: '/work/done', |
||||
query |
||||
}); |
||||
} |
||||
@ -0,0 +1,36 @@
|
||||
import request from "@/api/request"; |
||||
|
||||
export function listNegative(query) { |
||||
return request.get({ |
||||
url: `/negative`, |
||||
query |
||||
}); |
||||
} |
||||
|
||||
export function getNegativeDetails(id, workId) { |
||||
const query = workId ? {workId} : null; |
||||
return request.get({ |
||||
url: `/negative/${id}`, |
||||
query |
||||
}); |
||||
} |
||||
|
||||
export function addNegative(body) { |
||||
return request.post({ |
||||
url: `/negative`, |
||||
body |
||||
}); |
||||
} |
||||
|
||||
export function negativeExecute(id, body) { |
||||
return request.post({ |
||||
url: `/negative/${id}/execute`, |
||||
body |
||||
}); |
||||
} |
||||
|
||||
export function generateOriginId(problemSourcesCode, businessTypeCode) { |
||||
return request.post({ |
||||
url: `/negative/${problemSourcesCode}/${businessTypeCode}/generateOriginId` |
||||
}); |
||||
} |
||||
@ -0,0 +1,494 @@
|
||||
const data = [ |
||||
{ name: "海门", value: 9 }, |
||||
{ name: "鄂尔多斯", value: 12 }, |
||||
{ name: "招远", value: 12 }, |
||||
{ name: "舟山", value: 12 }, |
||||
{ name: "齐齐哈尔", value: 14 }, |
||||
{ name: "盐城", value: 15 }, |
||||
{ name: "赤峰", value: 16 }, |
||||
{ name: "青岛", value: 18 }, |
||||
{ name: "乳山", value: 18 }, |
||||
{ name: "金昌", value: 19 }, |
||||
{ name: "泉州", value: 21 }, |
||||
{ name: "莱西", value: 21 }, |
||||
{ name: "日照", value: 21 }, |
||||
{ name: "胶南", value: 22 }, |
||||
{ name: "南通", value: 23 }, |
||||
{ name: "拉萨", value: 24 }, |
||||
{ name: "云浮", value: 24 }, |
||||
{ name: "梅州", value: 25 }, |
||||
{ name: "文登", value: 25 }, |
||||
{ name: "上海", value: 25 }, |
||||
{ name: "攀枝花", value: 25 }, |
||||
{ name: "威海", value: 25 }, |
||||
{ name: "承德", value: 25 }, |
||||
{ name: "厦门", value: 26 }, |
||||
{ name: "汕尾", value: 26 }, |
||||
{ name: "潮州", value: 26 }, |
||||
{ name: "丹东", value: 27 }, |
||||
{ name: "太仓", value: 27 }, |
||||
{ name: "曲靖", value: 27 }, |
||||
{ name: "烟台", value: 28 }, |
||||
{ name: "福州", value: 29 }, |
||||
{ name: "瓦房店", value: 30 }, |
||||
{ name: "即墨", value: 30 }, |
||||
{ name: "抚顺", value: 31 }, |
||||
{ name: "玉溪", value: 31 }, |
||||
{ name: "张家口", value: 31 }, |
||||
{ name: "阳泉", value: 31 }, |
||||
{ name: "莱州", value: 32 }, |
||||
{ name: "湖州", value: 32 }, |
||||
{ name: "汕头", value: 32 }, |
||||
{ name: "昆山", value: 33 }, |
||||
{ name: "宁波", value: 33 }, |
||||
{ name: "湛江", value: 33 }, |
||||
{ name: "揭阳", value: 34 }, |
||||
{ name: "荣成", value: 34 }, |
||||
{ name: "连云港", value: 35 }, |
||||
{ name: "葫芦岛", value: 35 }, |
||||
{ name: "常熟", value: 36 }, |
||||
{ name: "东莞", value: 36 }, |
||||
{ name: "河源", value: 36 }, |
||||
{ name: "淮安", value: 36 }, |
||||
{ name: "泰州", value: 36 }, |
||||
{ name: "南宁", value: 37 }, |
||||
{ name: "营口", value: 37 }, |
||||
{ name: "惠州", value: 37 }, |
||||
{ name: "江阴", value: 37 }, |
||||
{ name: "蓬莱", value: 37 }, |
||||
{ name: "韶关", value: 38 }, |
||||
{ name: "嘉峪关", value: 38 }, |
||||
{ name: "广州", value: 38 }, |
||||
{ name: "延安", value: 38 }, |
||||
{ name: "太原", value: 39 }, |
||||
{ name: "清远", value: 39 }, |
||||
{ name: "中山", value: 39 }, |
||||
{ name: "昆明", value: 39 }, |
||||
{ name: "寿光", value: 40 }, |
||||
{ name: "盘锦", value: 40 }, |
||||
{ name: "长治", value: 41 }, |
||||
{ name: "深圳", value: 41 }, |
||||
{ name: "珠海", value: 42 }, |
||||
{ name: "宿迁", value: 43 }, |
||||
{ name: "咸阳", value: 43 }, |
||||
{ name: "铜川", value: 44 }, |
||||
{ name: "平度", value: 44 }, |
||||
{ name: "佛山", value: 44 }, |
||||
{ name: "海口", value: 44 }, |
||||
{ name: "江门", value: 45 }, |
||||
{ name: "章丘", value: 45 }, |
||||
{ name: "肇庆", value: 46 }, |
||||
{ name: "大连", value: 47 }, |
||||
{ name: "临汾", value: 47 }, |
||||
{ name: "吴江", value: 47 }, |
||||
{ name: "石嘴山", value: 49 }, |
||||
{ name: "沈阳", value: 50 }, |
||||
{ name: "苏州", value: 50 }, |
||||
{ name: "茂名", value: 50 }, |
||||
{ name: "嘉兴", value: 51 }, |
||||
{ name: "长春", value: 51 }, |
||||
{ name: "胶州", value: 52 }, |
||||
{ name: "银川", value: 52 }, |
||||
{ name: "张家港", value: 52 }, |
||||
{ name: "三门峡", value: 53 }, |
||||
{ name: "锦州", value: 54 }, |
||||
{ name: "南昌", value: 54 }, |
||||
{ name: "柳州", value: 54 }, |
||||
{ name: "三亚", value: 54 }, |
||||
{ name: "自贡", value: 56 }, |
||||
{ name: "吉林", value: 56 }, |
||||
{ name: "阳江", value: 57 }, |
||||
{ name: "泸州", value: 57 }, |
||||
{ name: "西宁", value: 57 }, |
||||
{ name: "宜宾", value: 58 }, |
||||
{ name: "呼和浩特", value: 58 }, |
||||
{ name: "成都", value: 58 }, |
||||
{ name: "大同", value: 58 }, |
||||
{ name: "镇江", value: 59 }, |
||||
{ name: "桂林", value: 59 }, |
||||
{ name: "张家界", value: 59 }, |
||||
{ name: "宜兴", value: 59 }, |
||||
{ name: "北海", value: 60 }, |
||||
{ name: "西安", value: 61 }, |
||||
{ name: "金坛", value: 62 }, |
||||
{ name: "东营", value: 62 }, |
||||
{ name: "牡丹江", value: 63 }, |
||||
{ name: "遵义", value: 63 }, |
||||
{ name: "绍兴", value: 63 }, |
||||
{ name: "扬州", value: 64 }, |
||||
{ name: "常州", value: 64 }, |
||||
{ name: "潍坊", value: 65 }, |
||||
{ name: "重庆", value: 66 }, |
||||
{ name: "台州", value: 67 }, |
||||
{ name: "南京", value: 67 }, |
||||
{ name: "滨州", value: 70 }, |
||||
{ name: "贵阳", value: 71 }, |
||||
{ name: "无锡", value: 71 }, |
||||
{ name: "本溪", value: 71 }, |
||||
{ name: "克拉玛依", value: 72 }, |
||||
{ name: "渭南", value: 72 }, |
||||
{ name: "马鞍山", value: 72 }, |
||||
{ name: "宝鸡", value: 72 }, |
||||
{ name: "焦作", value: 75 }, |
||||
{ name: "句容", value: 75 }, |
||||
{ name: "北京", value: 79 }, |
||||
{ name: "徐州", value: 79 }, |
||||
{ name: "衡水", value: 80 }, |
||||
{ name: "包头", value: 80 }, |
||||
{ name: "绵阳", value: 80 }, |
||||
{ name: "乌鲁木齐", value: 84 }, |
||||
{ name: "枣庄", value: 84 }, |
||||
{ name: "杭州", value: 84 }, |
||||
{ name: "淄博", value: 85 }, |
||||
{ name: "鞍山", value: 86 }, |
||||
{ name: "溧阳", value: 86 }, |
||||
{ name: "库尔勒", value: 86 }, |
||||
{ name: "安阳", value: 90 }, |
||||
{ name: "开封", value: 90 }, |
||||
{ name: "济南", value: 92 }, |
||||
{ name: "德阳", value: 93 }, |
||||
{ name: "温州", value: 95 }, |
||||
{ name: "九江", value: 96 }, |
||||
{ name: "邯郸", value: 98 }, |
||||
{ name: "临安", value: 99 }, |
||||
{ name: "兰州", value: 99 }, |
||||
{ name: "沧州", value: 100 }, |
||||
{ name: "临沂", value: 103 }, |
||||
{ name: "南充", value: 104 }, |
||||
{ name: "天津", value: 105 }, |
||||
{ name: "富阳", value: 106 }, |
||||
{ name: "泰安", value: 112 }, |
||||
{ name: "诸暨", value: 112 }, |
||||
{ name: "郑州", value: 113 }, |
||||
{ name: "哈尔滨", value: 114 }, |
||||
{ name: "聊城", value: 116 }, |
||||
{ name: "芜湖", value: 117 }, |
||||
{ name: "唐山", value: 119 }, |
||||
{ name: "平顶山", value: 119 }, |
||||
{ name: "邢台", value: 119 }, |
||||
{ name: "德州", value: 120 }, |
||||
{ name: "济宁", value: 120 }, |
||||
{ name: "荆州", value: 127 }, |
||||
{ name: "宜昌", value: 130 }, |
||||
{ name: "义乌", value: 132 }, |
||||
{ name: "丽水", value: 133 }, |
||||
{ name: "洛阳", value: 134 }, |
||||
{ name: "秦皇岛", value: 136 }, |
||||
{ name: "株洲", value: 143 }, |
||||
{ name: "石家庄", value: 147 }, |
||||
{ name: "莱芜", value: 148 }, |
||||
{ name: "常德", value: 152 }, |
||||
{ name: "保定", value: 153 }, |
||||
{ name: "湘潭", value: 154 }, |
||||
{ name: "金华", value: 157 }, |
||||
{ name: "岳阳", value: 169 }, |
||||
{ name: "长沙", value: 175 }, |
||||
{ name: "衢州", value: 177 }, |
||||
{ name: "廊坊", value: 193 }, |
||||
{ name: "菏泽", value: 194 }, |
||||
{ name: "合肥", value: 229 }, |
||||
{ name: "武汉", value: 273 }, |
||||
{ name: "大庆", value: 279 } |
||||
]; |
||||
const geoCoordMap = { |
||||
海门: [121.15, 31.89], |
||||
鄂尔多斯: [109.781327, 39.608266], |
||||
招远: [120.38, 37.35], |
||||
舟山: [122.207216, 29.985295], |
||||
齐齐哈尔: [123.97, 47.33], |
||||
盐城: [120.13, 33.38], |
||||
赤峰: [118.87, 42.28], |
||||
青岛: [120.33, 36.07], |
||||
乳山: [121.52, 36.89], |
||||
金昌: [102.188043, 38.520089], |
||||
泉州: [118.58, 24.93], |
||||
莱西: [120.53, 36.86], |
||||
日照: [119.46, 35.42], |
||||
胶南: [119.97, 35.88], |
||||
南通: [121.05, 32.08], |
||||
拉萨: [91.11, 29.97], |
||||
云浮: [112.02, 22.93], |
||||
梅州: [116.1, 24.55], |
||||
文登: [122.05, 37.2], |
||||
上海: [121.48, 31.22], |
||||
攀枝花: [101.718637, 26.582347], |
||||
威海: [122.1, 37.5], |
||||
承德: [117.93, 40.97], |
||||
厦门: [118.1, 24.46], |
||||
汕尾: [115.375279, 22.786211], |
||||
潮州: [116.63, 23.68], |
||||
丹东: [124.37, 40.13], |
||||
太仓: [121.1, 31.45], |
||||
曲靖: [103.79, 25.51], |
||||
烟台: [121.39, 37.52], |
||||
福州: [119.3, 26.08], |
||||
瓦房店: [121.979603, 39.627114], |
||||
即墨: [120.45, 36.38], |
||||
抚顺: [123.97, 41.97], |
||||
玉溪: [102.52, 24.35], |
||||
张家口: [114.87, 40.82], |
||||
阳泉: [113.57, 37.85], |
||||
莱州: [119.942327, 37.177017], |
||||
湖州: [120.1, 30.86], |
||||
汕头: [116.69, 23.39], |
||||
昆山: [120.95, 31.39], |
||||
宁波: [121.56, 29.86], |
||||
湛江: [110.359377, 21.270708], |
||||
揭阳: [116.35, 23.55], |
||||
荣成: [122.41, 37.16], |
||||
连云港: [119.16, 34.59], |
||||
葫芦岛: [120.836932, 40.711052], |
||||
常熟: [120.74, 31.64], |
||||
东莞: [113.75, 23.04], |
||||
河源: [114.68, 23.73], |
||||
淮安: [119.15, 33.5], |
||||
泰州: [119.9, 32.49], |
||||
南宁: [108.33, 22.84], |
||||
营口: [122.18, 40.65], |
||||
惠州: [114.4, 23.09], |
||||
江阴: [120.26, 31.91], |
||||
蓬莱: [120.75, 37.8], |
||||
韶关: [113.62, 24.84], |
||||
嘉峪关: [98.289152, 39.77313], |
||||
广州: [113.23, 23.16], |
||||
延安: [109.47, 36.6], |
||||
太原: [112.53, 37.87], |
||||
清远: [113.01, 23.7], |
||||
中山: [113.38, 22.52], |
||||
昆明: [102.73, 25.04], |
||||
寿光: [118.73, 36.86], |
||||
盘锦: [122.070714, 41.119997], |
||||
长治: [113.08, 36.18], |
||||
深圳: [114.07, 22.62], |
||||
珠海: [113.52, 22.3], |
||||
宿迁: [118.3, 33.96], |
||||
咸阳: [108.72, 34.36], |
||||
铜川: [109.11, 35.09], |
||||
平度: [119.97, 36.77], |
||||
佛山: [113.11, 23.05], |
||||
海口: [110.35, 20.02], |
||||
江门: [113.06, 22.61], |
||||
章丘: [117.53, 36.72], |
||||
肇庆: [112.44, 23.05], |
||||
大连: [121.62, 38.92], |
||||
临汾: [111.5, 36.08], |
||||
吴江: [120.63, 31.16], |
||||
石嘴山: [106.39, 39.04], |
||||
沈阳: [123.38, 41.8], |
||||
苏州: [120.62, 31.32], |
||||
茂名: [110.88, 21.68], |
||||
嘉兴: [120.76, 30.77], |
||||
长春: [125.35, 43.88], |
||||
胶州: [120.03336, 36.264622], |
||||
银川: [106.27, 38.47], |
||||
张家港: [120.555821, 31.875428], |
||||
三门峡: [111.19, 34.76], |
||||
锦州: [121.15, 41.13], |
||||
南昌: [115.89, 28.68], |
||||
柳州: [109.4, 24.33], |
||||
三亚: [109.511909, 18.252847], |
||||
自贡: [104.778442, 29.33903], |
||||
吉林: [126.57, 43.87], |
||||
阳江: [111.95, 21.85], |
||||
泸州: [105.39, 28.91], |
||||
西宁: [101.74, 36.56], |
||||
宜宾: [104.56, 29.77], |
||||
呼和浩特: [111.65, 40.82], |
||||
成都: [104.06, 30.67], |
||||
大同: [113.3, 40.12], |
||||
镇江: [119.44, 32.2], |
||||
桂林: [110.28, 25.29], |
||||
张家界: [110.479191, 29.117096], |
||||
宜兴: [119.82, 31.36], |
||||
北海: [109.12, 21.49], |
||||
西安: [108.95, 34.27], |
||||
金坛: [119.56, 31.74], |
||||
东营: [118.49, 37.46], |
||||
牡丹江: [129.58, 44.6], |
||||
遵义: [106.9, 27.7], |
||||
绍兴: [120.58, 30.01], |
||||
扬州: [119.42, 32.39], |
||||
常州: [119.95, 31.79], |
||||
潍坊: [119.1, 36.62], |
||||
重庆: [106.54, 29.59], |
||||
台州: [121.420757, 28.656386], |
||||
南京: [118.78, 32.04], |
||||
滨州: [118.03, 37.36], |
||||
贵阳: [106.71, 26.57], |
||||
无锡: [120.29, 31.59], |
||||
本溪: [123.73, 41.3], |
||||
克拉玛依: [84.77, 45.59], |
||||
渭南: [109.5, 34.52], |
||||
马鞍山: [118.48, 31.56], |
||||
宝鸡: [107.15, 34.38], |
||||
焦作: [113.21, 35.24], |
||||
句容: [119.16, 31.95], |
||||
北京: [116.46, 39.92], |
||||
徐州: [117.2, 34.26], |
||||
衡水: [115.72, 37.72], |
||||
包头: [110, 40.58], |
||||
绵阳: [104.73, 31.48], |
||||
乌鲁木齐: [87.68, 43.77], |
||||
枣庄: [117.57, 34.86], |
||||
杭州: [120.19, 30.26], |
||||
淄博: [118.05, 36.78], |
||||
鞍山: [122.85, 41.12], |
||||
溧阳: [119.48, 31.43], |
||||
库尔勒: [86.06, 41.68], |
||||
安阳: [114.35, 36.1], |
||||
开封: [114.35, 34.79], |
||||
济南: [117, 36.65], |
||||
德阳: [104.37, 31.13], |
||||
温州: [120.65, 28.01], |
||||
九江: [115.97, 29.71], |
||||
邯郸: [114.47, 36.6], |
||||
临安: [119.72, 30.23], |
||||
兰州: [103.73, 36.03], |
||||
沧州: [116.83, 38.33], |
||||
临沂: [118.35, 35.05], |
||||
南充: [106.110698, 30.837793], |
||||
天津: [117.2, 39.13], |
||||
富阳: [119.95, 30.07], |
||||
泰安: [117.13, 36.18], |
||||
诸暨: [120.23, 29.71], |
||||
郑州: [113.65, 34.76], |
||||
哈尔滨: [126.63, 45.75], |
||||
聊城: [115.97, 36.45], |
||||
芜湖: [118.38, 31.33], |
||||
唐山: [118.02, 39.63], |
||||
平顶山: [113.29, 33.75], |
||||
邢台: [114.48, 37.05], |
||||
德州: [116.29, 37.45], |
||||
济宁: [116.59, 35.38], |
||||
荆州: [112.239741, 30.335165], |
||||
宜昌: [111.3, 30.7], |
||||
义乌: [120.06, 29.32], |
||||
丽水: [119.92, 28.45], |
||||
洛阳: [112.44, 34.7], |
||||
秦皇岛: [119.57, 39.95], |
||||
株洲: [113.16, 27.83], |
||||
石家庄: [114.48, 38.03], |
||||
莱芜: [117.67, 36.19], |
||||
常德: [111.69, 29.05], |
||||
保定: [115.48, 38.85], |
||||
湘潭: [112.91, 27.87], |
||||
金华: [119.64, 29.12], |
||||
岳阳: [113.09, 29.37], |
||||
长沙: [113, 28.21], |
||||
衢州: [118.88, 28.97], |
||||
廊坊: [116.7, 39.53], |
||||
菏泽: [115.480656, 35.23375], |
||||
合肥: [117.27, 31.86], |
||||
武汉: [114.31, 30.52], |
||||
大庆: [125.03, 46.58] |
||||
}; |
||||
|
||||
function convertData(data) { |
||||
const res = []; |
||||
for (let i = 0; i < data.length; i++) { |
||||
const geoCoord = geoCoordMap[data[i].name]; |
||||
if (geoCoord) { |
||||
res.push({ |
||||
name: data[i].name, |
||||
value: geoCoord.concat(data[i].value) |
||||
}); |
||||
} |
||||
} |
||||
return res; |
||||
} |
||||
|
||||
export default function getData() { |
||||
return { |
||||
textStyle: { |
||||
fontFamily: 'Inter, "Helvetica Neue", Arial, sans-serif', |
||||
fontWeight: 300 |
||||
}, |
||||
backgroundColor: "#404a59", |
||||
title: { |
||||
text: "Air quality of major cities in China", |
||||
subtext: "data from PM25.in", |
||||
sublink: "http://www.pm25.in", |
||||
top: "5%", |
||||
left: "center", |
||||
textStyle: { |
||||
color: "#fff" |
||||
} |
||||
}, |
||||
tooltip: { |
||||
trigger: "item" |
||||
}, |
||||
legend: { |
||||
orient: "vertical", |
||||
right: "5%", |
||||
bottom: "5%", |
||||
data: ["PM2.5"], |
||||
textStyle: { |
||||
color: "#fff" |
||||
} |
||||
}, |
||||
geo: { |
||||
map: "china", |
||||
emphasis: { |
||||
label: { |
||||
show: false |
||||
}, |
||||
itemStyle: { |
||||
areaColor: "#2a333d" |
||||
} |
||||
}, |
||||
itemStyle: { |
||||
areaColor: "#323c48", |
||||
borderColor: "#111" |
||||
}, |
||||
top: "20%", |
||||
bottom: "7%" |
||||
}, |
||||
series: [ |
||||
{ |
||||
name: "PM2.5", |
||||
type: "scatter", |
||||
coordinateSystem: "geo", |
||||
data: convertData(data), |
||||
symbolSize: val => val[2] / 10, |
||||
tooltip: { |
||||
formatter: function (val) { |
||||
return val.name + ": " + val.value[2]; |
||||
} |
||||
}, |
||||
itemStyle: { |
||||
color: "#ddb926" |
||||
} |
||||
}, |
||||
{ |
||||
name: "Top 5", |
||||
type: "effectScatter", |
||||
coordinateSystem: "geo", |
||||
data: convertData(data.sort((a, b) => b.value - a.value).slice(0, 6)), |
||||
symbolSize: val => val[2] / 10, |
||||
showEffectOn: "render", |
||||
rippleEffect: { |
||||
brushType: "stroke" |
||||
}, |
||||
emphasis: { |
||||
scale: true |
||||
}, |
||||
tooltip: { |
||||
formatter: function (val) { |
||||
return val.name + ": " + val.value[2]; |
||||
} |
||||
}, |
||||
label: { |
||||
formatter: "{b}", |
||||
position: "right", |
||||
show: true |
||||
}, |
||||
itemStyle: { |
||||
color: "#f4e925", |
||||
shadowBlur: 10, |
||||
shadowColor: "#333" |
||||
}, |
||||
zlevel: 1 |
||||
} |
||||
] |
||||
}; |
||||
} |
||||
|
After Width: | Height: | Size: 7.3 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
@ -0,0 +1,66 @@
|
||||
<template> |
||||
<div v-if="timeVal > 0" class="success"> |
||||
<span class="mr-4">剩余</span> |
||||
<span class="text">{{ |
||||
formatTimeText(timeVal) |
||||
}}</span> |
||||
</div> |
||||
<div v-if="timeVal < 0" class="error"> |
||||
<span class="mr-4">超时</span> |
||||
<span class="text">{{ |
||||
formatTimeText(-timeVal) |
||||
}}</span> |
||||
</div> |
||||
</template> |
||||
<script setup> |
||||
import { formatTimeText } from "@/utils/util"; |
||||
|
||||
const props = defineProps({ |
||||
time: { |
||||
type: Number, |
||||
default: 0 |
||||
} |
||||
}) |
||||
|
||||
const timeVal = ref(props.time) |
||||
|
||||
watch(() => props.time, (val) => { |
||||
timeVal.value = val; |
||||
startTimeInterval() |
||||
}) |
||||
|
||||
let timer = 0; |
||||
startTimeInterval() |
||||
function startTimeInterval() { |
||||
if (timeVal.value < 3600 && timeVal.value > -3600) { |
||||
clearInterval(timer) |
||||
timer = setInterval(() => { |
||||
timeVal.value -= 1; |
||||
}, 1000); |
||||
} else { |
||||
clearInterval(timer) |
||||
} |
||||
} |
||||
</script> |
||||
<style lang="scss" scoped> |
||||
.success { |
||||
padding: 0 8px; |
||||
height: 24px; |
||||
line-height: 24px; |
||||
text-align: center; |
||||
|
||||
.text { |
||||
color: #128009; |
||||
} |
||||
} |
||||
|
||||
.error { |
||||
background-color: #ff0000; |
||||
color: #fff; |
||||
padding: 0 8px; |
||||
height: 24px; |
||||
line-height: 24px; |
||||
border-radius: 20px; |
||||
text-align: center; |
||||
} |
||||
</style> |
||||
@ -0,0 +1,52 @@
|
||||
<template> |
||||
<div class="datav-card"> |
||||
<div class="datav-card_content"> |
||||
<header class="flex between"> |
||||
<div class="datav-card_title">{{ title }}</div> |
||||
<div> |
||||
<slot name="header-append"></slot> |
||||
</div> |
||||
</header> |
||||
<slot></slot> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
<script setup> |
||||
defineProps({ |
||||
title: { |
||||
type: String, |
||||
}, |
||||
}); |
||||
</script> |
||||
<style lang="scss" scoped> |
||||
.datav-card { |
||||
padding: 12px; |
||||
margin-top: 20px; |
||||
position: relative; |
||||
z-index: 1; |
||||
&::before { |
||||
display: block; |
||||
content: ""; |
||||
position: absolute; |
||||
top: 0; |
||||
left: 0; |
||||
right: 0; |
||||
bottom: 0; |
||||
background: linear-gradient( |
||||
180deg, |
||||
rgba(8, 69, 180, 0.86) 0%, |
||||
rgba(13, 51, 185, 0.42) 100% |
||||
); |
||||
opacity: 0.2; |
||||
} |
||||
.datav-card_content { |
||||
position: relative; |
||||
} |
||||
.datav-card_title { |
||||
font-size: 23px; |
||||
color: #fff; |
||||
margin-top: 0; |
||||
margin-bottom: 8px; |
||||
} |
||||
} |
||||
</style> |
||||
@ -0,0 +1,101 @@
|
||||
<template> |
||||
<div class="flex between v-center mb-10"> |
||||
<span class="bar-title">{{ title }}</span> |
||||
<span class="bar-sub-title">{{ subTitle }}</span> |
||||
</div> |
||||
<div> |
||||
<div class="flex v-center bar-item between" v-for="item in data" :size="size"> |
||||
<span class="bar-item-name mr-10">{{ item.name }}</span> |
||||
<div class="bar-item_content mr-16"> |
||||
<div |
||||
class="bar-item_content-bar" |
||||
:style="{ |
||||
width: `${(item.value / max) * 100}%`, |
||||
background: getColor(item.value / max), |
||||
}" |
||||
></div> |
||||
</div> |
||||
<span class="mr-16">{{ item.value }}{{ item.unit }}</span> |
||||
<span class="bar-item_remark text-right" v-if="item.denominator" style="min-width: 40px"> |
||||
<span class="text-success">{{ item.numerator }}</span> |
||||
<span>/</span> |
||||
<span>{{ item.denominator }}</span> |
||||
</span> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
<script setup> |
||||
defineProps({ |
||||
title: { |
||||
type: String, |
||||
default: "", |
||||
}, |
||||
subTitle: { |
||||
type: String, |
||||
default: "", |
||||
}, |
||||
data: { |
||||
type: Array, |
||||
default: [], |
||||
}, |
||||
max: { |
||||
type: Number, |
||||
default: 100, |
||||
}, |
||||
size: { |
||||
type: String, |
||||
default: "", |
||||
}, |
||||
}); |
||||
|
||||
function getColor(val) { |
||||
if (val > 0.8) { |
||||
return "linear-gradient(270deg, #63e700 0%, #19674c 100%)"; |
||||
} |
||||
if (val >= 0.7) { |
||||
return "linear-gradient( 270deg, #FFB90E 0%, #71501D 100%)"; |
||||
} |
||||
return "linear-gradient( 270deg, #FB002D 0%, #822232 100%)"; |
||||
} |
||||
</script> |
||||
<style lang="scss" scoped> |
||||
.bar-title { |
||||
font-size: 19px; |
||||
} |
||||
.bar-sub-title { |
||||
color: #597ae9; |
||||
font-size: 14px; |
||||
} |
||||
.bar-item { |
||||
font-size: 17px; |
||||
height: 32px; |
||||
&[size="large"] { |
||||
height: 46px; |
||||
.bar-item_content { |
||||
.bar-item_content-bar { |
||||
height: 13px; |
||||
} |
||||
} |
||||
} |
||||
.bar-item-name { |
||||
width: 19%; |
||||
text-align: right; |
||||
} |
||||
.bar-item_content { |
||||
width: 55%; |
||||
.bar-item_content-bar { |
||||
width: 0; |
||||
height: 9px; |
||||
background: linear-gradient(270deg, #63e700 0%, #19674c 100%); |
||||
box-shadow: 1px 0 0px 0px #020b5f; |
||||
transition: width .3s; |
||||
} |
||||
} |
||||
.bar-item_remark { |
||||
font-size: 14px; |
||||
.text-success { |
||||
color: #09c700; |
||||
} |
||||
} |
||||
} |
||||
</style> |
||||
@ -0,0 +1,103 @@
|
||||
<template> |
||||
<header> |
||||
<div class="flex flex v-center between relative"> |
||||
<nav class="flex"> |
||||
<router-link to="/datav/videoInsp" v-slot="{ isActive }"> |
||||
<span>视频督察</span> |
||||
<img :src="isActive ? Img2 : Img1" alt="" /> |
||||
</router-link> |
||||
<router-link to="/datav/sceneInsp" v-slot="{ isActive }"> |
||||
<span>现场督察</span> |
||||
<img :src="isActive ? Img2 : Img1" alt="" /> |
||||
</router-link> |
||||
<router-link to="/datav/caseVerif" v-slot="{ isActive }"> |
||||
<span>案件核查</span> |
||||
<img :src="isActive ? Img2 : Img1" alt="" /> |
||||
</router-link> |
||||
<router-link to="/datav/jwpy" v-slot="{ isActive }"> |
||||
<span>警务评议</span> |
||||
<img :src="isActive ? Img2 : Img1" alt="" /> |
||||
</router-link> |
||||
</nav> |
||||
<h1> |
||||
<router-link to="/datav/gobal"> |
||||
<span>长沙公安数字督察一体化平台</span> |
||||
</router-link> |
||||
</h1> |
||||
<nav class="flex right"> |
||||
<router-link to="/datav/mailVisits" v-slot="{ isActive }"> |
||||
<span>信访投诉</span> |
||||
<img :src="isActive ? Img2 : Img1" alt="" /> |
||||
</router-link> |
||||
<router-link to="/datav/rightsComfort" v-slot="{ isActive }"> |
||||
<span>维权抚慰</span> |
||||
<img :src="isActive ? Img2 : Img1" alt="" /> |
||||
</router-link> |
||||
<router-link to="/datav/auditSuper" v-slot="{ isActive }"> |
||||
<span>审计监督</span> |
||||
<img :src="isActive ? Img2 : Img1" alt="" /> |
||||
</router-link> |
||||
<router-link to="/datav/lmgz" v-slot="{ isActive }"> |
||||
<span>灵敏感知</span> |
||||
<img :src="isActive ? Img2 : Img1" alt="" /> |
||||
</router-link> |
||||
</nav> |
||||
</div> |
||||
</header> |
||||
</template> |
||||
<script setup> |
||||
import Img1 from '/imgs/datav/base.png' |
||||
import Img2 from '/imgs/datav/base_active.png' |
||||
</script> |
||||
<style lang="scss" scoped> |
||||
header { |
||||
color: #fff; |
||||
margin-bottom: 20px; |
||||
position: relative; |
||||
&::before { |
||||
display: block; |
||||
content: ""; |
||||
position: absolute; |
||||
width: 73.5%; |
||||
height: 213px; |
||||
top: 0; |
||||
left: 50%; |
||||
transform: translateX(-50%); |
||||
background-image: url("/imgs/datav/bg-1.png"); |
||||
z-index: 0; |
||||
} |
||||
h1 { |
||||
font-family: PangMenZhengDao; |
||||
font-size: 48px; |
||||
line-height: 1; |
||||
letter-spacing: 2px; |
||||
margin: 0; |
||||
} |
||||
nav { |
||||
a { |
||||
color: inherit; |
||||
text-decoration: none; |
||||
text-align: center; |
||||
font-size: 19px; |
||||
} |
||||
span { |
||||
display: block; |
||||
} |
||||
img { |
||||
display: block; |
||||
margin-top: -12px; |
||||
} |
||||
&.right { |
||||
img { |
||||
transform: scaleX(-1); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
:deep() { |
||||
a { |
||||
color: inherit; |
||||
text-decoration: none; |
||||
} |
||||
} |
||||
</style> |
||||
@ -0,0 +1,70 @@
|
||||
<template> |
||||
<div class="statistic"> |
||||
<div class="statistic-number"> |
||||
{{ parseInt(outputValue) }}{{ valueUnit }} |
||||
</div> |
||||
<div class="statistic-title">{{ title }}</div> |
||||
</div> |
||||
</template> |
||||
<script setup> |
||||
import { useTransition } from "@vueuse/core"; |
||||
|
||||
const props = defineProps({ |
||||
title: { |
||||
type: String |
||||
}, |
||||
value: { |
||||
type: Number, |
||||
defalut: 0, |
||||
}, |
||||
valueUnit: { |
||||
type: String, |
||||
}, |
||||
}); |
||||
const value = ref(0); |
||||
const outputValue = useTransition(value, { |
||||
duration: 1500, |
||||
}); |
||||
|
||||
value.value = props.value; |
||||
</script> |
||||
<style lang="scss" scoped> |
||||
.statistic { |
||||
text-align: center; |
||||
|
||||
.statistic-number { |
||||
font-size: 39px; |
||||
font-weight: 700; |
||||
height: 64px; |
||||
line-height: 64px; |
||||
background: linear-gradient( |
||||
270deg, |
||||
rgba(3, 11, 57, 0) 0%, |
||||
#00117d 49%, |
||||
rgba(3, 11, 57, 0) 100% |
||||
); |
||||
margin-bottom: 14px; |
||||
&::before, |
||||
&::after { |
||||
display: block; |
||||
content: ""; |
||||
height: 4px; |
||||
width: 85%; |
||||
margin: auto; |
||||
background: linear-gradient( |
||||
270deg, |
||||
rgba(1, 7, 94, 0) 0%, |
||||
#213ffb 49%, |
||||
rgba(1, 7, 93, 0) 100% |
||||
); |
||||
} |
||||
} |
||||
.statistic-title { |
||||
font-size: 27px; |
||||
} |
||||
|
||||
.statistic-footer { |
||||
font-size: 27px; |
||||
} |
||||
} |
||||
</style> |
||||
@ -0,0 +1,12 @@
|
||||
<template> |
||||
<el-tree-select :data="departs" :props="{label: 'shortName', value: 'id'}" node-key="id" :default-expanded-keys="['12630']" clearable filterable check-strictly /> |
||||
</template> |
||||
<script setup> |
||||
import useCatchStore from '@/stores/modules/catch' |
||||
|
||||
const departs = useCatchStore().getDeparts(); |
||||
|
||||
</script> |
||||
<style lang="scss" scoped> |
||||
|
||||
</style> |
||||
@ -0,0 +1,31 @@
|
||||
<template> |
||||
<el-form-item> |
||||
<template #label> |
||||
<div class="flex v-center gap-4"> |
||||
<el-tooltip |
||||
effect="dark" |
||||
:content="content" |
||||
raw-content |
||||
placement="top" |
||||
> |
||||
<icon name="local-icon-question" :size="18" /> |
||||
</el-tooltip> |
||||
<span>{{ label }}</span> |
||||
</div> |
||||
</template> |
||||
<slot></slot> |
||||
</el-form-item> |
||||
</template> |
||||
<script setup> |
||||
defineProps({ |
||||
label: { |
||||
type: String, |
||||
default: "", |
||||
}, |
||||
content: { |
||||
type: String, |
||||
}, |
||||
}); |
||||
</script> |
||||
<style lang="scss" scoped> |
||||
</style> |
||||
@ -0,0 +1,438 @@
|
||||
<template> |
||||
<div class="flex gap-12 wrap file-container"> |
||||
<div v-for="(item, index) in files" :key="index" class="item pointer"> |
||||
<template v-if="item.loading"> |
||||
<el-progress |
||||
type="circle" |
||||
:percentage="item.percent" |
||||
:width="80" |
||||
color="var(--primary-color)" |
||||
></el-progress> |
||||
</template> |
||||
<template v-else-if="getFileType(item.fileName) === FileType.IMG"> |
||||
<div |
||||
class="img-box" |
||||
:style="{ |
||||
backgroundImage: `url(${BASE_PATH}/file/stream/${item.filePath})`, |
||||
}" |
||||
@click="filePreview(item)" |
||||
></div> |
||||
<a |
||||
class="remove-btn" |
||||
@click="remove(index)" |
||||
v-if="removeEnable" |
||||
> |
||||
<icon name="el-icon-CircleCloseFilled" :size="20" /> |
||||
</a> |
||||
</template> |
||||
|
||||
<template v-else> |
||||
<div |
||||
class="item flex end v-center column text-center" |
||||
:title="item.fileName" |
||||
@click="filePreview(item)" |
||||
> |
||||
<icon :name="getIconName(item.fileName)" :size="40" /> |
||||
<span class="filename">{{ item.fileName }}</span> |
||||
<a |
||||
class="remove-btn" |
||||
@click.stop="remove(index)" |
||||
v-if="removeEnable" |
||||
> |
||||
<icon name="el-icon-CircleCloseFilled" :size="16" /> |
||||
</a> |
||||
</div> |
||||
</template> |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="file-preview-wrapper flex overlay" v-if="preview"> |
||||
<el-scrollbar height="100vh"> |
||||
<div class="file-list"> |
||||
<section |
||||
v-for="(item, index) in files" |
||||
:key="index" |
||||
class="flex gap v-center pointer" |
||||
:active="files.indexOf(activeFile) === index" |
||||
@click="filePreview(item)" |
||||
> |
||||
<icon :name="getIconName(item.fileName)" :size="24" /> |
||||
<span>{{ item.fileName }}</span> |
||||
</section> |
||||
</div> |
||||
</el-scrollbar> |
||||
<div |
||||
class="file-content flex center v-center" |
||||
@click="preview = false" |
||||
@wheel="wheel" |
||||
> |
||||
<div |
||||
class="img-container flex center" |
||||
v-if="getFileType(activeFile.fileName) === FileType.IMG" |
||||
> |
||||
<img |
||||
:src="`${BASE_PATH}/file/stream/${activeFile.filePath}`" |
||||
ref="imgRef" |
||||
@click.stop |
||||
:style="{ |
||||
transform: `rotate(${rotate}deg) scale(${scale}) translate(${translateX}px, ${translateY}px)`, |
||||
}" |
||||
@mousedown="mousedown" |
||||
@mousemove="mousemove" |
||||
@mouseup="mouseup" |
||||
draggable="false" |
||||
/> |
||||
<button |
||||
class="rotate-left-btn pointer" |
||||
@click.stop="rotateLeft" |
||||
size="small" |
||||
title="左旋转" |
||||
> |
||||
<icon name="local-icon-rotate-left" :size="28" /> |
||||
</button> |
||||
<button |
||||
class="rotate-right-btn pointer" |
||||
@click.stop="rotateRight" |
||||
size="small" |
||||
title="右旋转" |
||||
> |
||||
<icon name="local-icon-rotate-right" :size="28" /> |
||||
</button> |
||||
</div> |
||||
<template |
||||
v-else-if="getFileType(activeFile.fileName) === FileType.PDF" |
||||
> |
||||
<iframe |
||||
:src="`${BASE_PATH}/file/stream/${activeFile.filePath}`" |
||||
style="height: 100vh; width: 900px" |
||||
></iframe> |
||||
</template> |
||||
<template |
||||
v-else-if="getFileType(activeFile.fileName) === FileType.MP3" |
||||
> |
||||
<audio |
||||
controls |
||||
style="width: 50vw" |
||||
> |
||||
<source :src="`${BASE_PATH}/file/stream/${activeFile.filePath}`" type="audio/mp3"> |
||||
</audio> |
||||
</template> |
||||
<template |
||||
v-else-if="getFileType(activeFile.fileName) === FileType.MP4" |
||||
> |
||||
<video controls @click.stop style="max-height: 100vh"> |
||||
<source |
||||
:src="`${BASE_PATH}/file/stream/${activeFile.filePath}`" |
||||
type="video/mp4" |
||||
/> |
||||
</video> |
||||
</template> |
||||
<template |
||||
v-else-if="getFileType(activeFile.fileName) === FileType.WORD && activeFile.fileName.toLocaleLowerCase().endsWith('.docx')" |
||||
> |
||||
<vue-office-docx |
||||
:src="`${BASE_PATH}/file/stream/${activeFile.filePath}`" |
||||
style="height: 100vh; width: 900px" |
||||
@error="fileRrror = true" |
||||
v-if="!fileRrror" |
||||
@click.stop |
||||
/> |
||||
<div v-else class="error flex column text-center"> |
||||
<span style="padding: 20px" |
||||
>文件预览解析错误,如有需要请下载到本地预览</span |
||||
> |
||||
</div> |
||||
</template> |
||||
<template |
||||
v-else-if="getFileType(activeFile.fileName) === FileType.EXCEL && activeFile.fileName.toLocaleLowerCase().endsWith('.xlsx')" |
||||
> |
||||
<vue-office-excel |
||||
:src="`${BASE_PATH}/file/stream/${activeFile.filePath}`" |
||||
style="height: 100vh; width: 60vw" |
||||
@error="fileRrror = true" |
||||
v-if="!fileRrror" |
||||
@click.stop |
||||
/> |
||||
<div v-else class="error flex column text-center"> |
||||
<span style="padding: 20px" |
||||
>文件预览解析错误,如有需要请下载到本地预览</span |
||||
> |
||||
</div> |
||||
</template> |
||||
<template v-else> |
||||
<div style="background: #fff"> |
||||
<el-result |
||||
icon="error" |
||||
title="不支持预览" |
||||
sub-title="该文件格式暂不支持预览,请下载预览" |
||||
style="background: #fff; width: 400px" |
||||
@click.stop |
||||
></el-result> |
||||
</div> |
||||
</template> |
||||
<div class="file-number" @click.stop> |
||||
<span |
||||
>{{ files.indexOf(activeFile) + 1 }} / |
||||
{{ files.length }}</span |
||||
> |
||||
</div> |
||||
<button |
||||
class="left-btn pointer" |
||||
@click.stop.prevent="prev" |
||||
v-if="files.length > 1" |
||||
> |
||||
<icon name="el-icon-ArrowLeftBold" :size="28" /> |
||||
</button> |
||||
<button |
||||
class="right-btn pointer" |
||||
@click.stop.prevent="next" |
||||
v-if="files.length > 1" |
||||
> |
||||
<icon name="el-icon-ArrowRightBold" :size="28" /> |
||||
</button> |
||||
</div> |
||||
<div class="close-btn"></div> |
||||
<button class="close-btn pointer" @click="preview = false"> |
||||
<icon name="el-icon-Close" :size="28" /> |
||||
</button> |
||||
|
||||
<el-button |
||||
class="download-btn" |
||||
@click="download" |
||||
type="primary" |
||||
size="large" |
||||
> |
||||
<template #icon> |
||||
<icon name="el-icon-Download" :size="20" /> |
||||
</template> |
||||
下载文件 |
||||
</el-button> |
||||
</div> |
||||
</template> |
||||
|
||||
<script setup> |
||||
import { BASE_PATH } from "@/api/request"; |
||||
import { FileType } from "@/enums/fileEnums"; |
||||
import { getFileType, getIconName } from "@/utils/util"; |
||||
import "@vue-office/docx/lib/index.css"; |
||||
import "@vue-office/excel/lib/index.css"; |
||||
|
||||
import VueOfficeDocx from "@vue-office/docx"; |
||||
import VueOfficeExcel from "@vue-office/excel"; |
||||
|
||||
const props = defineProps({ |
||||
files: { |
||||
type: Array, |
||||
default: () => [], |
||||
}, |
||||
removeEnable: { |
||||
type: Boolean, |
||||
default: false, |
||||
}, |
||||
}); |
||||
const emit = defineEmits(["update:files"]); |
||||
|
||||
const preview = ref(false); |
||||
const activeFile = ref({}); |
||||
const rotate = ref(0); |
||||
const scale = ref(0); |
||||
const translateX = ref(0); |
||||
const translateY = ref(0); |
||||
let moveFlag = false; |
||||
const fileRrror = ref(false); |
||||
|
||||
function filePreview(file) { |
||||
preview.value = true; |
||||
activeFile.value = file; |
||||
rotate.value = 0; |
||||
scale.value = 1; |
||||
translateX.value = 0; |
||||
translateY.value = 0; |
||||
moveFlag = false; |
||||
} |
||||
|
||||
function remove(index) { |
||||
const files = [...props.files]; |
||||
files.splice(index, 1); |
||||
emit("update:files", files); |
||||
} |
||||
|
||||
function download() { |
||||
window.open(`${BASE_PATH}/file/stream/${activeFile.value.filePath}`); |
||||
} |
||||
|
||||
function prev() { |
||||
const index = props.files.indexOf(activeFile.value); |
||||
if (index === 0) { |
||||
filePreview(props.files[props.files.length - 1]); |
||||
} else { |
||||
filePreview(props.files[index - 1]); |
||||
} |
||||
} |
||||
function next() { |
||||
const index = props.files.indexOf(activeFile.value); |
||||
if (index === props.files.length - 1) { |
||||
filePreview(props.files[0]); |
||||
} else { |
||||
filePreview(props.files[index + 1]); |
||||
} |
||||
} |
||||
</script> |
||||
<style lang="scss" scoped> |
||||
.file-container { |
||||
min-height: 80px; |
||||
.item { |
||||
width: 80px; |
||||
height: 80px; |
||||
margin-bottom: 12px; |
||||
border-radius: 2px; |
||||
color: var(--primary-color); |
||||
position: relative; |
||||
&:hover { |
||||
background-color: #ededed; |
||||
span.filename { |
||||
font-weight: 700; |
||||
} |
||||
} |
||||
span.filename { |
||||
line-height: 1.2; |
||||
font-size: 12px; |
||||
width: 100%; |
||||
white-space: nowrap; |
||||
text-overflow: ellipsis; |
||||
margin-top: 10px; |
||||
overflow: hidden; |
||||
} |
||||
.img-box { |
||||
width: 80px; |
||||
height: 80px; |
||||
background-size: cover; |
||||
background-position: center; |
||||
border-radius: 2px; |
||||
|
||||
&:hover { |
||||
outline: 2px solid #ff9800; |
||||
} |
||||
} |
||||
.remove-btn { |
||||
position: absolute; |
||||
top: -8px; |
||||
right: -8px; |
||||
display: block; |
||||
border-radius: 50%; |
||||
height: 16px; |
||||
background-color: #fff; |
||||
color: #666; |
||||
&:hover { |
||||
color: red; |
||||
cursor: pointer; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
.file-preview-wrapper { |
||||
.file-list { |
||||
width: 15vw; |
||||
height: 100vh; |
||||
padding: 16px 8px; |
||||
background-color: #fff; |
||||
box-sizing: border-box; |
||||
section { |
||||
padding: 8px 16px; |
||||
border: 2px solid transparent; |
||||
background-color: #fff; |
||||
&:hover { |
||||
color: var(--primary-color); |
||||
font-weight: 700; |
||||
} |
||||
&[active="true"] { |
||||
border-color: var(--primary-color); |
||||
} |
||||
span { |
||||
width: calc(100% - 32px); |
||||
overflow: hidden; |
||||
white-space: nowrap; |
||||
text-overflow: ellipsis; |
||||
} |
||||
} |
||||
} |
||||
.file-content { |
||||
width: 86vw; |
||||
position: relative; |
||||
|
||||
.img-container { |
||||
height: 100vh; |
||||
img { |
||||
max-height: 100%; |
||||
display: block; |
||||
&:hover { |
||||
cursor: pointer; |
||||
} |
||||
} |
||||
} |
||||
.error { |
||||
background-color: #fff; |
||||
img { |
||||
width: 500px; |
||||
} |
||||
} |
||||
} |
||||
.close-btn { |
||||
position: absolute; |
||||
top: 12px; |
||||
right: 8px; |
||||
background-color: transparent; |
||||
border: none; |
||||
color: #fff; |
||||
&:hover { |
||||
color: red; |
||||
} |
||||
} |
||||
.rotate-left-btn { |
||||
position: absolute; |
||||
top: 12px; |
||||
right: 118px; |
||||
background-color: transparent; |
||||
border: none; |
||||
color: #fff; |
||||
} |
||||
.rotate-right-btn { |
||||
position: absolute; |
||||
top: 12px; |
||||
right: 68px; |
||||
background-color: transparent; |
||||
border: none; |
||||
color: #fff; |
||||
} |
||||
.left-btn { |
||||
position: absolute; |
||||
top: 50%; |
||||
left: 0; |
||||
transform: translateY(-50%); |
||||
background-color: transparent; |
||||
border: none; |
||||
color: #fff; |
||||
} |
||||
.right-btn { |
||||
position: absolute; |
||||
top: 50%; |
||||
right: 0; |
||||
transform: translateY(-50%); |
||||
background-color: transparent; |
||||
border: none; |
||||
color: #fff; |
||||
} |
||||
.download-btn { |
||||
position: absolute; |
||||
bottom: 20px; |
||||
right: 20px; |
||||
} |
||||
.file-number { |
||||
position: absolute; |
||||
top: 16px; |
||||
left: 18px; |
||||
color: #fff; |
||||
} |
||||
} |
||||
</style> |
||||
@ -0,0 +1,425 @@
|
||||
<template> |
||||
<el-button @click="show = true" |
||||
>上传 |
||||
<template #icon> |
||||
<icon name="el-icon-upload-filled" /> |
||||
</template> |
||||
</el-button> |
||||
|
||||
<el-dialog |
||||
title="上传佐证材料" |
||||
v-model="show" |
||||
width="60vw" |
||||
:close-on-click-modal="false" |
||||
> |
||||
<el-row :gutter="20" style="min-height: 60vh"> |
||||
<el-col :span="12"> |
||||
<el-upload |
||||
drag |
||||
multiple |
||||
:action="`${BASE_PATH}/file/upload`" |
||||
:headers="{ Authorization: getToken() }" |
||||
:before-upload="beforeUpload" |
||||
@progress="uploadProgress" |
||||
@success="handleSuccess" |
||||
@error="handleError" |
||||
:show-file-list="false" |
||||
> |
||||
<el-icon class="el-icon--upload"><upload-filled /></el-icon> |
||||
<div class="el-upload__text"> |
||||
将文件拖拽到此处或<em>点击上传</em> |
||||
</div> |
||||
<template #tip> |
||||
<div class="el-upload__tip">文件大小限制为 100MB</div> |
||||
</template> |
||||
</el-upload> |
||||
</el-col> |
||||
<el-col :span="12"> |
||||
<div class="flex gap v-center" style="height: 32px"> |
||||
<span style="font-size: 12px" v-if="activeFileIds.length" |
||||
>选中{{ activeFileIds.length }}个文件</span |
||||
> |
||||
<el-select |
||||
size="small" |
||||
style="width: 200px" |
||||
v-model="fileClassId" |
||||
clearable |
||||
> |
||||
<el-option |
||||
v-for="item in fileClasss" |
||||
:value="item.id" |
||||
:label="item.classTitle" |
||||
/> |
||||
</el-select> |
||||
<div v-if="activeFileIds.length"> |
||||
<el-button |
||||
type="primary" |
||||
size="small" |
||||
plain |
||||
@click="handleUpdateFileClass" |
||||
:disabled="!fileClassId" |
||||
>修改文件分类</el-button |
||||
> |
||||
<el-button |
||||
size="small" |
||||
@click=" |
||||
activeFileIds = []; |
||||
fileClassId = ''; |
||||
" |
||||
>取消选中</el-button |
||||
> |
||||
</div> |
||||
</div> |
||||
<el-scrollbar max-height="500px"> |
||||
<div style="margin: 2px"> |
||||
<p> |
||||
<span class="text-danger">未分类</span> |
||||
<span |
||||
style="font-size: 12px; color: #999" |
||||
class="ml-10" |
||||
>可单击选择下面文件进行分类</span |
||||
> |
||||
</p> |
||||
<div class="flex gap"> |
||||
<template |
||||
v-for="(item, index) in fileList.filter( |
||||
(item) => !item.fileClassId |
||||
)" |
||||
:key="index" |
||||
> |
||||
<template v-if="item.loading"> |
||||
<el-progress |
||||
type="circle" |
||||
:percentage="item.percent" |
||||
:width="80" |
||||
color="var(--primary-color)" |
||||
></el-progress> |
||||
</template> |
||||
<div |
||||
v-else |
||||
class="pointer" |
||||
@click=" |
||||
() => { |
||||
if ( |
||||
activeFileIds.includes(item.uid) |
||||
) { |
||||
activeFileIds.splice( |
||||
activeFileIds.indexOf( |
||||
item.uid |
||||
), |
||||
1 |
||||
); |
||||
} else { |
||||
activeFileIds.push(item.uid); |
||||
} |
||||
} |
||||
" |
||||
> |
||||
<div |
||||
class="item flex end v-center column text-center" |
||||
:title="item.fileName" |
||||
:active=" |
||||
activeFileIds.includes(item.uid) |
||||
" |
||||
> |
||||
<icon |
||||
:name="getIconName(item.fileName)" |
||||
:size="40" |
||||
/> |
||||
<span class="filename">{{ |
||||
item.fileName |
||||
}}</span> |
||||
<a |
||||
class="remove-btn" |
||||
@click.stop="remove(index)" |
||||
> |
||||
<icon |
||||
name="el-icon-CircleCloseFilled" |
||||
:size="16" |
||||
/> |
||||
</a> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
</div> |
||||
<div v-for="item in fileClasss" class="file-class-item"> |
||||
<header class="text-primary"> |
||||
{{ item.classTitle }} |
||||
</header> |
||||
<div class="flex gap file-class-body"> |
||||
<template |
||||
v-for="(item, index) in fileList.filter( |
||||
(file) => file.fileClassId === item.id |
||||
)" |
||||
:key="index" |
||||
> |
||||
<template v-if="item.loading"> |
||||
<el-progress |
||||
type="circle" |
||||
:percentage="item.percent" |
||||
:width="80" |
||||
color="var(--primary-color)" |
||||
></el-progress> |
||||
</template> |
||||
<div |
||||
v-else |
||||
class="pointer" |
||||
@click=" |
||||
() => { |
||||
if ( |
||||
activeFileIds.includes( |
||||
item.uid |
||||
) |
||||
) { |
||||
activeFileIds.splice( |
||||
activeFileIds.indexOf( |
||||
item.uid |
||||
), |
||||
1 |
||||
); |
||||
} else { |
||||
activeFileIds.push( |
||||
item.uid |
||||
); |
||||
} |
||||
} |
||||
" |
||||
> |
||||
<div |
||||
class="item flex end v-center column text-center" |
||||
:title="item.fileName" |
||||
:active=" |
||||
activeFileIds.includes(item.uid) |
||||
" |
||||
> |
||||
<icon |
||||
:name=" |
||||
getIconName(item.fileName) |
||||
" |
||||
:size="40" |
||||
/> |
||||
<span class="filename">{{ |
||||
item.fileName |
||||
}}</span> |
||||
<a |
||||
class="remove-btn" |
||||
@click.stop="remove(index)" |
||||
> |
||||
<icon |
||||
name="el-icon-CircleCloseFilled" |
||||
:size="16" |
||||
/> |
||||
</a> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</el-scrollbar> |
||||
</el-col> |
||||
</el-row> |
||||
<footer class="flex end mt-20"> |
||||
<el-button type="primary" size="large" @click="handleSubmit" |
||||
>确定</el-button |
||||
> |
||||
</footer> |
||||
</el-dialog> |
||||
</template> |
||||
<script setup> |
||||
import { getToken } from "@/utils/token"; |
||||
import { BASE_PATH } from "@/api/request"; |
||||
import feedback from "@/utils/feedback"; |
||||
import { getIconName } from "@/utils/util"; |
||||
import { |
||||
ProblemSources |
||||
} from "@/enums/dictEnums"; |
||||
|
||||
const props = defineProps({ |
||||
files: { |
||||
type: Array, |
||||
default: () => [], |
||||
}, |
||||
problemSourcesCode: { |
||||
type: String, |
||||
default: "", |
||||
}, |
||||
}); |
||||
|
||||
const emit = defineEmits(["update:files"]); |
||||
|
||||
const show = ref(false); |
||||
const fileList = ref(props.files); |
||||
|
||||
const activeFileIds = ref([]); |
||||
const fileClassId = ref(""); |
||||
|
||||
const fileClasss = ref([]); |
||||
|
||||
if (props.problemSourcesCode === ProblemSources.JWDC) { |
||||
fileClasss.value = [ |
||||
{ |
||||
id: 1, |
||||
classTitle: "容错免责样本申请表", |
||||
classRemarks: "", |
||||
}, |
||||
{ |
||||
id: 2, |
||||
classTitle: |
||||
"针对群众不满意原因,提供无过错的音视频、微信或短信截图等证明资料", |
||||
classRemarks: "", |
||||
}, |
||||
{ |
||||
id: 3, |
||||
classTitle: "110、122接处警开始、结束及处置过程中的视频截图", |
||||
classRemarks: "", |
||||
}, |
||||
{ |
||||
id: 4, |
||||
classTitle: "自动回访不满意后所对的回访录音", |
||||
classRemarks: "", |
||||
}, |
||||
{ |
||||
id: 5, |
||||
classTitle: "单位/个人所做其他工作", |
||||
classRemarks: "", |
||||
}, |
||||
]; |
||||
} |
||||
|
||||
function beforeUpload(file) { |
||||
fileList.value.push({ |
||||
uid: file.uid, |
||||
percent: 0, |
||||
loading: true, |
||||
fileName: file.name, |
||||
}); |
||||
} |
||||
|
||||
function uploadProgress(progressEvent, file) { |
||||
const filterFiles = fileList.value.filter((item) => file.uid === item.uid); |
||||
if (filterFiles.length) { |
||||
filterFiles[0].percent = parseInt(progressEvent.percent); |
||||
} |
||||
} |
||||
|
||||
function handleSuccess(data, file) { |
||||
if (data.code !== 200) { |
||||
return; |
||||
} |
||||
const filterFiles = fileList.value.filter((item) => file.uid === item.uid); |
||||
if (filterFiles.length) { |
||||
filterFiles[0].fileName = data.data.fileName; |
||||
filterFiles[0].filePath = data.data.filePath; |
||||
filterFiles[0].loading = false; |
||||
} |
||||
} |
||||
|
||||
function handleError(e) { |
||||
console.log(e); |
||||
feedback.msgError("上传失败!"); |
||||
} |
||||
|
||||
function handleUpdateFileClass() { |
||||
fileList.value |
||||
.filter((item) => activeFileIds.value.includes(item.uid)) |
||||
.forEach((file) => { |
||||
file.fileClassId = fileClassId.value; |
||||
}); |
||||
activeFileIds.value = []; |
||||
fileClassId.value = ""; |
||||
} |
||||
|
||||
function handleSubmit() { |
||||
debugger; |
||||
emit( |
||||
"update:files", |
||||
fileList.value.filter((item) => item.filePath) |
||||
); |
||||
show.value = false; |
||||
} |
||||
|
||||
function remove(index) { |
||||
const files = [...props.files]; |
||||
files.splice(index, 1); |
||||
emit("update:files", files); |
||||
} |
||||
</script> |
||||
<style lang="scss" scoped> |
||||
.el-upload { |
||||
--el-upload-dragger-padding-horizontal: 80px; |
||||
} |
||||
.item { |
||||
width: 80px; |
||||
height: 80px; |
||||
margin-bottom: 12px; |
||||
border-radius: 2px; |
||||
color: var(--primary-color); |
||||
position: relative; |
||||
border: 1px solid transparent; |
||||
border-radius: 10px; |
||||
&:hover { |
||||
background-color: #ededed; |
||||
border-color: var(--primary-color); |
||||
span.filename { |
||||
font-weight: 700; |
||||
} |
||||
} |
||||
&[active="true"] { |
||||
background-color: #ededed; |
||||
border-color: var(--primary-color); |
||||
} |
||||
span.filename { |
||||
line-height: 1.2; |
||||
font-size: 12px; |
||||
width: 100%; |
||||
white-space: nowrap; |
||||
text-overflow: ellipsis; |
||||
margin-top: 10px; |
||||
overflow: hidden; |
||||
} |
||||
.img-box { |
||||
width: 80px; |
||||
height: 80px; |
||||
background-size: cover; |
||||
background-position: center; |
||||
border-radius: 2px; |
||||
|
||||
&:hover { |
||||
outline: 2px solid #ff9800; |
||||
} |
||||
} |
||||
.remove-btn { |
||||
position: absolute; |
||||
top: -8px; |
||||
right: -8px; |
||||
display: block; |
||||
border-radius: 50%; |
||||
height: 16px; |
||||
background-color: #fff; |
||||
color: #666; |
||||
&:hover { |
||||
color: red; |
||||
cursor: pointer; |
||||
} |
||||
} |
||||
} |
||||
|
||||
.file-class-item { |
||||
box-shadow: 0px 0px 12px rgba(0, 0, 0, 0.12); |
||||
margin-bottom: 10px; |
||||
header { |
||||
padding: 4px 8px; |
||||
border-bottom: 1px solid #e4e7ed; |
||||
} |
||||
.file-class-body { |
||||
min-height: 20px; |
||||
padding: 4px 8px; |
||||
} |
||||
} |
||||
:deep() { |
||||
.el-dialog .el-dialog__body { |
||||
background-color: #f5f5f5; |
||||
} |
||||
} |
||||
</style> |
||||
@ -0,0 +1,101 @@
|
||||
<template> |
||||
<div> |
||||
<el-upload |
||||
:action="`${BASE_PATH}/file/upload`" |
||||
:headers="{ Authorization: getToken() }" |
||||
multiple |
||||
:before-upload="beforeUpload" |
||||
@progress="uploadProgress" |
||||
@success="handleSuccess" |
||||
@error="handleError" |
||||
:show-file-list="false" |
||||
class="mb-16" |
||||
> |
||||
<el-button |
||||
>上传 |
||||
<template #icon> |
||||
<icon name="el-icon-upload-filled" /> |
||||
</template> |
||||
</el-button> |
||||
|
||||
<template #tip> |
||||
<div class="el-upload__tip"> |
||||
{{ tips }} |
||||
</div> |
||||
</template> |
||||
</el-upload> |
||||
<file-list v-model:files="files" :removeEnable="true" /> |
||||
</div> |
||||
</template> |
||||
<script setup> |
||||
import feedback from "@/utils/feedback"; |
||||
import { getToken } from "@/utils/token"; |
||||
import { BASE_PATH } from "@/api/request"; |
||||
|
||||
const props = defineProps({ |
||||
files: { |
||||
type: Array, |
||||
default: () => [], |
||||
}, |
||||
tips: { |
||||
type: String, |
||||
default: '' |
||||
} |
||||
}); |
||||
|
||||
const emit = defineEmits(["update:files"]); |
||||
|
||||
const files = ref(props.files); |
||||
|
||||
watch( |
||||
() => props.files, |
||||
(newValue) => { |
||||
if (newValue) { |
||||
files.value = newValue; |
||||
} else { |
||||
files.value = []; |
||||
} |
||||
} |
||||
); |
||||
|
||||
watch(files, () => { |
||||
emit("update:files", files.value); |
||||
}); |
||||
|
||||
function beforeUpload(file) { |
||||
console.log("uid", file.uid); |
||||
files.value.push({ |
||||
uid: file.uid, |
||||
percent: 0, |
||||
loading: true, |
||||
}); |
||||
} |
||||
|
||||
function uploadProgress(progressEvent, file) { |
||||
const filterFiles = files.value.filter((item) => file.uid === item.uid); |
||||
if (filterFiles.length) { |
||||
filterFiles[0].percent = parseInt(progressEvent.percent); |
||||
} |
||||
} |
||||
|
||||
function handleSuccess(data, file) { |
||||
const filterFiles = files.value.filter((item) => file.uid === item.uid); |
||||
if (data.code !== 200) { |
||||
feedback.msgError(data.message); |
||||
files.value.splice(files.value.indexOf(filterFiles[0]), 1); |
||||
return; |
||||
} |
||||
if (filterFiles.length) { |
||||
filterFiles[0].fileName = data.data.fileName; |
||||
filterFiles[0].filePath = data.data.filePath; |
||||
filterFiles[0].loading = false; |
||||
console.log("file", filterFiles[0]); |
||||
} |
||||
emit("update:files", files.value); |
||||
} |
||||
|
||||
function handleError(e, file) { |
||||
console.log(e, file); |
||||
feedback.msgError("上传失败!"); |
||||
} |
||||
</script> |
||||
@ -0,0 +1,73 @@
|
||||
<template> |
||||
<el-tabs v-model="activeName"> |
||||
<el-tab-pane name="todo"> |
||||
<template #label> |
||||
<el-badge :value="myTodoTotal" v-if="myTodoTotal"> |
||||
<span class="tab-nav-title">我的待办</span> |
||||
</el-badge> |
||||
<span class="tab-nav-title" v-else>我的待办</span> |
||||
</template> |
||||
<div v-loading="loading"> |
||||
<home-work-my-todo :data="todos" @refresh="getList" /> |
||||
</div> |
||||
</el-tab-pane> |
||||
<el-tab-pane name="second"> |
||||
<template #label> |
||||
<el-badge :value="todoToExpires.length" v-if="todoToExpires.length"> |
||||
<span class="tab-nav-title">即将到期</span> |
||||
</el-badge> |
||||
<span class="tab-nav-title" v-else>即将到期</span> |
||||
</template> |
||||
<div v-loading="loading"> |
||||
<home-work-my-todo :data="todoToExpires" @refresh="getList" /> |
||||
</div> |
||||
</el-tab-pane> |
||||
<el-tab-pane name="third"> |
||||
<template #label> |
||||
<span class="tab-nav-title">我的收藏</span> |
||||
</template> |
||||
<home-work-my-fav /> |
||||
</el-tab-pane> |
||||
</el-tabs> |
||||
</template> |
||||
<script setup> |
||||
import { listTodos } from "@/api/work"; |
||||
const activeName = "todo"; |
||||
const myTodoTotal = ref(0) |
||||
const todos = ref([]) |
||||
const todoToExpires = ref([]) |
||||
|
||||
const loading = ref(true) |
||||
function getList() { |
||||
loading.value = true |
||||
listTodos({ |
||||
current: 1, |
||||
size: 100, |
||||
}).then((data) => { |
||||
todos.value = data.records; |
||||
myTodoTotal.value = data.total |
||||
todoToExpires.value = data.records.filter(item => item.remainingDuration < 43200) |
||||
loading.value = false |
||||
}); |
||||
} |
||||
|
||||
onMounted(() => { |
||||
getList(); |
||||
}); |
||||
</script> |
||||
<style lang="scss" scoped> |
||||
.tab-nav-title { |
||||
font-size: 24px; |
||||
} |
||||
:deep() { |
||||
.tab-nav-title_second { |
||||
font-size: 18px; |
||||
} |
||||
.el-tabs__nav-wrap { |
||||
overflow: visible; |
||||
.el-tabs__nav-scroll { |
||||
overflow: visible; |
||||
} |
||||
} |
||||
} |
||||
</style> |
||||
@ -0,0 +1,140 @@
|
||||
<template> |
||||
<div class="table-container" v-loading="loading"> |
||||
<el-table :data="favs"> |
||||
<el-table-column type="expand"> |
||||
<template #default="{ row }"> |
||||
<div class="row mt-10"> |
||||
<div class="col col-6"> |
||||
<label>样本源头编号</label> |
||||
<span>{{ row.originId }}</span> |
||||
</div> |
||||
<div class="col col-6"> |
||||
<label>问题发现时间</label> |
||||
<span>{{ row.discoveryTime }}</span> |
||||
</div> |
||||
<div class="col col-6"> |
||||
<label>问题发生时间</label> |
||||
<span>{{ row.happenTime }}</span> |
||||
</div> |
||||
<div class="col col-6"> |
||||
<label>问题来源</label> |
||||
<span>{{ row.problemSources }}</span> |
||||
</div> |
||||
</div> |
||||
<div class="row mt-10"> |
||||
<div class="col col-6"> |
||||
<label>投诉反映人</label> |
||||
<span>{{ row.responderName }}</span> |
||||
</div> |
||||
<div class="col col-6"> |
||||
<label>联系电话</label> |
||||
<span>{{ row.contactPhone }}</span> |
||||
</div> |
||||
</div> |
||||
<div class="row mt-10"> |
||||
<div class="col col-6"> |
||||
<label>业务类别</label> |
||||
<span>{{ row.businessTypeName }}</span> |
||||
</div> |
||||
<div class="col col-6"> |
||||
<label>涉嫌问题</label> |
||||
<span>{{}}</span> |
||||
</div> |
||||
</div> |
||||
<div class="row mt-10"> |
||||
<div class="col col-6"> |
||||
<label>涉及警种</label> |
||||
<span>{{ row.policeTypeName }}</span> |
||||
</div> |
||||
<div class="col col-6"> |
||||
<label>涉及单位</label> |
||||
<span>{{ row.involveDepartName }}</span> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
</el-table-column> |
||||
<el-table-column |
||||
label="问题发生时间" |
||||
prop="happenTime" |
||||
width="164" |
||||
/> |
||||
<el-table-column |
||||
label="问题来源" |
||||
prop="problemSources" |
||||
width="84" |
||||
/> |
||||
<el-table-column |
||||
label="业务类别" |
||||
prop="businessTypeName" |
||||
width="98" |
||||
/> |
||||
<el-table-column label="涉嫌问题" width="100" /> |
||||
<el-table-column |
||||
label="问题内容" |
||||
prop="thingDesc" |
||||
show-overflow-tooltip |
||||
/> |
||||
<el-table-column label="办理状态" width="98"> |
||||
<template #default="{ row }"> |
||||
<el-tag>{{ |
||||
getDictLable( |
||||
dict.processingStatus, |
||||
row.processingStatus |
||||
) |
||||
}}</el-tag> |
||||
</template> |
||||
</el-table-column> |
||||
<el-table-column label="流程节点" prop="" width="98" /> |
||||
<el-table-column label="操作" width="98"> |
||||
<template #default="{ row }"> |
||||
<el-button type="primary" link @click="handleAction(row)" |
||||
>立即处理</el-button |
||||
> |
||||
</template> |
||||
</el-table-column> |
||||
</el-table> |
||||
</div> |
||||
|
||||
<negative-dialog |
||||
v-model="show" |
||||
:id="activeNegativeId" |
||||
@change="getList" |
||||
@close="show = false" |
||||
/> |
||||
</template> |
||||
<script setup> |
||||
import { listFav } from "@/api/work/fav"; |
||||
import { getDictLable } from "@/utils/util"; |
||||
import useCatchStore from "@/stores/modules/catch"; |
||||
|
||||
const dict = useCatchStore().getDicts(["processingStatus"]); |
||||
|
||||
const favs = ref([]); |
||||
|
||||
const loading = ref(true); |
||||
|
||||
function getList() { |
||||
loading.value = true; |
||||
listFav({ |
||||
current: 1, |
||||
size: 100, |
||||
}).then((data) => { |
||||
favs.value = data.records; |
||||
loading.value = false |
||||
}); |
||||
} |
||||
|
||||
onMounted(() => { |
||||
getList(); |
||||
}); |
||||
|
||||
const show = ref(false); |
||||
const activeNegativeId = ref(""); |
||||
|
||||
function handleAction(row) { |
||||
show.value = true; |
||||
activeNegativeId.value = row.negativeId; |
||||
} |
||||
</script> |
||||
<style lang="scss" scoped> |
||||
</style> |
||||
@ -0,0 +1,217 @@
|
||||
<template> |
||||
<div> |
||||
<div class="relative"> |
||||
<el-tabs v-model="myTodoActionTab"> |
||||
<el-tab-pane |
||||
v-for="item in tabs" |
||||
:key="item.name" |
||||
:name="item.name" |
||||
> |
||||
<template #label> |
||||
<el-badge :value="item.total"> |
||||
<span class="tab-nav-title_second">{{ |
||||
item.name |
||||
}}</span> |
||||
</el-badge> |
||||
</template> |
||||
</el-tab-pane> |
||||
</el-tabs> |
||||
<el-button |
||||
type="primary" |
||||
@click="addShow = true" |
||||
v-perms="['negative:add']" |
||||
plain |
||||
class="add-btn" |
||||
><template #icon> |
||||
<icon name="el-icon-Plus" /> </template |
||||
>问题录入</el-button |
||||
> |
||||
</div> |
||||
<div class="table-container"> |
||||
<el-table :data="todos"> |
||||
<el-table-column type="expand"> |
||||
<template #default="{ row }"> |
||||
<div class="row mt-10"> |
||||
<div class="col col-6"> |
||||
<label>样本源头编号</label> |
||||
<span>{{ row.originId }}</span> |
||||
</div> |
||||
<div class="col col-6"> |
||||
<label>问题发现时间</label> |
||||
<span>{{ row.discoveryTime }}</span> |
||||
</div> |
||||
<div class="col col-6"> |
||||
<label>问题发生时间</label> |
||||
<span>{{ row.happenTime }}</span> |
||||
</div> |
||||
<div class="col col-6"> |
||||
<label>问题来源</label> |
||||
<span>{{ row.problemSources }}</span> |
||||
</div> |
||||
</div> |
||||
<div class="row mt-10"> |
||||
<div class="col col-6"> |
||||
<label>投诉反映人</label> |
||||
<span>{{ row.responderName }}</span> |
||||
</div> |
||||
<div class="col col-6"> |
||||
<label>联系电话</label> |
||||
<span>{{ row.contactPhone }}</span> |
||||
</div> |
||||
</div> |
||||
<div class="row mt-10"> |
||||
<div class="col col-6"> |
||||
<label>业务类别</label> |
||||
<span>{{ row.businessTypeName }}</span> |
||||
</div> |
||||
<div class="col col-6"> |
||||
<label>涉嫌问题</label> |
||||
<span>{{}}</span> |
||||
</div> |
||||
</div> |
||||
<div class="row mt-10"> |
||||
<div class="col col-6"> |
||||
<label>涉及警种</label> |
||||
<span>{{ row.policeTypeName }}</span> |
||||
</div> |
||||
<div class="col col-6"> |
||||
<label>涉及单位</label> |
||||
<span>{{ row.involveDepartName }}</span> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
</el-table-column> |
||||
<el-table-column |
||||
label="问题发现时间" |
||||
prop="discoveryTime" |
||||
width="164" |
||||
/> |
||||
<el-table-column |
||||
label="问题来源" |
||||
prop="problemSources" |
||||
width="84" |
||||
/> |
||||
<el-table-column |
||||
label="业务类别" |
||||
prop="businessTypeName" |
||||
width="98" |
||||
/> |
||||
<el-table-column |
||||
label="问题内容" |
||||
prop="thingDesc" |
||||
show-overflow-tooltip |
||||
/> |
||||
<el-table-column label="办理状态" width="98"> |
||||
<template #default="{ row }"> |
||||
<el-tag>{{ |
||||
getDictLable( |
||||
dict.processingStatus, |
||||
row.processingStatus |
||||
) |
||||
}}</el-tag> |
||||
</template> |
||||
</el-table-column> |
||||
<el-table-column label="流程节点" prop="" width="98" /> |
||||
<el-table-column label="操作" width="98"> |
||||
<template #default="{ row }"> |
||||
<el-button |
||||
type="primary" |
||||
link |
||||
@click="handleAction(row)" |
||||
>立即处理</el-button |
||||
> |
||||
</template> |
||||
</el-table-column> |
||||
</el-table> |
||||
</div> |
||||
</div> |
||||
|
||||
<negative-dialog |
||||
v-model="show" |
||||
:id="activeNegativeId" |
||||
@change="emit('refresh')" |
||||
@close="show = false" |
||||
:disabled="false" |
||||
/> |
||||
|
||||
<negative-add |
||||
v-model="addShow" |
||||
@close="addShow = false" |
||||
/> |
||||
</template> |
||||
<script setup> |
||||
import { listTodos } from "@/api/work"; |
||||
import { getDictLable } from "@/utils/util"; |
||||
import useCatchStore from "@/stores/modules/catch"; |
||||
|
||||
const dict = useCatchStore().getDicts(["processingStatus"]); |
||||
|
||||
const props = defineProps({ |
||||
data: { |
||||
type: Array, |
||||
required: [], |
||||
}, |
||||
}); |
||||
|
||||
const emit = defineEmits(["refresh"]); |
||||
|
||||
const ALL_LABEL = "全部"; |
||||
const tabs = ref([ |
||||
{ |
||||
name: ALL_LABEL, |
||||
total: 0, |
||||
}, |
||||
]); |
||||
|
||||
const todos = ref([]); |
||||
watch( |
||||
() => props.data, |
||||
(val) => { |
||||
todos.value = val; |
||||
tabs.value = []; |
||||
tabs.value.push({ |
||||
name: "全部", |
||||
total: val.length, |
||||
}); |
||||
new Set(val.map((item) => item.problemSources)).forEach( |
||||
(problemSources) => { |
||||
tabs.value.push({ |
||||
name: problemSources, |
||||
total: val.filter( |
||||
(item) => item.problemSources === problemSources |
||||
).length, |
||||
}); |
||||
} |
||||
); |
||||
} |
||||
); |
||||
|
||||
const myTodoActionTab = ref(ALL_LABEL); |
||||
watch(myTodoActionTab, (val) => { |
||||
if (val === ALL_LABEL) { |
||||
todos.value = props.data; |
||||
} else { |
||||
todos.value = props.data.filter((item) => item.problemSources === val); |
||||
} |
||||
}); |
||||
|
||||
const show = ref(false); |
||||
const activeNegativeId = ref(""); |
||||
const activeWork = ref({}); |
||||
provide("work", activeWork); |
||||
|
||||
function handleAction(row) { |
||||
show.value = true; |
||||
activeNegativeId.value = row.negativeId; |
||||
activeWork.value = row; |
||||
} |
||||
|
||||
const addShow = ref(false); |
||||
</script> |
||||
<style lang="scss" scoped> |
||||
.add-btn { |
||||
position: absolute; |
||||
top: 0; |
||||
right: 0; |
||||
} |
||||
</style> |
||||
@ -0,0 +1,19 @@
|
||||
import * as ElementPlusIcons from '@element-plus/icons-vue' |
||||
//@ts-ignore
|
||||
import localIconsName from 'virtual:svg-icons-names' |
||||
|
||||
export const LOCAL_ICON_PREFIX = 'local-icon-' |
||||
export const EL_ICON_PREFIX = 'el-icon-' |
||||
|
||||
const elIconsName: string[] = [] |
||||
|
||||
for (const [, component] of Object.entries(ElementPlusIcons)) { |
||||
elIconsName.push(`${EL_ICON_PREFIX}${component.name}`) |
||||
} |
||||
|
||||
export function getElementPlusIconNames() { |
||||
return elIconsName |
||||
} |
||||
export function getLocalIconNames() { |
||||
return localIconsName |
||||
} |
||||
@ -0,0 +1,53 @@
|
||||
<script lang="ts"> |
||||
import { createVNode } from 'vue' |
||||
import { ElIcon } from 'element-plus' |
||||
import { EL_ICON_PREFIX, LOCAL_ICON_PREFIX } from './index' |
||||
import svgIcon from './svg-icon.vue' |
||||
export default defineComponent({ |
||||
name: 'Icon', |
||||
props: { |
||||
name: { |
||||
type: String, |
||||
required: true |
||||
}, |
||||
size: { |
||||
type: [String, Number], |
||||
default: '14px' |
||||
}, |
||||
color: { |
||||
type: String, |
||||
default: 'inherit' |
||||
} |
||||
}, |
||||
setup(props) { |
||||
if (props.name.indexOf(EL_ICON_PREFIX) === 0) { |
||||
// el-icon |
||||
return () => |
||||
createVNode( |
||||
ElIcon, |
||||
{ |
||||
size: props.size, |
||||
color: props.color |
||||
}, |
||||
() => [createVNode(resolveComponent(props.name.replace(EL_ICON_PREFIX, '')))] |
||||
) |
||||
} |
||||
if (props.name.indexOf(LOCAL_ICON_PREFIX) === 0) { |
||||
// 本地icon |
||||
return () => |
||||
h( |
||||
'i', |
||||
{ |
||||
class: ['local-icon'] |
||||
}, |
||||
createVNode(svgIcon, { ...props }) |
||||
) |
||||
} |
||||
} |
||||
}) |
||||
</script> |
||||
<style scoped> |
||||
.local-icon { |
||||
display: flex; |
||||
} |
||||
</style> |
||||
@ -0,0 +1,182 @@
|
||||
<template> |
||||
<div class="icon-select"> |
||||
<el-popover |
||||
trigger="contextmenu" |
||||
v-model:visible="state.popoverVisible" |
||||
:width="state.popoverWidth" |
||||
> |
||||
<div |
||||
@mouseover.stop="state.mouseoverSelect = true" |
||||
@mouseout.stop="state.mouseoverSelect = false" |
||||
> |
||||
<div> |
||||
<div class="flex between"> |
||||
<div class="mb-3">请选择图标</div> |
||||
<div> |
||||
<span |
||||
v-for="(item, index) in iconTabsMap" |
||||
:key="index" |
||||
class="cursor-pointer text-sm ml-2" |
||||
:class="{ |
||||
'text-primary': index == tabIndex |
||||
}" |
||||
@click="tabIndex = index" |
||||
> |
||||
{{ item.name }} |
||||
</span> |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="h-280"> |
||||
<el-scrollbar> |
||||
<div class="flex flex-wrap"> |
||||
<div v-for="item in iconNamesFliter" :key="item" class="m-1"> |
||||
<el-button @click="handleSelect(item)"> |
||||
<icon :name="item" :size="18" /> |
||||
</el-button> |
||||
</div> |
||||
</div> |
||||
</el-scrollbar> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
<template #reference> |
||||
<el-input |
||||
ref="inputRef" |
||||
v-model.trim="state.inputValue" |
||||
placeholder="搜索图标" |
||||
:autofocus="false" |
||||
:disabled="disabled" |
||||
@focus="handleFocus" |
||||
@blur="handleBlur" |
||||
clearable |
||||
> |
||||
<template #prepend> |
||||
<div class="flex items-center" v-if="modelValue"> |
||||
<el-tooltip class="flex-1 w-20" :content="modelValue" placement="top"> |
||||
<icon |
||||
class="mr-1" |
||||
:key="modelValue" |
||||
:name="modelValue" |
||||
:size="16" |
||||
/> |
||||
</el-tooltip> |
||||
</div> |
||||
|
||||
<template v-else>无</template> |
||||
</template> |
||||
<template #append> |
||||
<el-button> |
||||
<icon name="el-icon-Close" :size="18" @click="handleClear" /> |
||||
</el-button> |
||||
</template> |
||||
</el-input> |
||||
</template> |
||||
</el-popover> |
||||
</div> |
||||
</template> |
||||
|
||||
<script lang="ts" setup> |
||||
import { getElementPlusIconNames, getLocalIconNames } from './index' |
||||
interface Props { |
||||
modelValue: string |
||||
disabled?: boolean |
||||
} |
||||
withDefaults(defineProps<Props>(), { |
||||
modelValue: '', |
||||
disabled: false |
||||
}) |
||||
|
||||
const emits = defineEmits<{ |
||||
(e: 'update:modelValue', value: string): void |
||||
(e: 'change', value: string): void |
||||
}>() |
||||
|
||||
const tabIndex = ref(0) |
||||
const iconTabsMap = [ |
||||
{ |
||||
name: 'element图标', |
||||
icons: getElementPlusIconNames() |
||||
}, |
||||
{ |
||||
name: '本地图标', |
||||
icons: getLocalIconNames() |
||||
} |
||||
] |
||||
|
||||
const inputRef = shallowRef<InstanceType<typeof ElInput>>() |
||||
|
||||
const state = reactive({ |
||||
inputValue: '', |
||||
popoverVisible: false, |
||||
popoverWidth: 0, |
||||
mouseoverSelect: false, |
||||
inputFocus: false |
||||
}) |
||||
|
||||
// input 框聚焦 |
||||
const handleFocus = () => { |
||||
state.inputFocus = state.popoverVisible = true |
||||
} |
||||
|
||||
// input 框失去焦点 |
||||
const handleBlur = () => { |
||||
state.inputFocus = false |
||||
state.popoverVisible = state.mouseoverSelect |
||||
} |
||||
|
||||
// 选中图标 |
||||
const handleSelect = (icon: string) => { |
||||
state.mouseoverSelect = state.popoverVisible = false |
||||
emits('update:modelValue', icon) |
||||
emits('change', icon) |
||||
} |
||||
//取消选中 |
||||
const handleClear = () => { |
||||
emits('update:modelValue', '') |
||||
emits('change', '') |
||||
} |
||||
|
||||
//根据输入框内容塞选 |
||||
const iconNamesFliter = computed(() => { |
||||
const iconNames = iconTabsMap[tabIndex.value]?.icons ?? [] |
||||
if (!state.inputValue) { |
||||
return iconNames |
||||
} |
||||
const inputValue = state.inputValue.toLowerCase() |
||||
return iconNames.filter((icon: string) => { |
||||
if (icon.toLowerCase().indexOf(inputValue) !== -1) { |
||||
return icon |
||||
} |
||||
}) |
||||
}) |
||||
|
||||
// 获取 input 的宽度 |
||||
const getInputWidth = () => { |
||||
nextTick(() => { |
||||
const inputWidth = inputRef.value?.$el.offsetWidth |
||||
state.popoverWidth = inputWidth < 300 ? 300 : inputWidth |
||||
}) |
||||
} |
||||
|
||||
//监听body点击事件 |
||||
// useEventListener(document.body, 'click', () => { |
||||
// state.popoverVisible = state.inputFocus || state.mouseoverSelect ? true : false |
||||
// }) |
||||
|
||||
watch( |
||||
() => state.popoverVisible, |
||||
async (value) => { |
||||
await nextTick() |
||||
if (value) { |
||||
inputRef.value?.focus() |
||||
} else { |
||||
inputRef.value?.blur() |
||||
} |
||||
} |
||||
) |
||||
|
||||
onMounted(() => { |
||||
getInputWidth() |
||||
}) |
||||
</script> |
||||
@ -0,0 +1,38 @@
|
||||
<template> |
||||
<svg aria-hidden="true" :style="styles"> |
||||
<use :xlink:href="symbolId" fill="currentColor" /> |
||||
</svg> |
||||
</template> |
||||
|
||||
<script lang="ts"> |
||||
import { addUnit } from '@/utils/util' |
||||
import type { CSSProperties } from 'vue' |
||||
|
||||
export default defineComponent({ |
||||
props: { |
||||
name: { |
||||
type: String, |
||||
required: true |
||||
}, |
||||
size: { |
||||
type: [Number, String], |
||||
default: 16 |
||||
}, |
||||
color: { |
||||
type: String, |
||||
default: 'inherit' |
||||
} |
||||
}, |
||||
setup(props) { |
||||
const symbolId = computed(() => `#${props.name}`) |
||||
const styles = computed<CSSProperties>(() => { |
||||
return { |
||||
width: addUnit(props.size), |
||||
height: addUnit(props.size), |
||||
color: props.color |
||||
} |
||||
}) |
||||
return { symbolId, styles } |
||||
} |
||||
}) |
||||
</script> |
||||
@ -0,0 +1,37 @@
|
||||
<template> |
||||
<div class="flex gap-20"> |
||||
<div v-for="item in icons" :key="item" class="icon-container" @click="handleClick(item)" :active="item === active"> |
||||
<icon :name="item" :size="84" /> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
<script setup> |
||||
|
||||
defineProps({ |
||||
modelValue: { |
||||
type: String, |
||||
} |
||||
}) |
||||
|
||||
const emit = defineEmits(['update:modelValue']) |
||||
|
||||
const icons = ['local-icon-ic_01', 'local-icon-ic_02', 'local-icon-ic_03', 'local-icon-ic_04', 'local-icon-ic_05', 'local-icon-ic_06'] |
||||
|
||||
const active = ref('') |
||||
|
||||
function handleClick(item) { |
||||
active.value = item |
||||
emit('update:modelValue', item) |
||||
} |
||||
</script> |
||||
<style lang="scss" scoped> |
||||
.icon-container { |
||||
border: 1px solid transparent; |
||||
cursor: pointer; |
||||
padding: 8px; |
||||
&:hover, &[active=true] { |
||||
background: #DCE4FF; |
||||
border-color: #4759D9; |
||||
} |
||||
} |
||||
</style> |
||||
@ -0,0 +1,107 @@
|
||||
<template> |
||||
<div class="flow"> |
||||
<header class="flex between v-center flow-header" ref="flowHeaderRef"> |
||||
<span> |
||||
<span class="second mr-8">总耗时</span> |
||||
<span style="color: var(--primary-color)">{{ |
||||
|
||||
}}</span> |
||||
</span> |
||||
</header> |
||||
<el-scrollbar height="300"> |
||||
<div class="flow-container"> |
||||
<div v-for="(item, index) in actionHistory" :key="item.id"> |
||||
<div class="mb-4 mt-4 flow-info"> |
||||
<span class="second mr-8">{{ item.crtTime }}</span> |
||||
<span class="mr-8">{{ item.departName }}</span> |
||||
<span class="mr-8">{{ item.crtName }}</span> |
||||
<span class="text-primary">{{ item.actionName }}</span> |
||||
</div> |
||||
<div |
||||
class="flow-time" |
||||
:danger="false" |
||||
> |
||||
<span class="second mr-8">用时</span> |
||||
<span class="primary">{{ |
||||
formatTimeText(getConsumingTime(item.crtTime, index)) |
||||
}}</span> |
||||
</div> |
||||
</div> |
||||
<div |
||||
class="text-center mt-20" |
||||
v-if="!actionHistory.length" |
||||
style="color: #999" |
||||
> |
||||
无 |
||||
</div> |
||||
</div> |
||||
</el-scrollbar> |
||||
</div> |
||||
</template> |
||||
<script setup> |
||||
import { formatTimeText } from "@/utils/util"; |
||||
import moment from 'moment' |
||||
|
||||
const actionHistory = inject('actionHistory') |
||||
const negative = inject('negative') |
||||
|
||||
function getConsumingTime(crtTime, index) { |
||||
if (index === 0) { |
||||
return moment(crtTime).diff(moment(negative.value.crtTime), "seconds") |
||||
} |
||||
return moment(crtTime).diff(moment(actionHistory.value[index - 1].crtTime), "seconds") |
||||
} |
||||
</script> |
||||
<style lang="scss" scoped> |
||||
.flow { |
||||
.flow-header { |
||||
background: #f5f6ff; |
||||
padding: 8px; |
||||
font-size: 12px; |
||||
} |
||||
.flow-container { |
||||
background-color: #eff0f5; |
||||
padding: 12px 8px; |
||||
min-height: 120px; |
||||
font-size: 12px; |
||||
.second { |
||||
color: #999; |
||||
} |
||||
.flow-time { |
||||
display: inline-flex; |
||||
padding: 6px; |
||||
margin-left: 6px; |
||||
background-color: #f5f6ff; |
||||
border-left: 2px solid #00d050; |
||||
padding-left: 20px; |
||||
&[danger="true"] { |
||||
border-color: #ff0000; |
||||
span { |
||||
color: #ff0000; |
||||
} |
||||
} |
||||
} |
||||
.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); |
||||
} |
||||
} |
||||
} |
||||
</style> |
||||
@ -0,0 +1,45 @@
|
||||
<template> |
||||
<el-dialog width="50vw" title="申请办结" v-model="show"> |
||||
<div style="height: 400px" class="mt-20"> |
||||
<p>请确认已按要求上传相关佐证材料,待</p> |
||||
<template v-if="negative.approvalFlow === ApprovalFlow.SECOND"> |
||||
<h3 >{{ negative.handleSecondDepartName || '二级机构' }}</h3> |
||||
<p style="margin-bottom: 40px">审核通过后,即可完成办理。</p> |
||||
</template> |
||||
<template v-else> |
||||
<h3>{{ negative.handleSecondDepartName || '二级机构' }} → 市局专班</h3> |
||||
<p style="margin-bottom: 40px">逐级审核通过后即可完成办理。</p> |
||||
</template> |
||||
|
||||
|
||||
</div> |
||||
|
||||
<footer class="flex end"> |
||||
<el-button type="primary" size="large" @click="submit" |
||||
>提交办结</el-button |
||||
> |
||||
</footer> |
||||
</el-dialog> |
||||
</template> |
||||
<script setup> |
||||
import { ApprovalFlow } from '@/enums/dictEnums' |
||||
const negative = inject("negative"); |
||||
const emit = defineEmits(["submit"]); |
||||
const show = ref(false); |
||||
|
||||
function submit() { |
||||
show.value = false; |
||||
emit("submit"); |
||||
} |
||||
|
||||
async function open() { |
||||
show.value = true; |
||||
} |
||||
|
||||
defineExpose({ |
||||
open |
||||
}); |
||||
</script> |
||||
<style lang="scss" scoped> |
||||
|
||||
</style> |
||||
@ -0,0 +1,94 @@
|
||||
<template> |
||||
<div |
||||
class="card-info mb-10" |
||||
v-if="extensionApply.id" |
||||
> |
||||
<header class="flex"> |
||||
<span class="primary">延期申请中</span> |
||||
<div class="step-item flex v-center"> |
||||
<div class="number" active="true">1</div> |
||||
<span>延期申请</span> |
||||
</div> |
||||
<div class="step-item flex v-center"> |
||||
<div class="number">2</div> |
||||
<span>二级机构专班审批</span> |
||||
</div> |
||||
<div class="step-item flex v-center"> |
||||
<div class="number" >3</div> |
||||
<span>市局专班审批</span> |
||||
</div> |
||||
</header> |
||||
<div class="flex"> |
||||
<div class="col col-6"> |
||||
<label>延期时长</label> |
||||
<span>{{ extensionApply.extensionDays }}天</span> |
||||
</div> |
||||
<div class="col col-18"> |
||||
<label>延期理由</label> |
||||
<span class="content" style="padding: 0">{{ extensionApply.comments }}</span> |
||||
</div> |
||||
</div> |
||||
<!-- <div class="flex mt-20"> |
||||
<div class="col" style="width: 100%"> |
||||
<label>驳回理由</label> |
||||
<span style="width: calc(100% - 74px)">{{ }}</span> |
||||
</div> |
||||
</div> --> |
||||
</div> |
||||
</template> |
||||
<script setup> |
||||
|
||||
const extensionApply = inject('extensionApply') |
||||
</script> |
||||
<style lang="scss" scoped> |
||||
.card-info { |
||||
background: #f4f5ff; |
||||
border: 1px solid rgba(195, 202, 245, 1); |
||||
padding: 12px 20px; |
||||
header { |
||||
margin-bottom: 20px; |
||||
.primary { |
||||
font-size: 16px; |
||||
color: var(--primary-color); |
||||
margin-left: 20px; |
||||
margin-right: 60px; |
||||
} |
||||
.step-item { |
||||
font-size: 12px; |
||||
padding-right: 52px; |
||||
margin-right: 8px; |
||||
position: relative; |
||||
&::before { |
||||
display: block; |
||||
content: ""; |
||||
width: 45px; |
||||
height: 1px; |
||||
background: #ccc; |
||||
position: absolute; |
||||
top: 50%; |
||||
right: 0; |
||||
} |
||||
&:last-child::before { |
||||
display: none; |
||||
} |
||||
.number { |
||||
width: 24px; |
||||
height: 24px; |
||||
border-radius: 50%; |
||||
text-align: center; |
||||
line-height: 24px; |
||||
margin-right: 6px; |
||||
border: 1px solid #ccc; |
||||
&[active="true"] { |
||||
background: var(--primary-color); |
||||
color: #fff; |
||||
} |
||||
&[return="true"] { |
||||
background: var(--danger-color); |
||||
} |
||||
|
||||
} |
||||
} |
||||
} |
||||
} |
||||
</style> |
||||
@ -0,0 +1,82 @@
|
||||
<template> |
||||
<el-dialog width="50vw" title="申请延期" v-model="show"> |
||||
<div> |
||||
<p> |
||||
全流程可延期的总时长不超过30日(中间包括节假日)。申请延期必需经过 |
||||
</p> |
||||
<h3>二级专班 → 市局</h3> |
||||
<p> |
||||
审批通过之后,才能正式生效。在申请延期过程中,三级机构仍然可以按照正常流程进行信件的处理,如果已提交信件办结申请,并通过市局专班的审批,则申请延期的流程自动结束。 |
||||
</p> |
||||
</div> |
||||
<el-form label-position="top" :model="form" ref="formRef" class="mb-40"> |
||||
<el-form-item |
||||
prop="extensionDays" |
||||
:rules="{ |
||||
required: true, |
||||
message: '请输入延期天数', |
||||
trigger: ['blur', 'change'], |
||||
}" |
||||
> |
||||
<template #label> |
||||
<h4 class="inline-block mb-10">延期时长</h4> |
||||
</template> |
||||
<div> |
||||
<el-input-number v-model="form.extensionDays" :max="negative.maxExtensionDuration" :min="1" /> |
||||
<span class="ml-8">天</span> |
||||
</div> |
||||
<div class="tips" style="width: 100%">最多延期{{ negative.maxExtensionDuration }}天</div> |
||||
</el-form-item> |
||||
<el-form-item |
||||
prop="comments" |
||||
:rules="{ |
||||
required: true, |
||||
message: '请输入延期理由', |
||||
trigger: ['blur', 'change'], |
||||
}" |
||||
> |
||||
<template #label> |
||||
<h4 class="inline-block mt-10 mb-10">延期理由</h4> |
||||
</template> |
||||
<el-input |
||||
type="textarea" |
||||
placeholder="请输入延期理由" |
||||
:autosize="{ minRows: 4 }" |
||||
v-model="form.comments" |
||||
/> |
||||
</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 emit = defineEmits(["submit"]); |
||||
const negative = inject("negative"); |
||||
|
||||
const show = ref(false); |
||||
const form = ref({}); |
||||
const formRef = ref(null); |
||||
async function submit() { |
||||
await formRef.value.validate(); |
||||
show.value = false; |
||||
emit("submit", form.value); |
||||
} |
||||
|
||||
async function open() { |
||||
show.value = true; |
||||
} |
||||
|
||||
defineExpose({ |
||||
open, |
||||
}); |
||||
</script> |
||||
<style lang="scss" scoped> |
||||
p { |
||||
line-height: 1.7; |
||||
} |
||||
</style> |
||||
@ -0,0 +1,125 @@
|
||||
<template> |
||||
<header>审批意见</header> |
||||
<div class="comments-container flex gap-20"> |
||||
<div |
||||
class="item" |
||||
v-for="(item, index) in approves" |
||||
:key="index" |
||||
:approved="item.approved" |
||||
> |
||||
<div class="flex center mb-20 relative comments-header"> |
||||
<icon |
||||
name="local-icon-return" |
||||
:size="41" |
||||
color="#FF0606" |
||||
v-if="item.state === ApproveState.REJECTED" |
||||
/> |
||||
<icon |
||||
name="el-icon-CircleCheck" |
||||
:size="41" |
||||
color="var(--primary-color)" |
||||
v-else-if="item.state === ApproveState.APPROVED" |
||||
/> |
||||
<div class="icon" v-else></div> |
||||
</div> |
||||
<h1 class="text-center mb-16"> |
||||
<span |
||||
v-if="item.state === ApproveState.REJECTED" |
||||
style="color: #ff0606" |
||||
>退回整改</span |
||||
> |
||||
<span v-else-if="item.state === ApproveState.APPROVED" style="color: var(--primary-color)">审批通过</span> |
||||
<span v-else>待审批</span> |
||||
</h1> |
||||
<h2 class="text-center mb-20"> |
||||
{{ item.handlerDepartName }} {{ item.handlerName }} |
||||
</h2> |
||||
<div |
||||
:danger="item.state === ApproveState.REJECTED" |
||||
style="padding: 8px" |
||||
v-if="item.state" |
||||
> |
||||
<h3> |
||||
{{ |
||||
item.state === ApproveState.REJECTED |
||||
? "退回整改意见" |
||||
: "审批意见" |
||||
}} |
||||
</h3> |
||||
<p style="font-weight: 700">{{ item.comments }}</p> |
||||
<h4 class="text-right">{{ item.updateTime }}</h4> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
<script setup> |
||||
import { ApproveState } from "@/enums/flowEnums"; |
||||
|
||||
const approves = inject("approves"); |
||||
console.log("approves", approves); |
||||
</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; |
||||
margin-top: 0; |
||||
} |
||||
h4 { |
||||
font-size: 12px; |
||||
color: #666; |
||||
font-weight: 500; |
||||
} |
||||
p { |
||||
color: #333; |
||||
} |
||||
div[danger="true"] { |
||||
background-color: #feeded; |
||||
} |
||||
.icon { |
||||
width: 22px; |
||||
height: 22px; |
||||
border: 3px solid #e0e0e0; |
||||
border-radius: 50%; |
||||
margin: 6.5px; |
||||
} |
||||
} |
||||
</style> |
||||
@ -0,0 +1,61 @@
|
||||
<template> |
||||
<el-dialog title="办结审批" v-model="show"> |
||||
<el-form |
||||
label-position="top" |
||||
ref="formRef" |
||||
:model="form" |
||||
style="height: 400px" |
||||
> |
||||
<el-form-item |
||||
prop="comments" |
||||
:rules="{ |
||||
required: true, |
||||
message: '请输入审批意见', |
||||
trigger: ['blur', 'change'], |
||||
}" |
||||
> |
||||
<template #label> |
||||
<h3 class="inline-block mb-10"> |
||||
审批意见 |
||||
</h3> |
||||
</template> |
||||
<el-input |
||||
type="textarea" |
||||
placeholder="请输入审批意见" |
||||
:autosize="{ minRows: 5 }" |
||||
v-model="form.comments" |
||||
/> |
||||
</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 emit = defineEmits(["submit"]); |
||||
|
||||
const negative = inject("negative"); |
||||
|
||||
const show = ref(false); |
||||
const form = ref({}); |
||||
const formRef = ref(null); |
||||
|
||||
async function submit() { |
||||
await formRef.value.validate(); |
||||
show.value = false; |
||||
emit("submit", form.value); |
||||
} |
||||
|
||||
async function open() { |
||||
show.value = true; |
||||
} |
||||
|
||||
defineExpose({ |
||||
open, |
||||
}); |
||||
</script> |
||||
<style lang="scss" scoped> |
||||
</style> |
||||
@ -0,0 +1,166 @@
|
||||
<template> |
||||
<el-progress |
||||
type="dashboard" |
||||
:percentage="percentage" |
||||
:stroke-width="16" |
||||
:width="236" |
||||
:color="colors" |
||||
class="timer" |
||||
v-if="time >= 0" |
||||
> |
||||
<div class="text-center"> |
||||
<div class="time-val"> |
||||
<template v-if="state.day !== 0"> |
||||
<span class="number">{{ state.day }}</span> |
||||
<span>天</span> |
||||
</template> |
||||
<template v-if="state.hour !== 0"> |
||||
<span class="number">{{ state.hour }}</span> |
||||
<span>时</span> |
||||
</template> |
||||
<template v-if="state.minute !== 0"> |
||||
<span class="number">{{ state.minute }}</span> |
||||
<span>分</span> |
||||
</template> |
||||
<template v-if="state.second !== 0"> |
||||
<span class="number">{{ state.second }}</span> |
||||
<span>秒</span> |
||||
</template> |
||||
</div> |
||||
<div class="tips">剩余处理时间</div> |
||||
</div> |
||||
</el-progress> |
||||
<div v-else class="countdown-container_danger flex center v-center column"> |
||||
<div class="time-val"> |
||||
<template v-if="state.day !== 0"> |
||||
<span class="number">{{ state.day }}</span> |
||||
<span>天</span> |
||||
</template> |
||||
<template v-if="state.hour !== 0"> |
||||
<span class="number">{{ state.hour }}</span> |
||||
<span>时</span> |
||||
</template> |
||||
<template v-if="state.minute !== 0"> |
||||
<span class="number">{{ state.minute }}</span> |
||||
<span>分</span> |
||||
</template> |
||||
<template v-if="state.second !== 0"> |
||||
<span class="number">{{ state.second }}</span> |
||||
<span>秒</span> |
||||
</template> |
||||
</div> |
||||
<span>已超时</span> |
||||
</div> |
||||
</template> |
||||
<script setup> |
||||
const colors = [ |
||||
{ color: "#F30000", percentage: 30 }, |
||||
{ color: "#E56D2B", percentage: 60 }, |
||||
{ color: "#2B45E5", percentage: 100 }, |
||||
]; |
||||
|
||||
const props = defineProps({ |
||||
time: { |
||||
type: Number, |
||||
default: 0, |
||||
}, |
||||
maxTime: { |
||||
type: Number, |
||||
default: 100, |
||||
}, |
||||
}); |
||||
const emit = defineEmits(["update:time"]); |
||||
|
||||
const state = reactive({ |
||||
day: 0, |
||||
hour: 0, |
||||
minute: 0, |
||||
second: 0, |
||||
}); |
||||
const percentage = computed(() => { |
||||
if (props.maxTime === 0) { |
||||
return 0; |
||||
} |
||||
const val = parseInt(props.time / props.maxTime * 100); |
||||
return val === 0 ? 1 : val; |
||||
}); |
||||
|
||||
watch( |
||||
() => props.time, |
||||
(val) => { |
||||
updateState(val > 0 ? val : -val); |
||||
} |
||||
); |
||||
|
||||
function updateState(val) { |
||||
state.day = Math.floor(val / (60 * 60 * 24)); |
||||
state.hour = Math.floor(val / (60 * 60)) % 24; |
||||
if (state.day === 0) { |
||||
state.minute = Math.floor(val / 60) % 60; |
||||
} else { |
||||
state.minute = 0; |
||||
} |
||||
if (state.day === 0 && state.hour === 0) { |
||||
state.second = Math.floor(val) % 60; |
||||
} else { |
||||
state.second = 0; |
||||
} |
||||
} |
||||
|
||||
let timer; |
||||
onMounted(() => { |
||||
console.log("onMounted"); |
||||
updateState(props.time); |
||||
if (props.time < 3600 && props.time > -3600) { |
||||
clearInterval(timer); |
||||
timer = setInterval(() => { |
||||
emit("update:time", props.time - 1); |
||||
}, 1000); |
||||
} |
||||
}); |
||||
|
||||
onUnmounted(() => { |
||||
console.log("onUnmounted"); |
||||
}); |
||||
</script> |
||||
<style lang="scss" scoped> |
||||
.time-val { |
||||
color: #999; |
||||
margin-top: 20px; |
||||
vertical-align: bottom; |
||||
margin-bottom: 18px; |
||||
span { |
||||
font-size: 18px; |
||||
line-height: 1; |
||||
} |
||||
.tips { |
||||
font-size: 14px; |
||||
color: #333; |
||||
} |
||||
.number { |
||||
font-size: 60px; |
||||
font-weight: 700; |
||||
color: var(--primary-color); |
||||
} |
||||
} |
||||
.tips { |
||||
font-size: 14px; |
||||
color: #333; |
||||
} |
||||
.countdown-container_danger { |
||||
background: linear-gradient(180deg, #ff7158 0%, #f40000 100%); |
||||
border: 4px solid #ffcdcd; |
||||
color: #fff; |
||||
border-radius: 11px; |
||||
box-sizing: border-box; |
||||
margin-bottom: 20px; |
||||
height: 190px; |
||||
width: 100%; |
||||
.time-val { |
||||
color: #fff; |
||||
.number { |
||||
color: #fff; |
||||
} |
||||
} |
||||
} |
||||
</style> |
||||
@ -0,0 +1,80 @@
|
||||
<template> |
||||
<div class="info-container"> |
||||
<h3>问题信息</h3> |
||||
<div class="row"> |
||||
<div class="col col-6"> |
||||
<label>样本源头编号</label> |
||||
<span>{{ negative.originId }}</span> |
||||
</div> |
||||
<div class="col col-6"> |
||||
<label>问题发现时间</label> |
||||
<span>{{ negative.discoveryTime }}</span> |
||||
</div> |
||||
<div class="col col-6"> |
||||
<label>问题发生时间</label> |
||||
<span>{{ negative.happenTime }}</span> |
||||
</div> |
||||
<div class="col col-6"> |
||||
<label>问题来源</label> |
||||
<span>{{ negative.problemSources }}</span> |
||||
</div> |
||||
</div> |
||||
<div class="row"> |
||||
<div class="col col-6"> |
||||
<label>投诉反映人</label> |
||||
<span>{{ negative.responderName }}</span> |
||||
</div> |
||||
<div class="col col-6"> |
||||
<label>联系电话</label> |
||||
<span>{{ negative.contactPhone }}</span> |
||||
</div> |
||||
</div> |
||||
<div class="row"> |
||||
<div class="col col-6"> |
||||
<label>业务类别</label> |
||||
<span>{{ negative.businessTypeName }}</span> |
||||
</div> |
||||
<div class="col col-6"> |
||||
<label>案件/警情编号</label> |
||||
<span>{{ negative.caseNumber || '/' }}</span> |
||||
</div> |
||||
<div class="col col-12"> |
||||
<label>涉嫌问题</label> |
||||
<span>{{ negative.involveProblemLables || '/' }}</span> |
||||
</div> |
||||
</div> |
||||
<div class="row"> |
||||
<div class="col col-6"> |
||||
<label>涉及警种</label> |
||||
<span>{{ negative.policeTypeName }}</span> |
||||
</div> |
||||
<div class="col col-18"> |
||||
<label>涉及单位</label> |
||||
<span>{{ negative.involveDepartName || '/' }}</span> |
||||
</div> |
||||
</div> |
||||
<div> |
||||
<div class="text-primary mt-10">事情简要描述</div> |
||||
<div class="content">{{ negative.thingDesc }}</div> |
||||
</div> |
||||
<div v-if="negative.thingFiles?.length"> |
||||
<div class="text-primary mt-10 mb-10">附件</div> |
||||
<file-list :files="negative.thingFiles" /> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
<script setup> |
||||
|
||||
const negative = inject('negative') |
||||
</script> |
||||
<style lang="scss" scoped> |
||||
.info-container { |
||||
background: #F9FAFF; |
||||
box-shadow: 0px 2px 4px 0px rgba(133,150,248,0.47); |
||||
padding: 20px; |
||||
margin: 0px 2px 4px 2px; |
||||
h3 { |
||||
margin-top: 0; |
||||
} |
||||
} |
||||
</style> |
||||
@ -0,0 +1,368 @@
|
||||
<template> |
||||
<el-dialog |
||||
:show-close="false" |
||||
width="84vw" |
||||
top="2vh" |
||||
class="dialog-header-nopadding" |
||||
style="--el-dialog-padding-primary: 10px; margin-bottom: 2vh" |
||||
:lock-scroll="false" |
||||
> |
||||
<template #header="{ close }"> |
||||
<header class="flex between v-center dialog-header"> |
||||
<div class="ml-16"> |
||||
<span class="mr-8 second">问题编号</span> |
||||
<span>{{ id }}</span> |
||||
</div> |
||||
<div class="flex step-box"> |
||||
<div |
||||
class="step flex center v-center" |
||||
v-for="(item, index) in dict.processingStatus" |
||||
:key="index" |
||||
:active="negative.processingStatus === item.dictValue" |
||||
:completed=" |
||||
index < |
||||
dict.processingStatus.indexOf( |
||||
negative.processingStatus |
||||
) |
||||
" |
||||
> |
||||
<span class="bloder">{{ index + 1 }}</span> |
||||
<span>{{ item.remark }}</span> |
||||
</div> |
||||
</div> |
||||
<div class="flex"> |
||||
<el-button |
||||
type="primary" |
||||
size="large" |
||||
:class="isFav ? 'fav-btn active' : 'fav-btn'" |
||||
@click="handleFav" |
||||
>{{ isFav ? "已收藏" : "收藏" }} |
||||
<template #icon> |
||||
<icon |
||||
:name=" |
||||
isFav |
||||
? 'el-icon-StarFilled' |
||||
: 'el-icon-star' |
||||
" |
||||
:size="22" |
||||
/> |
||||
</template> |
||||
</el-button> |
||||
<el-button |
||||
link |
||||
circle |
||||
size="large" |
||||
@click="close" |
||||
class="close-btn" |
||||
> |
||||
<template #icon> |
||||
<icon |
||||
name="el-icon-close" |
||||
:size="26" |
||||
:color="'#fff'" |
||||
/> |
||||
</template> |
||||
</el-button> |
||||
</div> |
||||
</header> |
||||
</template> |
||||
<main v-loading="loading"> |
||||
<el-row style="height: 100%"> |
||||
<el-col :span="5" style="height: 100%"> |
||||
<div ref="leftContainerRef" class="left-container h100"> |
||||
<template v-if="negative.flowKey !== FlowNodeEnum.FIRST_DISTRIBUTE && negative.processingStatus !== ProcessingStatus.COMPLETED"> |
||||
<negative-countdown v-model:time="remainingDuration" :max-time="maxDuration" /> |
||||
</template> |
||||
<negative-action-history /> |
||||
</div> |
||||
</el-col> |
||||
<el-col :span="19" style="height: 100%"> |
||||
<el-scrollbar max-height="100%" class="main-container"> |
||||
|
||||
<negative-sign-return-description /> |
||||
<negative-apply-extension-description /> |
||||
<negative-description /> |
||||
<template |
||||
v-if=" |
||||
components.indexOf( |
||||
'negative-verify-description' |
||||
) > -1 |
||||
" |
||||
> |
||||
<negative-verify-description /> |
||||
</template> |
||||
<template v-if="!disabled"> |
||||
<template |
||||
v-if=" |
||||
components.indexOf('negative-distribute') > |
||||
-1 |
||||
" |
||||
> |
||||
<negative-distribute ref="componentRef" /> |
||||
</template> |
||||
<template |
||||
v-if=" |
||||
components.indexOf('negative-verify') > -1 |
||||
" |
||||
> |
||||
<negative-verify |
||||
ref="componentRef" |
||||
@submit="handleSubmitExecute" |
||||
/> |
||||
</template> |
||||
</template> |
||||
<template v-if="approves.length"> |
||||
<negative-approve-description /> |
||||
</template> |
||||
</el-scrollbar> |
||||
</el-col> |
||||
</el-row> |
||||
</main> |
||||
<footer class="flex between v-center"> |
||||
<div> |
||||
<!-- <el-button type="primary" plain size="large">打印</el-button> --> |
||||
</div> |
||||
<div v-if="!disabled"> |
||||
<el-button |
||||
size="large" |
||||
v-for="item in flowActions" |
||||
:key="item.actionKey" |
||||
:type="item.buttonType" |
||||
:plain="item.plain" |
||||
@click="handleExecute(item)" |
||||
:disabled=" |
||||
loading || |
||||
(!negative.extensionApplyFlag && |
||||
(item.actionKey === |
||||
FlowActionEnum.APPLY_COMPLETION || |
||||
item.actionKey === |
||||
FlowActionEnum.APPLY_EXTENSION || |
||||
item.actionKey === |
||||
FlowActionEnum.THREE_SIGN_RETURN)) |
||||
" |
||||
>{{ item.buttonLabel }}</el-button |
||||
> |
||||
</div> |
||||
</footer> |
||||
<template v-for="item in flowActions" :key="item.actionKey"> |
||||
<template v-if="item.actionKey.includes('apply_completion')"> |
||||
<negative-apply-completion |
||||
:ref="(el) => setActionItemRef(item.actionKey, el)" |
||||
@submit="handleSubmitExecute" |
||||
/> |
||||
</template> |
||||
<template v-if="item.actionKey.includes('apply_extension')"> |
||||
<negative-apply-extension |
||||
:ref="(el) => setActionItemRef(item.actionKey, el)" |
||||
@submit="handleSubmitExecute" |
||||
/> |
||||
</template> |
||||
<template v-if="item.actionKey.includes('_approve')"> |
||||
<negative-approve |
||||
:ref="(el) => setActionItemRef(item.actionKey, el)" |
||||
@submit="handleSubmitExecute" |
||||
/> |
||||
</template> |
||||
<template v-if="item.actionKey.includes('_return')"> |
||||
<negative-return |
||||
:ref="(el) => setActionItemRef(item.actionKey, el)" |
||||
@submit="handleSubmitExecute" |
||||
/> |
||||
</template> |
||||
</template> |
||||
</el-dialog> |
||||
</template> |
||||
<script lang="ts" setup> |
||||
import { FlowActionEnum, FlowNodeEnum, ProcessingStatus } from "@/enums/flowEnums"; |
||||
import { getNegativeDetails, negativeExecute } from "@/api/work/negative"; |
||||
import { addFav, delFav } from "@/api/work/fav"; |
||||
import feedback from "@/utils/feedback"; |
||||
import { getComponents } from "@/utils/flow"; |
||||
import useCatchStore from "@/stores/modules/catch"; |
||||
|
||||
const dict = useCatchStore().getDicts(["processingStatus"]); |
||||
|
||||
const props = defineProps({ |
||||
id: { |
||||
type: String, |
||||
required: true, |
||||
}, |
||||
disabled: { |
||||
type: Boolean, |
||||
default: true, |
||||
}, |
||||
}); |
||||
|
||||
const emit = defineEmits(["close", "change"]); |
||||
|
||||
const work = inject("work", null); |
||||
|
||||
const loading = ref(false); |
||||
|
||||
const negative = ref({}); |
||||
const actionHistory = ref([]); |
||||
const signReturns = ref([]); |
||||
const approves = ref([]) |
||||
const extensionApply = ref({}); |
||||
provide("negative", negative); |
||||
provide("actionHistory", actionHistory); |
||||
provide("signReturns", signReturns); |
||||
provide("approves", approves); |
||||
provide("extensionApply", extensionApply); |
||||
|
||||
const isFav = ref(false); |
||||
const remainingDuration = ref(0) |
||||
const maxDuration = ref(0) |
||||
const flowActions = ref([]); |
||||
const components = ref([]); |
||||
watch( |
||||
() => props.id, |
||||
() => { |
||||
getDetails(); |
||||
} |
||||
); |
||||
|
||||
function getDetails() { |
||||
loading.value = true; |
||||
getNegativeDetails(props.id, work?.value.id).then((data) => { |
||||
negative.value = data.negative; |
||||
flowActions.value = data.flowActions; |
||||
actionHistory.value = data.actionHistory; |
||||
signReturns.value = data.signReturns; |
||||
approves.value = data.approves |
||||
extensionApply.value = data.extensionApply || {}; |
||||
isFav.value = data.isFav; |
||||
remainingDuration.value = data.remainingDuration; |
||||
console.log(remainingDuration.value) |
||||
maxDuration.value = data.maxDuration; |
||||
components.value = getComponents(data.flowNode?.flowKey); |
||||
loading.value = false; |
||||
}); |
||||
} |
||||
|
||||
const componentRef = ref([]); |
||||
const actionItemRefs = ref({}); |
||||
const setActionItemRef = (actionKey, el) => { |
||||
if (el) { |
||||
actionItemRefs.value[actionKey] = el; |
||||
} |
||||
}; |
||||
const activeAction = ref({}); |
||||
async function handleExecute(action, data) { |
||||
if (action.validateForm) { |
||||
try { |
||||
data = await componentRef.value.validate(); |
||||
} catch (e) { |
||||
feedback.msgWarning("请检查输入项"); |
||||
throw e; |
||||
} |
||||
} |
||||
if (action.openDialog) { |
||||
actionItemRefs.value[action.actionKey].open(); |
||||
activeAction.value = action; |
||||
return; |
||||
} |
||||
loading.value = true; |
||||
await negativeExecute(props.id, { |
||||
workId: work?.value.id, |
||||
actionKey: action.actionKey, |
||||
nextFlowKey: action.nextFlowKey, |
||||
actionName: action.actionName, |
||||
data, |
||||
}); |
||||
feedback.msgSuccess("操作成功"); |
||||
if (action.doClose) { |
||||
emit("change"); |
||||
emit("close"); |
||||
return; |
||||
} |
||||
getDetails(); |
||||
} |
||||
|
||||
async function handleSubmitExecute(data) { |
||||
activeAction.value.openDialog = false; |
||||
handleExecute(activeAction.value, data); |
||||
} |
||||
|
||||
function handleFav() { |
||||
if (isFav.value) { |
||||
delFav(props.id); |
||||
} else { |
||||
addFav(props.id); |
||||
} |
||||
isFav.value = !isFav.value; |
||||
} |
||||
</script> |
||||
<style lang="scss" scoped> |
||||
.dialog-header { |
||||
--dialog-header-font-color: #acb7ff; |
||||
--dialog-header-font-size: 16px; |
||||
background-color: var(--primary-color); |
||||
color: #fff; |
||||
padding: 1em; |
||||
font-size: var(--dialog-header-font-size); |
||||
.second { |
||||
color: var(--dialog-header-font-color); |
||||
} |
||||
.step-box { |
||||
.step { |
||||
--setp-background-color: #3a4dc1; |
||||
--setp-border-color: #4b60e4; |
||||
--setp-font-color: var(--dialog-header-font-color); |
||||
--setp-font-size: var(--dialog-header-font-size); |
||||
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; |
||||
} |
||||
} |
||||
} |
||||
.fav-btn { |
||||
--el-font-size-base: 18px; |
||||
--el-button-bg-color: #283aac; |
||||
--el-button-hover-bg-color: #c20921; |
||||
--el-button-hover-border-color: #fa8695; |
||||
&.active { |
||||
--el-button-bg-color: #c20921; |
||||
} |
||||
&:focus { |
||||
background-color: var(--el-button-bg-color); |
||||
} |
||||
} |
||||
.close-btn:hover { |
||||
:deep() { |
||||
.el-icon { |
||||
color: #c20921; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
main { |
||||
height: calc(96vh - 180px); |
||||
} |
||||
.left-container { |
||||
padding: 0 20px 0 40px; |
||||
} |
||||
.main-container { |
||||
padding: 0 20px; |
||||
} |
||||
footer { |
||||
padding: 10px 20px 0; |
||||
} |
||||
</style> |
||||
@ -0,0 +1,186 @@
|
||||
<template> |
||||
<el-form :label-width="140" :model="form" ref="formRef" :rules="rules"> |
||||
<h2>办理单位</h2> |
||||
<el-row> |
||||
<el-col :span="12"> |
||||
<el-form-item label="主办层级" prop="hostLevel"> |
||||
<el-select style="width: 400px" v-model="form.hostLevel"> |
||||
<el-option |
||||
v-for="item in dict.hostLevel" |
||||
:key="item.id" |
||||
:label="item.dictLabel" |
||||
:value="item.dictValue" |
||||
/> |
||||
</el-select> |
||||
<div class="tips mt-10"> |
||||
<p>如主办层级 为 市局主办, 则由市局直属支队办理;</p> |
||||
<p> |
||||
如主办层级 为 |
||||
二级机构主办时,则由涉及单位的二级机构办理; |
||||
</p> |
||||
<p>如主办层级 为 三级机构主办时, 则由涉及单位办理。</p> |
||||
</div> |
||||
</el-form-item> |
||||
</el-col> |
||||
<el-col :span="12"> |
||||
<el-form-item label="办理单位" prop="departId"> |
||||
<el-tree-select |
||||
:data="departs" |
||||
:props="{ label: 'shortName', value: 'id' }" |
||||
node-key="id" |
||||
clearable |
||||
filterable |
||||
v-model="form.departId" |
||||
@change="handleSelectDepart" |
||||
/> |
||||
<div class="tips mt-10"> |
||||
<p>请根据问题信息的内容,再次确认涉及单位是否正确</p> |
||||
</div> |
||||
</el-form-item> |
||||
</el-col> |
||||
</el-row> |
||||
<template v-if="negative.flowKey === FlowNodeEnum.FIRST_DISTRIBUTE"> |
||||
<el-form-item label="办理时限" prop="timeLimit"> |
||||
<el-radio-group v-model="form.timeLimit"> |
||||
<el-radio |
||||
v-for="item in dict.timeLimit" |
||||
:key="item.dictCode" |
||||
:value="item.dictValue" |
||||
>{{ item.dictLabel |
||||
}}{{ item.remark ? `(${item.remark})` : "" }}</el-radio |
||||
> |
||||
</el-radio-group> |
||||
</el-form-item> |
||||
<el-form-item label="审批流程" prop="approvalFlow"> |
||||
<el-radio-group v-model="form.approvalFlow"> |
||||
<el-radio |
||||
v-for="item in dict.approvalFlow" |
||||
:key="item.dictCode" |
||||
:value="item.dictValue" |
||||
>{{ item.dictLabel |
||||
}}{{ item.remark ? `(${item.remark})` : "" }}</el-radio |
||||
> |
||||
</el-radio-group> |
||||
</el-form-item> |
||||
</template> |
||||
</el-form> |
||||
</template> |
||||
<script lang="ts" setup> |
||||
import useCatchStore from "@/stores/modules/catch"; |
||||
import { secondList, listChildren, departTree } from "@/api/system/depart"; |
||||
import { FlowNodeEnum } from "@/enums/flowEnums"; |
||||
import { HostLevel } from "@/enums/dictEnums"; |
||||
|
||||
const dict = useCatchStore().getDicts([ |
||||
"hostLevel", |
||||
"timeLimit", |
||||
"approvalFlow", |
||||
]); |
||||
|
||||
const departs = ref<any[]>([]); |
||||
|
||||
const work = inject("work"); |
||||
|
||||
const negative = inject("negative"); |
||||
|
||||
function getDeparts() { |
||||
if (negative.value.flowKey === FlowNodeEnum.FIRST_DISTRIBUTE) { |
||||
if (form.value.hostLevel === HostLevel.FIRST) { |
||||
departs.value = [ |
||||
{ |
||||
id: '2785', |
||||
name: '警务督察支队' |
||||
}, |
||||
{ |
||||
id: '13494', |
||||
name: '机关纪委' |
||||
}, |
||||
{ |
||||
id: '2780', |
||||
name: '市纪委市监委派驻纪检监察组' |
||||
}, |
||||
] |
||||
} |
||||
if (form.value.hostLevel === HostLevel.SECOND) { |
||||
secondList().then((data) => { |
||||
departs.value = data; |
||||
}); |
||||
} |
||||
if (form.value.hostLevel === HostLevel.THREE) { |
||||
departTree().then((data) => { |
||||
departs.value = data; |
||||
}); |
||||
} |
||||
} else { |
||||
listChildren(work.value.workDepartId).then((data) => { |
||||
departs.value = data; |
||||
}); |
||||
} |
||||
} |
||||
|
||||
onMounted(() => { |
||||
getDeparts(); |
||||
}); |
||||
|
||||
const form = ref({ |
||||
hostLevel: negative.value.hostLevel, |
||||
}); |
||||
|
||||
watch( |
||||
() => form.value.hostLevel, |
||||
() => { |
||||
getDeparts(); |
||||
} |
||||
); |
||||
|
||||
const formRef = ref(null); |
||||
const rules = { |
||||
hostLevel: [ |
||||
{ |
||||
required: true, |
||||
message: "请选择主办层级", |
||||
trigger: ["blur", "change"], |
||||
}, |
||||
], |
||||
departId: [ |
||||
{ |
||||
required: true, |
||||
message: "请选择办理单位", |
||||
trigger: ["blur", "change"], |
||||
}, |
||||
], |
||||
timeLimit: [ |
||||
{ |
||||
required: true, |
||||
message: "请选择办理时限", |
||||
trigger: ["blur", "change"], |
||||
}, |
||||
], |
||||
approvalFlow: [ |
||||
{ |
||||
required: true, |
||||
message: "请选择审批流程", |
||||
trigger: ["blur", "change"], |
||||
}, |
||||
], |
||||
}; |
||||
|
||||
function handleSelectDepart(val: string) { |
||||
form.value.departName = departs.value.filter( |
||||
(item) => item.id === val |
||||
)[0].name; |
||||
} |
||||
|
||||
async function validate() { |
||||
const flag = await formRef.value.validate(); |
||||
if (flag) { |
||||
return form.value; |
||||
} |
||||
} |
||||
|
||||
defineExpose({ |
||||
validate, |
||||
}); |
||||
</script> |
||||
<style lang="scss" scoped> |
||||
</style> |
||||
@ -0,0 +1,59 @@
|
||||
<template> |
||||
<el-dialog title="问题退回" v-model="show"> |
||||
<el-form |
||||
label-position="top" |
||||
ref="formRef" |
||||
:model="form" |
||||
style="height: 400px" |
||||
> |
||||
<el-form-item |
||||
prop="comments" |
||||
:rules="{ |
||||
required: true, |
||||
message: '请输入退回原因', |
||||
trigger: ['blur', 'change'], |
||||
}" |
||||
> |
||||
<template #label> |
||||
<h3 class="inline-block mb-10"> |
||||
退回原因 |
||||
</h3> |
||||
</template> |
||||
<el-input |
||||
type="textarea" |
||||
placeholder="请输入退回原因" |
||||
:autosize="{ minRows: 5 }" |
||||
v-model="form.comments" |
||||
/> |
||||
</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 emit = defineEmits(["submit"]); |
||||
|
||||
const show = ref(false); |
||||
const form = ref({}); |
||||
const formRef = ref(null); |
||||
|
||||
async function submit() { |
||||
await formRef.value.validate(); |
||||
show.value = false; |
||||
emit("submit", form.value); |
||||
} |
||||
|
||||
async function open() { |
||||
show.value = true; |
||||
} |
||||
|
||||
defineExpose({ |
||||
open, |
||||
}); |
||||
</script> |
||||
<style lang="scss" scoped> |
||||
</style> |
||||
@ -0,0 +1,34 @@
|
||||
<template> |
||||
<div class="box" v-for="item in signReturns" :key="item.id"> |
||||
<div class="flex mb-10"> |
||||
<div class="flex v-center gap"> |
||||
<icon name="el-icon-Warning" :size="20" class="danger" /> |
||||
<span class="danger">问题已退回</span> |
||||
<span>{{ item.handlerDepartName }}</span> |
||||
<span>{{ item.handlerName }}</span> |
||||
</div> |
||||
</div> |
||||
<div class="col"> |
||||
<label class="mt-8">退回理由</label> |
||||
<div class="content">{{ item.comments }}</div> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
<script setup> |
||||
const signReturns = inject("signReturns"); |
||||
</script> |
||||
<style lang="scss" scoped> |
||||
.box { |
||||
border: 1px solid #ffe3e3; |
||||
padding: 16px; |
||||
margin-bottom: 12px; |
||||
.danger { |
||||
color: #ff1414; |
||||
font-weight: bold; |
||||
} |
||||
.content { |
||||
background-color: #feeded; |
||||
width: calc(100% - 100px); |
||||
} |
||||
} |
||||
</style> |
||||
@ -0,0 +1,183 @@
|
||||
<template> |
||||
<el-collapse v-model="activeNames"> |
||||
<el-collapse-item |
||||
title="核查办理" |
||||
name="verify" |
||||
v-if="negative.checkStatus" |
||||
> |
||||
<div class="row"> |
||||
<div class="col col-6"> |
||||
<label>核查情况</label> |
||||
<span>{{ negative.checkStatusName }}</span> |
||||
</div> |
||||
<div class="col col-6" v-if="negative.isRectifyName"> |
||||
<label>是否整改</label> |
||||
<span>{{ negative.isRectifyName }}</span> |
||||
</div> |
||||
<div class="col col-6" v-if="negative.accountabilityTarget"> |
||||
<label>追责对象</label> |
||||
<span>{{ getDictLable(dict.accountabilityTarget, negative.accountabilityTarget) }}</span> |
||||
</div> |
||||
<div class="col col-6" v-if="negative.rectifyRestrictionDays"> |
||||
<label>整改限制</label> |
||||
<span>{{ `${negative.rectifyRestrictionDays}天` }}</span> |
||||
</div> |
||||
</div> |
||||
<div class="row" v-if="negative.checkStatusDesc"> |
||||
<div class="col col-24"> |
||||
<label>问题核查结果</label> |
||||
<span>{{ negative.checkStatusDesc }}</span> |
||||
</div> |
||||
</div> |
||||
</el-collapse-item> |
||||
<el-collapse-item v-for="(blame, index) in negative.blames.filter(item => item.type === BlameType.PERSONAL)" :key="index" :title="`涉及人员${index + 1}`" :name="`involved${index}`"> |
||||
<div class="row"> |
||||
<div class="col col-6"> |
||||
<label>涉及人员姓名</label> |
||||
<span>{{ blame.blameName }} {{ blame.blameEmpNo }}</span> |
||||
</div> |
||||
<div class="col col-6"> |
||||
<label>身份证号码</label> |
||||
<span>{{ blame.blameIdCode }}</span> |
||||
</div> |
||||
<div class="col col-6"> |
||||
<label>人员属性</label> |
||||
<span>{{ blame.ivPersonType }}</span> |
||||
</div> |
||||
<div class="col col-6"> |
||||
<label>督察措施</label> |
||||
<span>{{ blame.superviseMeasuresName }}</span> |
||||
</div> |
||||
|
||||
</div> |
||||
<div class="row" v-for="(problem, index) in blame.problems"> |
||||
<div class="col col-24"> |
||||
<label>问题类型{{ index + 1 }}</label> |
||||
<span>{{ problem.oneLevelContent }} / {{ problem.twoLevelContent }}</span> |
||||
</div> |
||||
</div> |
||||
<div class="row"> |
||||
<div class="col col-6"> |
||||
<label>主观方面</label> |
||||
<span>{{ blame.subjectiveAspectName }}</span> |
||||
</div> |
||||
<div class="col col-6"> |
||||
<label>责任类别</label> |
||||
<span>{{ blame.responsibilityTypeName }}</span> |
||||
</div> |
||||
<div class="col col-6"> |
||||
<label>处置结果</label> |
||||
<span>{{ blame.handleResultName }}</span> |
||||
</div> |
||||
<div class="col col-6" v-if="blame.protectRightsName"> |
||||
<label>维权容错</label> |
||||
<span>{{ blame.protectRightsName }}</span> |
||||
</div> |
||||
</div> |
||||
<div class="row" v-if="blame.assistCaseName"> |
||||
<div class="col col-6"> |
||||
<label>帮扶对象</label> |
||||
<span>{{ blame.assistCaseName }}</span> |
||||
</div> |
||||
<div class="col col-18"> |
||||
<label>帮扶起止时间</label> |
||||
<span>{{ blame.assistStartTime }}-{{ blame.assistEndTime }}</span> |
||||
</div> |
||||
</div> |
||||
<div class="row" style="background: #f5f5f5"> |
||||
<div class="col col-6"> |
||||
<label>涉及领导姓名</label> |
||||
<span>{{ blame.leadName }} {{ blame.leadEmpNo }}</span> |
||||
</div> |
||||
<div class="col col-6"> |
||||
<label>身份证号码</label> |
||||
<span>{{ blame.leadIdCode }}</span> |
||||
</div> |
||||
|
||||
<div class="col col-6"> |
||||
<label>责任类别</label> |
||||
<span>{{ blame.leadResponsibilityTypeName }}</span> |
||||
</div> |
||||
<div class="col col-6"> |
||||
<label>督察措施</label> |
||||
<span>{{ blame.leadMeasuresName || '/' }}</span> |
||||
</div> |
||||
<div class="col col-6"> |
||||
<label>处置结果</label> |
||||
<span>{{ blame.leadHandleResultName }}</span> |
||||
</div> |
||||
<div class="col col-6" v-if="blame.leadProtectRightsName"> |
||||
<label>维权容错</label> |
||||
<span>{{ blame.leadProtectRightsName }}</span> |
||||
</div> |
||||
</div> |
||||
</el-collapse-item> |
||||
<el-collapse-item v-for="(blame, index) in negative.blames.filter(item => item.type === BlameType.DEPARTMENT)" :key="index" :title="`涉及单位`" :name="`involved_department`"> |
||||
<div class="row"> |
||||
<div class="col col-6"> |
||||
<label>涉及人姓名</label> |
||||
<span>{{ blame.blameName }}</span> |
||||
</div> |
||||
<div class="col col-6"> |
||||
<label>身份证号码</label> |
||||
<span>{{ blame.blameIdCode }}</span> |
||||
</div> |
||||
<div class="col col-6"> |
||||
<label>人员属性</label> |
||||
<span>{{ blame.ivPersonType }}</span> |
||||
</div> |
||||
</div> |
||||
<div class="row" v-for="problem in blame.problems"> |
||||
<div class="col col-6"> |
||||
<label>问题类型</label> |
||||
<span>{{ problem.oneLevelContent }}</span> |
||||
</div> |
||||
<div class="col col-6"> |
||||
<label>问题类别</label> |
||||
<span>{{ problem.twoLevelContent }}</span> |
||||
</div> |
||||
</div> |
||||
<div class="row"> |
||||
<div class="col col-6"> |
||||
<label>主观方面</label> |
||||
<span>{{ blame.subjectiveAspectName }}</span> |
||||
</div> |
||||
<div class="col col-6"> |
||||
<label>责任类别</label> |
||||
<span>{{ blame.responsibilityTypeName }}</span> |
||||
</div> |
||||
<div class="col col-6"> |
||||
<label>处置结果</label> |
||||
<span>{{ blame.handleResultName }}</span> |
||||
</div> |
||||
</div> |
||||
</el-collapse-item> |
||||
<el-collapse-item |
||||
title="办结佐证材料" |
||||
name="completionAttachment" |
||||
v-if="negative.files.length" |
||||
> |
||||
<file-list :files="negative.files" /> |
||||
</el-collapse-item> |
||||
</el-collapse> |
||||
</template> |
||||
<script setup> |
||||
import { |
||||
BlameType |
||||
} from "@/enums/dictEnums"; |
||||
import { getDictLable } from "@/utils/util"; |
||||
import useCatchStore from "@/stores/modules/catch"; |
||||
|
||||
const dict = useCatchStore().getDicts(["accountabilityTarget"]); |
||||
|
||||
const activeNames = ref(['verify', 'involved_department', 'completionAttachment']) |
||||
const negative = inject('negative'); |
||||
for (let i = 0; i < negative.value.blames.filter(item => item.type === BlameType.PERSONAL).length; i++) { |
||||
activeNames.value.push('involved' + i) |
||||
} |
||||
</script> |
||||
<style lang="scss" scoped> |
||||
.el-collapse { |
||||
--el-collapse-header-text-color: var(--primary-color); |
||||
} |
||||
</style> |
||||
@ -0,0 +1,77 @@
|
||||
<template> |
||||
<div class="flex gap"> |
||||
<template |
||||
v-if=" |
||||
props.hostLevel === HostLevel.FIRST || |
||||
props.hostLevel === HostLevel.SECOND |
||||
" |
||||
> |
||||
<depart-tree-select v-model="departId" /> |
||||
</template> |
||||
<el-select-v2 |
||||
v-model="value" |
||||
:options="polices" |
||||
placeholder="请选择" |
||||
style="width: 240px" |
||||
:props="{ label: 'name', value: 'empNo' }" |
||||
filterable |
||||
clearable |
||||
@change="onChange" |
||||
> |
||||
<template #default="{ item }"> |
||||
<span class="mr-8">{{ item.name }}</span> |
||||
<span style="color: #999; font-size: 12px">{{ |
||||
item.empNo |
||||
}}</span> |
||||
</template> |
||||
</el-select-v2> |
||||
</div> |
||||
</template> |
||||
<script setup> |
||||
import { listPoliceAll } from "@/api/system/police"; |
||||
import { HostLevel } from "@/enums/dictEnums"; |
||||
|
||||
const props = defineProps({ |
||||
modelValue: { |
||||
type: String, |
||||
}, |
||||
hostLevel: { |
||||
type: String, |
||||
default: HostLevel.THREE, |
||||
}, |
||||
departId: { |
||||
type: String, |
||||
required: true, |
||||
}, |
||||
}); |
||||
|
||||
const emit = defineEmits("update:modelValue", "change"); |
||||
|
||||
const value = ref(props.modelValue); |
||||
const departId = ref(props.departId); |
||||
const polices = ref([]); |
||||
|
||||
getPolices(); |
||||
|
||||
watch(value, (val) => { |
||||
emit("update:modelValue", val); |
||||
}); |
||||
watch(departId, () => { |
||||
getPolices(); |
||||
}); |
||||
|
||||
function getPolices() { |
||||
if (departId.value) { |
||||
listPoliceAll(departId.value).then((data) => { |
||||
polices.value = data; |
||||
}); |
||||
} |
||||
} |
||||
|
||||
function onChange(val) { |
||||
console.log("onChange", val); |
||||
emit("change", polices.value.filter((item) => item.empNo === val)[0]); |
||||
} |
||||
</script> |
||||
<style lang="scss" scoped> |
||||
</style> |
||||
@ -0,0 +1,176 @@
|
||||
<template> |
||||
<div class="query-select flex" :multiple="multiple"> |
||||
<label |
||||
class="text-center query-select-label" |
||||
:style="{ width: `${labelWidth}px` }" |
||||
>{{ label }}</label |
||||
> |
||||
<div |
||||
class="query-select-body" |
||||
:style="{ width: `calc(100% - ${labelWidth}px)` }" |
||||
> |
||||
<div class="flex gap"> |
||||
<div class="query-select-content"> |
||||
<el-checkbox-group v-model="checkList" class="flex wrap"> |
||||
<template v-if="multiple"> |
||||
<el-checkbox |
||||
size="small" |
||||
v-for="item in data" |
||||
:key="item.value" |
||||
:label="item.value" |
||||
> |
||||
<span class="text-primary mr-8">{{ |
||||
item.text |
||||
}}</span> |
||||
<span class="total">{{ item.total }}</span> |
||||
</el-checkbox> |
||||
</template> |
||||
<template v-else> |
||||
<div |
||||
class="flex v-center gap query-select-item pointer" |
||||
v-for="item in more? data : data.slice(0, 10)" |
||||
:key="item.value" |
||||
@click="check(item.value)" |
||||
> |
||||
<span class="text-primary">{{ |
||||
item.text |
||||
}}</span> |
||||
<span class="total">{{ item.total }}</span> |
||||
</div> |
||||
</template> |
||||
</el-checkbox-group> |
||||
</div> |
||||
<div class="flex end query-select-tools"> |
||||
<el-button |
||||
size="small" |
||||
v-if="data.length > 13 && !multiple" |
||||
@click="more = !more" |
||||
class="more-btn" |
||||
:more="more" |
||||
>{{ more? '收起' : '更多' }}<icon name="el-icon-ArrowDown" class="ml-4" /></el-button> |
||||
<el-button |
||||
size="small" |
||||
@click="multiple = true" |
||||
v-if="!multiple" |
||||
><icon |
||||
name="el-icon-plus" |
||||
class="mr-4" |
||||
/>多选</el-button |
||||
> |
||||
</div> |
||||
</div> |
||||
<div v-if="multiple" class="flex center"> |
||||
<el-button type="primary" size="small" @click="submit">确定</el-button> |
||||
<el-button size="small" @click="multiple = false" |
||||
>取消</el-button |
||||
> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
<script setup> |
||||
const props = defineProps({ |
||||
label: { |
||||
type: String, |
||||
default: "", |
||||
}, |
||||
labelWidth: { |
||||
type: Number, |
||||
default: 126, |
||||
}, |
||||
data: { |
||||
type: Array, |
||||
default: [], |
||||
}, |
||||
modelValue: { |
||||
type: Array, |
||||
default: [], |
||||
}, |
||||
}); |
||||
const emit = defineEmits(["update:modelValue", "update"]); |
||||
|
||||
const more = ref(false); |
||||
const multiple = ref(false); |
||||
watch(multiple, (val) => { |
||||
more.value = val; |
||||
}); |
||||
const checkList = ref([]); |
||||
function check(val) { |
||||
if (!multiple.value) { |
||||
emit("update:modelValue", [val]); |
||||
emit("update"); |
||||
} |
||||
const index = checkList.value.indexOf(val); |
||||
if (index === -1) { |
||||
checkList.value.push(val); |
||||
} else { |
||||
checkList.value.slice(index, 1); |
||||
} |
||||
} |
||||
|
||||
function submit() { |
||||
emit("update:modelValue", checkList.value); |
||||
emit("update"); |
||||
multiple.value = false |
||||
} |
||||
</script> |
||||
<style lang="scss" scoped> |
||||
.query-select { |
||||
box-shadow: inset 0px -1px 0px 0px #ebebeb; |
||||
&[multiple="true"] { |
||||
border: 1px solid #19257d; |
||||
.query-select-label { |
||||
background: #e8ebfd; |
||||
} |
||||
} |
||||
.query-select-label { |
||||
padding: 8px 0; |
||||
} |
||||
} |
||||
.query-select-body { |
||||
padding: 8px 0; |
||||
.query-select-tools { |
||||
width: 140px; |
||||
} |
||||
.el-button + .el-button { |
||||
margin-left: 8px; |
||||
} |
||||
} |
||||
.query-select-content { |
||||
width: calc(100% - 130px); |
||||
.el-checkbox-group { |
||||
font-size: inherit; |
||||
line-height: inherit; |
||||
padding: 0 20px; |
||||
gap: 8px 20px; |
||||
.el-checkbox { |
||||
margin-right: 0; |
||||
} |
||||
} |
||||
.text-primary { |
||||
font-size: 14px; |
||||
} |
||||
.total { |
||||
font-size: 12px; |
||||
color: #666; |
||||
} |
||||
} |
||||
.query-select-item { |
||||
&:hover { |
||||
.text-primary { |
||||
font-weight: 700; |
||||
} |
||||
} |
||||
|
||||
} |
||||
.more-btn { |
||||
.el-icon { |
||||
transition: .6s; |
||||
} |
||||
&[more=true] { |
||||
.el-icon { |
||||
transform: rotate(180deg); |
||||
} |
||||
} |
||||
} |
||||
</style> |
||||