[add]auth 认证

This commit is contained in:
sdaduanbilei-d1581 2025-03-31 17:29:55 +08:00
commit c44239fa1e
87 changed files with 11983 additions and 0 deletions

24
.eslintrc.cjs Normal file
View File

@ -0,0 +1,24 @@
module.exports = {
env: {
browser: true,
es2021: true
},
extends: ['plugin:vue/vue3-essential','prettier'],
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module'
},
globals:{
AMap:true,
AMapUI:true,
Local:true
},
plugins: ['vue', 'prettier'],
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'vue/multi-word-component-names': 'off',
'prettier/prettier': 'error'
},
ignorePatterns: ['**/index.html']
}

24
.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

19
.prettierrc.cjs Normal file
View File

@ -0,0 +1,19 @@
module.exports = {
// 一行的字符数如果超过会进行换行默认为80
printWidth: 80,
// 一个tab代表几个空格数默认为80
tabWidth: 4,
// 是否使用tab进行缩进默认为false表示用空格进行缩减
useTabs: false,
// 字符串是否使用单引号默认为false使用双引号
singleQuote: true,
// 行位是否使用分号默认为true
semi: false,
// 是否使用尾逗号,有三个可选值"<none|es5|all>"
trailingComma: 'none',
// 对象大括号直接是否有空格默认为true效果{ foo: bar }
bracketSpacing: true, // 对象大括号内两边是否加空格 { a:0 }
arrowParens: 'avoid', // 单个参数的箭头函数不加括号 x => x
// 开启 eslint 支持
eslintIntegration: true
}

3
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}

5
README.md Normal file
View File

@ -0,0 +1,5 @@
# Vue 3 + Vite
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).

12
eslint.config.js Normal file
View File

@ -0,0 +1,12 @@
import globals from "globals";
import pluginJs from "@eslint/js";
import pluginVue from "eslint-plugin-vue";
/** @type {import('eslint').Linter.Config[]} */
export default [
{files: ["**/*.{js,mjs,cjs,vue}"]},
{languageOptions: { globals: globals.browser }},
pluginJs.configs.recommended,
...pluginVue.configs["flat/essential"],
];

13
index.html Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<link rel="icon" type="image/svg+xml" href="/vite.svg"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>梧桐工作台</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

45
package.json Normal file
View File

@ -0,0 +1,45 @@
{
"name": "wt_workbench_web",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@vue-office/docx": "^1.6.2",
"@vue-office/excel": "^1.7.11",
"@vue-office/pptx": "^0.0.6",
"axios": "^1.7.7",
"js-md5": "^0.8.3",
"pinia": "^2.2.6",
"pinia-plugin-persistedstate": "^4.1.3",
"vue": "^3.5.12",
"vue-cookies": "^1.8.4",
"vue-demi": "0.14.6",
"vue-router": "^4.4.5"
},
"devDependencies": {
"@arco-design/web-vue": "^2.56.3",
"@babel/eslint-parser": "^7.25.9",
"@eslint/js": "^9.14.0",
"@vitejs/plugin-vue": "^5.1.4",
"eslint": "^9.14.0",
"eslint-config-prettier": "^9.1.0",
"eslint-config-standard": "^17.1.0",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-n": "^17.13.1",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-promise": "^7.1.0",
"eslint-plugin-vue": "^9.31.0",
"globals": "^15.12.0",
"prettier": "^3.3.3",
"sass": "^1.80.6",
"vite": "^5.4.10",
"vite-plugin-eslint": "^1.8.1",
"vite-plugin-pages": "^0.32.3",
"vite-plugin-vue-layouts": "^0.11.0"
}
}

1
public/vite.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

10
src/App.vue Normal file
View File

@ -0,0 +1,10 @@
<template>
<div id="app">
<router-view :key="$router.fullPath"> </router-view>
</div>
</template>
<script setup></script>
<style scoped>
</style>

20
src/api/base/contacts.js Normal file
View File

@ -0,0 +1,20 @@
import fetch from '../fetch.js'
export default {
/**
* 机构列表
* @param params
* @returns {Promise | Promise<unknown>}
*/
submit(params) {
return fetch('/base/customer/contacts/submit', params, 'post', 'json')
},
page(params) {
return fetch('/base/customer/contacts/page', params)
},
list(params) {
return fetch('/base/customer/contacts/list', params)
}
}

28
src/api/base/customer.js Normal file
View File

@ -0,0 +1,28 @@
import fetch from '../fetch.js'
export default {
/**
* 机构列表
* @param params
* @returns {Promise | Promise<unknown>}
*/
submit(params) {
return fetch('/base/customer/submit', params, 'post', 'json')
},
page(params) {
return fetch('/base/customer/page', params)
},
list(params) {
return fetch('/base/customer/list', params)
},
info(params) {
return fetch('/base/customer/info', params)
},
all(params) {
return fetch('/base/customer/all', params)
}
}

16
src/api/base/file.js Normal file
View File

@ -0,0 +1,16 @@
import fetch from '../fetch.js'
export default {
/**
* 机构列表
* @param params
* @returns {Promise | Promise<unknown>}
*/
info(fileId) {
return fetch('/file/info/' + fileId)
},
download(params) {
return fetch('/file/download', params, 'get', 'form', {}, 'blob')
}
}

41
src/api/base/index.js Normal file
View File

@ -0,0 +1,41 @@
import fetch from '../fetch.js'
export default {
/**
* 机构列表
* @param params
* @returns {Promise | Promise<unknown>}
*/
list(params) {
return fetch('/sys/dept/list/children', params)
},
submit(params) {
return fetch('/sys/dept/submit', params, 'post', 'json')
},
info(params) {
return fetch('/sys/dept/info/org', params)
},
// ==================
userList(params) {
return fetch('/user/list', params)
},
userAll(params) {
return fetch('/user/page', params)
},
userSave(params) {
return fetch('/user/submit', params, 'post', 'json')
},
// ===========
areaTree(params) {
return fetch('/sys/region/tree', params)
},
areaDetail(params) {
return fetch('/sys/region/detail', params)
}
}

16
src/api/contract/file.js Normal file
View File

@ -0,0 +1,16 @@
import fetch from '../fetch.js'
export default {
/**
* 机构列表
* @param params
* @returns {Promise | Promise<unknown>}
*/
submit(params) {
return fetch('/contract/file/submit', params, 'post', 'json')
},
page(params) {
return fetch('/contract/file/page', params)
}
}

16
src/api/contract/index.js Normal file
View File

@ -0,0 +1,16 @@
import fetch from '../fetch.js'
export default {
/**
* 机构列表
* @param params
* @returns {Promise | Promise<unknown>}
*/
submit(params) {
return fetch('/contract/submit', params, 'post', 'json')
},
page(params) {
return fetch('/contract/page', params)
}
}

16
src/api/contract/pay.js Normal file
View File

@ -0,0 +1,16 @@
import fetch from '../fetch.js'
export default {
/**
* 机构列表
* @param params
* @returns {Promise | Promise<unknown>}
*/
submit(params) {
return fetch('/contract/pay/log/submit', params, 'post', 'json')
},
page(params) {
return fetch('/contract/pay/log/page', params)
}
}

99
src/api/fetch.js Normal file
View File

@ -0,0 +1,99 @@
import axios from 'axios'
import router from '@/router'
axios.defaults.url = ''
axios.interceptors.request.use(
config => {
config.headers.Platform = 'pc'
config.headers.type = 'web'
config.headers.appId = 'E191C42B'
config.headers.Authorization = 'Bearer' + localStorage.getItem('token')
return config
},
err => {
return Promise.reject(err)
}
)
function fetch(
url = '',
params = {},
method = 'get',
contentType = 'form',
header = {},
responseType = '',
insurance,
timeout = 60000 * 10
) {
contentType === 'form' &&
(contentType = 'application/x-www-form-urlencoded')
contentType === 'json' && (contentType = 'application/json')
contentType === 'file' && (contentType = 'multipart/form-data')
const query = []
for (const k in params) {
query.push(k + '=' + params[k])
}
let qs = query.join('&')
if (
contentType === 'application/x-www-form-urlencoded' &&
query.length > 0 &&
method === 'get'
) {
url += (url.indexOf('?') < 0 ? '?' : '&') + qs
}
if (
contentType !== 'application/x-www-form-urlencoded' &&
method !== 'get'
) {
qs = params
}
return new Promise((resolve, reject) => {
const requestParams = {
timeout,
method,
url: '/api' + url,
data: qs,
responseType,
headers: {
'Content-Type': contentType,
...header
}
}
const success = response => {
const { status, data = {} } = response
if (status >= 200 && status <= 401) {
if (data.code === 401) {
const path = router.currentRoute.value.fullPath
router.push({
path: '/login',
query: { redirect: path }
})
reject(new Error('需要登录'))
return
}
if (data.code === 40005) {
console.log('权限不租')
router.push('/401')
}
resolve(data)
} else if (status === 500) {
resolve(data)
} else {
resolve(data)
}
}
axios(requestParams)
.then(success)
.catch(err => {
const { status, data = {} } = err.response
console.log(status)
if (status === 401) {
router.push(`/login`)
} else {
resolve(data)
}
})
})
}
export default fetch

29
src/api/index.js Normal file
View File

@ -0,0 +1,29 @@
import user from '@/api/user/index.js'
import sys from '@/api/sys/index.js'
import base from '@/api/base/index.js'
import customer from '@/api/base/customer.js'
import contacts from '@/api/base/contacts.js'
import project from '@/api/project/index.js'
import task from '@/api/project/task.js'
import projectFile from '@/api/project/file.js'
import file from '@/api/base/file.js'
import contract from '@/api/contract/index.js'
import contractFile from '@/api/contract/file.js'
import contractPay from '@/api/contract/pay.js'
import notice from '@/api/notice/index.js'
export default {
user,
sys,
base,
customer,
contacts,
project,
task,
projectFile,
file,
contract,
contractFile,
contractPay,
notice
}

14
src/api/notice/index.js Normal file
View File

@ -0,0 +1,14 @@
import fetch from '../fetch.js'
export default {
page(params) {
return fetch('/notice/page', params)
},
unReadAll(params) {
return fetch('/notice/unread/count', params)
},
readAll(params) {
return fetch('/notice/read/all', params)
}
}

19
src/api/project/file.js Normal file
View File

@ -0,0 +1,19 @@
import fetch from '../fetch.js'
export default {
initFolder(params) {
return fetch('/project/file/folder/init', params, 'post')
},
page(params) {
return fetch('/project/file/page', params)
},
submit(params) {
return fetch('/project/file/submit', params, 'post', 'json')
},
remove(params) {
return fetch('/project/file/remove', params, 'post')
}
}

15
src/api/project/index.js Normal file
View File

@ -0,0 +1,15 @@
import fetch from '../fetch.js'
export default {
submit(params) {
return fetch('/project/submit', params, 'post', 'json')
},
page(params) {
return fetch('/project/page', params)
},
findByCity(params) {
return fetch('/project/list/customer', params)
}
}

23
src/api/project/task.js Normal file
View File

@ -0,0 +1,23 @@
import fetch from '../fetch.js'
export default {
submit(params) {
return fetch('/project/task/submit', params, 'post', 'json')
},
page(params) {
return fetch('/project/task/page', params)
},
info(params) {
return fetch('/project/task/info', params)
},
removeFiles(params) {
return fetch('/project/task/remove/files', params)
},
allLog(params) {
return fetch('/project/task/log', params)
}
}

99
src/api/sys/index.js Normal file
View File

@ -0,0 +1,99 @@
import fetch from '../fetch.js'
export default {
/**
* 用户菜单
* @param params
* @returns {Promise | Promise<unknown>}
*/
menus() {
return fetch('/sys/menu/menu')
},
// ++++++++++++++++++++++++++++++++
/**
* 客户端列表
* @param params
* @returns {Promise | Promise<unknown>}
*/
clients(params) {
return fetch('/sys/client/list', params)
},
/**
* 获取全部客户端列表
* @param params
* @returns {Promise | Promise<unknown>}
*/
clientList() {
return fetch('/sys/client/list')
},
// ++++++++++++++++++++++++++++++++
/**
* 根据aapId 获取菜单
* @param params
* @returns {Promise | Promise<unknown>}
*/
menuTree(params) {
return fetch('/sys/menu/all/by/appid', params)
},
menuRemove(params) {
return fetch('/sys/menu/remove', params, 'post')
},
/**
* 新鲜或者编辑
* @param params
* @returns {Promise | Promise<unknown>}
*/
menuSave(params) {
return fetch('/sys/menu/submit', params, 'post', 'json')
},
// ++++++++++++++++++++++++++++++++
/**
* 用户权限
* @param params
* @returns {Promise | Promise<unknown>}
*/
roleList(params) {
return fetch('/sys/role/list', params)
},
roleChange(params) {
return fetch('/sys/role/update', params, 'post')
},
/**
* 新增 或者更新
* @param params
* @returns {Promise | Promise<unknown>}
*/
roleSubmit(params) {
return fetch('/sys/role/submit', params, 'post', 'json')
},
// ++++++++++++++++++++++++++++++++
/**
* 字典
* @param params
* @returns {Promise<unknown>}
*/
dictPage(params) {
return fetch('/sys/dict/page', params)
},
dictSave(params) {
return fetch('/sys/dict/submit', params, 'post', 'json')
},
dict(params) {
return fetch('/sys/dict/detail', params)
},
dictRemove(params) {
return fetch('/sys/dict/remove', params, 'post')
},
// +============================== log /
logPage(params) {
return fetch('/sys/log/page', params)
}
}

27
src/api/user/index.js Normal file
View File

@ -0,0 +1,27 @@
import fetch from '../fetch.js'
export default {
/**
* 用户登录
* @param params
* @returns {Promise | Promise<unknown>}
*/
login(params) {
return fetch('/user/login', params, 'post','json')
},
info(params) {
return fetch('/user/info', params)
},
retPwd(params) {
return fetch('/user/restPwd', params, 'post')
},
changeStatus(params) {
return fetch('/user/changeStatus', params, 'post')
},
list(params) {
return fetch('/user/list', params)
}
}

View File

@ -0,0 +1,5 @@
$primary: #AB7630;
$red: #ef0b0b;
$blue: #409eff;
$green: #5dc800;
$border: #F2F3F5;

882
src/assets/style/main.scss Normal file
View File

@ -0,0 +1,882 @@
$primary: #AB7630;
$red: #ef0b0b;
$blue: #409eff;
$green: #5dc800;
$border: #F2F3F5;
body, h1, h2, h3, h4, h5, h6, hr, p, blockquote, dl, dt, dd, ul, ol, li, pre, form, fieldset, legend, input, textarea, th, td {
margin: 0;
padding: 0
}
body, input, select, textarea {
font: 14px -apple-system;
color: #333;
-ms-overflow-style: scrollbar
}
.hover:hover {
background: #f0f1f3;
}
body, html {
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", Arial, sans-serif;
}
button, input, select, textarea {
font-size: 100%
}
a {
color: #333;
text-decoration: none;
outline: 0
}
a:hover {
color: $primary
}
table {
border-collapse: collapse;
border-spacing: 0
}
fieldset, img, area, a {
border: 0;
outline: 0
}
address, caption, cite, code, dfn, em, th, var, i {
font-style: normal;
font-weight: normal
}
code, kbd, pre, samp {
font-family: courier new, courier, monospace
}
ol, ul {
list-style: none
}
body {
background: #F7F8F9;
-webkit-text-size-adjust: none;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
small {
font-size: 12px
}
h1, h2, h3, h4, h5, h6 {
font-size: 100%
}
sup {
vertical-align: text-top
}
sub {
vertical-align: text-bottom
}
legend {
color: #000
}
a, input {
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
div {
-webkit-overflow-scrolling: touch;
}
input, textarea, select {
border: none;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
-webkit-appearance: none;
-moz-appearance: none;
border-radius: 0;
background: none;
outline: none;
}
input[type='checkbox'] {
-webkit-appearance: checkbox;
}
img {
vertical-align: top;
}
@media screen and (max-device-width: 828px) {
body {
-webkit-text-size-adjust: none;
}
}
/****************flexbox**************/
.flex {
display: flex;
}
.inline-flex {
display: inline-flex;
}
.flex-center {
justify-content: center;
align-items: center;
}
.flex-center-between {
justify-content: space-between;
align-items: center;
}
/*横向或纵向*/
.flex-row {
flex-direction: row;
}
.flex-col {
flex-direction: column;
}
.flex-row-reverse {
flex-direction: row-reverse;
}
.flex-col-reverse {
flex-direction: column-reverse;
}
.flex-wrap {
flex-wrap: wrap;
}
/*主轴对齐方式*/
.flex-justify-start {
justify-content: flex-start;
}
.flex-justify-end {
justify-content: flex-end;
}
.flex-justify-center {
justify-content: center;
}
.flex-justify-between {
justify-content: space-between;
}
.flex-justify-around {
justify-content: space-around;
}
/*侧轴对齐方式*/
.flex-align-start {
align-items: flex-start;
}
.flex-align-end {
align-items: flex-end;
}
.flex-align-center {
align-items: center;
}
.flex-align-baseline {
align-items: baseline;
}
.flex-align-stretch {
align-items: stretch;
}
/*主轴换行时行在侧轴的对齐方式必须定义flex-wrap为换行*/
.flex-content-start {
align-content: flex-start;
}
.flex-content-end {
align-content: flex-end;
}
.flex-content-center {
align-content: center;
}
.flex-content-between {
align-content: space-between;
}
.flex-content-around {
align-content: space-around;
}
.flex-content-stretch {
align-content: stretch;
}
/*允许子元素收缩*/
.flex-child-grow {
flex-grow: 1;
}
/*允许拉伸*/
.flex-child-shrink {
flex-shrink: 1;
}
/*允许收缩*/
.flex-child-noshrink {
flex-shrink: 0;
}
/*不允许收缩*/
.flex-child-average {
flex: 1;
}
/*平均分布,兼容旧版必须给宽度*/
.flex-child-first {
order: 1;
}
/*排第一个*/
/*子元素在侧轴的对齐方式*/
.flex-child-align-start {
align-self: flex-start;
}
.flex-child-align-end {
align-self: flex-end;
}
.flex-child-align-center {
align-self: center;
}
.flex-child-align-baseline {
align-self: baseline;
}
.flex-child-align-stretch {
align-self: stretch;
}
.wrapper {
width: 1340px;
margin: 0 auto;
}
.block {
display: block;
}
.inline-block {
display: inline-block;
}
.inline {
display: inline;
}
.hide {
display: none;
}
.full-height {
height: 100%;
}
.full-width {
width: 100%;
}
.full-size {
width: 100%;
height: 100%;
}
.full-screen {
width: 100vw;
height: 100vh;
}
.relative {
position: relative;
}
.absolute {
position: absolute;
}
.fixed {
position: fixed;
}
.clear {
clear: both;
}
.overflow-hide {
overflow: hidden;
}
.normal {
font-weight: normal;
}
.bold {
font-weight: bold;
}
.bold-500 {
font-weight: 500;
}
.radius-5 {
border-radius: 5px;
}
.radius {
border-radius: 8px;
}
.radius-5 {
border-radius: 5px;
}
.font-34 {
font-size: 34px;
}
.font-30 {
font-size: 30px;
}
.font-28 {
font-size: 28px;
}
.font-24 {
font-size: 24px;
}
.font-20 {
font-size: 20px;
}
.font-18 {
font-size: 18px;
}
.font-16 {
font-size: 16px;
}
.font-15 {
font-size: 15px;
}
.font-14 {
font-size: 14px;
}
.font-13 {
font-size: 13px;
}
.font-12 {
font-size: 12px;
}
.border-box {
box-sizing: border-box;
}
.border {
border: 1px solid $border;
}
.border-top {
border-top: 1px solid $border;
}
.border-bottom {
border-bottom: 1px solid $border;
}
.border-left {
border-left: 1px solid $border;
}
.border-right {
border-right: 1px solid $border;
}
.margin {
margin: 10px;
}
.mt-5 {
margin-top: 5px;
}
.mr-5 {
margin-right: 5px;
}
.mb-5 {
margin-bottom: 5px;
}
.ml-5 {
margin-left: 5px;
}
.mt-10 {
margin-top: 10px;
}
.mr-10 {
margin-right: 10px;
}
.mb-10 {
margin-bottom: 10px;
}
.ml-10 {
margin-left: 10px;
}
.mt-15 {
margin-top: 15px;
}
.mr-15 {
margin-right: 15px;
}
.mb-15 {
margin-bottom: 15px;
}
.ml-15 {
margin-left: 15px;
}
.mt-20 {
margin-top: 20px;
}
.mr-20 {
margin-right: 20px;
}
.mb-20 {
margin-bottom: 20px;
}
.ml-20 {
margin-left: 20px;
}
.padding {
padding: 10px;
}
.padding-14 {
padding: 14px;
}
.padding-20 {
padding: 20px;
}
.padding-top {
padding-top: 10px;
}
.padding-right {
padding-right: 10px;
}
.padding-bottom {
padding-bottom: 10px;
}
.padding-left {
padding-left: 10px;
}
.text-center {
text-align: center;
}
.text-right {
text-align: right;
}
.text-left {
text-align: left;
}
.pointer {
cursor: pointer;
}
.ellipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.nowrap {
word-wrap: normal;
white-space: nowrap;
}
.lines-1 {
text-overflow: ellipsis;
-webkit-line-clamp: 1;
display: -webkit-box;
overflow: hidden;
-webkit-box-orient: vertical;
}
.lines-2 {
text-overflow: ellipsis;
-webkit-line-clamp: 2;
display: -webkit-box;
overflow: hidden;
-webkit-box-orient: vertical;
word-break: break-all;
}
.lines-height-2 {
line-height: 2
}
.lines-height-15 {
line-height: 1.5
}
.row2 {
word-break: break-all;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
.card-item {
border-radius: 8px;
box-shadow: 0 2px 10px 0 rgba(0, 0, 0, 0.08);
background-color: #fff;
margin: 0 14px 14px;
overflow: hidden;
}
.btn {
height: 32px;
border-radius: 5px;
}
.pointer {
cursor: pointer;
}
.hide-scrollbar::-webkit-scrollbar {
display: none;
}
.box-shadow-blue {
box-shadow: 0 0 16px 2px #deedff;
}
.box-shadow {
box-shadow: 0px 2px 10px 0px rgba(0, 0, 0, 0.18)
}
.main-button {
color: #fff;
width: 100%;
height: 44px;
line-height: 44px;
border-radius: 8px;
font-size: 16px;
background: linear-gradient(239deg, #85CAFF 0%, #007CDD 100%);
cursor: pointer;
&:hover {
opacity: 0.9;
}
}
.button-plain {
width: 110px;
color: $primary;
text-align: center;
height: 32px;
border-radius: 16px;
border: 2px solid $primary;
line-height: 28px;
box-sizing: border-box;
&:hover {
background: rgba($primary, 0.1);
}
}
/*颜色*/
.main-color, a.main-color {
color: $primary;
}
/*主色*/
.main-bgcolor {
background-color: $primary;
}
.assist-color, a.assist-color {
color: #78b
}
/*辅助色*/
.assist-bgcolor {
background-color: #78b;
}
.orange, a.orange {
color: #ff6700;
}
.orange-bg {
background-color: #ff6700;
}
.grey, a.grey {
color: #707070;
}
.grey-bg {
background-color: #505050;
}
.grey-6, a.grey-6 {
color: #6b6b6b;
}
.grey-6-bg {
background-color: #F6F6F6;
}
.grey-9, a.grey-9 {
color: #9c9c9c;
}
.grey-9-bg {
background-color: #9c9c9c;
}
.grey-d, a.grey-d {
color: #dfdfdf;
}
.grey-d-bg {
background-color: #dfdfdf;
}
.grey-e, a.grey-e {
color: #efefef;
}
.grey-e-bg {
background-color: #efefef;
}
.grey-f, a.grey-f {
color: #f5f5f5;
}
.grey-f-bg {
background-color: #f5f5f5;
}
.grey-fa-bg {
background-color: #fafafa;
}
.black, a.black {
color: #333;
}
.black-bg {
background-color: #333;
}
.white, a.white {
color: #fff;
}
.white-bg {
background-color: #fff;
}
.red, a.red {
color: #e12e2e;
}
.red-bg {
background-color: #e12e2e;
}
.light-red, a.light-red {
color: #ff5050;
}
.light-red-bg {
background-color: #ff5050;
}
.orange-red, a.orange-red {
color: #ff4e00;
}
.orange-red-bg {
background-color: #ff4e00;
}
.yellow, a.yellow {
color: #fbcb30;
}
.yellow-bg {
background-color: #fbcb30;
}
.orange, a.orange {
color: #ff6700;
}
.orange-bg {
background-color: #ff6700;
}
.orange-yellow, a.orange-yellow {
color: #fd9712;
}
.orange-yellow-bg {
background-color: #fd9712;
}
.green, a.green {
color: $green;
}
.green-bg {
background-color: $green;
}
.green-text, a.green-text {
color: #008000;
}
.light-green, a.light-green {
color: #8fd14c;
}
.light-green-bg {
background-color: #8fd14c;
}
.blue, a.blue {
color: #3978F1;
}
.blue-bg {
background-color: #3978F1;
}
.light-blue, a.light-blue {
color: #7597dc;
}
.light-blue-bg {
background-color: #7597dc;
}
.pink, a.pink {
color: #fb5c9b;
}
.pink-bg {
background-color: #fb5c9b;
}
.purple, a.purple {
color: #a776d9;
}
.purple-bg {
background-color: #a776d9;
}
.light-purple, a.light-purple {
color: #b394f3;
}
.light-purple-bg {
background-color: #b394f3;
}
/* 表格头部标题左边的border */
.before-line {
position: relative;
margin-left: 10px;
}
.before-line:before {
content: '';
position: absolute;
left: -10px;
top: 54%;
transform: translateY(-50%);
border: 2px solid $primary;
border-radius: 2px;
height: 50%;
}
.hover-bg {
background-color: #f0f1f3;
}
.container {
background-color: white;
border-radius: 5px;
padding: 16px;
box-shadow: 0 0 2px rgba(0, 0, 0, 0.1);
}
.row {
padding: 16px;
border-bottom: #efeff2 solid 1px;
}
.row:hover {
padding: 16px;
background-color: #f0f1f3;
}

1
src/assets/vue.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

After

Width:  |  Height:  |  Size: 496 B

View File

@ -0,0 +1,28 @@
<template>
<div>
<div
class="flex flex-center flex-justify-between"
style="padding: 32px 0px"
>
<div class="font-24">{{ title }}</div>
<slot name="right"></slot>
</div>
</div>
</template>
<script>
export default {
props: {
title: {
type: String,
default: ''
}
}
}
</script>
<style lang="scss" scoped>
.title {
top: 0;
}
</style>

View File

@ -0,0 +1,143 @@
<template>
<div>
<a-button type="text" size="small" @click="show = true">查看</a-button>
<a-drawer
:mask-closable="false"
:header="false"
width="80%"
v-model:visible="show"
>
<div v-if="file">
<div class="font-18 bold">{{ file.fileName }}</div>
<a-divider />
<div>
<iframe
v-if="file.suffix === '.pdf'"
style="width: 100%; height: 100vh"
:src="`/api/file/` + file.id"
>
</iframe>
<vue-office-docx
v-else-if="['.docx'].includes(file.suffix)"
:src="`/api/file/` + file.id"
style="width: 100%; height: 100vh"
/>
<vue-office-excel
v-else-if="['.xlsx', '.xls'].includes(file.suffix)"
:src="`/api/file/` + file.id"
style="width: 100%; height: 100vh"
/>
<div v-else-if="file.fileType.indexOf('image') > -1">
<a-image
:src="`/api/file/` + file.id"
:fit="'contain'"
height="960px"
width="100%"
/>
</div>
<div
v-else
class="flex flex-center flex-col"
style="margin-top: 20%"
>
<a-button
type="primary"
status="danger"
@click="download"
>立即下载
</a-button>
<div class="grey-6 mt-20">
该类型的文件不支持在线预览请点击 立即下载
后使用对应软件查看
</div>
</div>
</div>
</div>
<div
class="flex flex-center flex-col"
v-else
style="margin-top: 20%"
>
<icon-loading :size="32" />
<div class="mt-20">加载中请稍后</div>
</div>
<template #footer>
<div>
<a-button type="primary" @click="download">下载</a-button>
</div>
</template>
</a-drawer>
</div>
</template>
<script>
//VueOfficeDocx
import VueOfficeDocx from '@vue-office/docx'
//
import '@vue-office/docx/lib/index.css'
//VueOfficeExcel
import VueOfficeExcel from '@vue-office/excel'
//
import '@vue-office/excel/lib/index.css'
export default {
components: {
VueOfficeDocx,
VueOfficeExcel
},
props: {
fileId: {
required: true,
type: String,
default: ''
}
},
watch: {
show: {
handler(val) {
if (val) {
this.fetchFile()
}
}
}
},
data() {
return {
show: false,
file: null
}
},
methods: {
fetchFile() {
this.$api.file.info(this.fileId).then(res => {
if (res.code === 200) {
this.file = res.data
}
})
},
download() {
this.$api.file.download({ fileId: this.file.id }).then(res => {
if (res.hasOwnProperty('code')) {
this.$message.error(res.msg)
return
}
this.downloadFile(res)
})
},
downloadFile(res) {
const url = window.URL.createObjectURL(new Blob([res]))
const link = document.createElement('a')
link.style.display = 'none'
link.href = url
const excelName = this.file.fileName
link.setAttribute('download', excelName)
document.body.appendChild(link)
link.click()
link.remove()
this.$notification.success('下载成功')
}
}
}
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,57 @@
<template>
<div>
<a-upload
:action="action"
:show-file-list="false"
:auto-upload="true"
multiple
@progress="progress"
@success="upload"
>
<template #upload-button>
<a-button size="small" type="outline">
<template #icon>
<icon-loading v-if="loading" />
<icon-upload v-else />
</template>
<template #default> 上传文件</template>
</a-button>
</template>
</a-upload>
</div>
</template>
<script>
export default {
props: {
action: {
type: String,
default: '/api/file/upload'
}
},
data() {
return {
loading: false
}
},
methods: {
progress(e, progress) {
console.log(e, progress)
if (progress.loaded > 0) {
this.loading = true
}
},
upload(res) {
const code = res.response.code
this.loading = false
if (code === 200) {
this.$emit('ok', res.response.data)
} else {
this.$notification.error('上传错误')
}
}
}
}
</script>
<style lang="scss" scoped></style>

81
src/layout/header.vue Normal file
View File

@ -0,0 +1,81 @@
<template>
<div
class="flex flex-center flex-justify-between"
style="padding: 12px 32px"
>
<div class="logo pointer" @click="this.$router.push('/')">
<img
src="https://res.wutongshucloud.com/res/2024/12/05/202412051410990.svg"
/>
</div>
<div class="flex flex-center">
<div>
<a-badge :count="list.length" dot :offset="[2, -2]">
<icon-notification
size="25"
class="pointer"
@click="$router.push('../message')"
/>
</a-badge>
</div>
<a-divider direction="vertical" />
<div v-if="user">
<a-dropdown :trigger="'hover'">
<div class="flex flex-center flex-justify-start pointer">
<h2 class="mr-10">{{ user.name }}</h2>
<a-avatar v-if="user.avatar">
<img alt="avatar" :src="user.avatar" />
</a-avatar>
<a-avatar v-else style="background-color: blue">
<div>{{ user.name }}</div>
</a-avatar>
</div>
<template #content>
<a-doption value="1" @click="goUser"
>个人中心
</a-doption>
<a-doption value="2" @click="logout">退出</a-doption>
</template>
</a-dropdown>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
user: null,
list: []
}
},
mounted() {
this.init()
},
methods: {
init() {
const tmp = localStorage.getItem('user')
if (tmp) {
this.user = JSON.parse(tmp)
console.log('rrrr')
this.fetchNotice()
}
},
fetchNotice() {
console.log('unread')
this.$api.notice.unReadAll().then(res => {
if (res.code === 200) {
this.list = res.data
}
})
},
logout() {
this.$cookies.remove('wt')
this.$router.push('/login')
}
}
}
</script>
<style lang="scss" scoped></style>

62
src/layout/index.vue Normal file
View File

@ -0,0 +1,62 @@
<template>
<a-layout class="full-screen">
<a-layout-header class="header">
<xheader />
</a-layout-header>
<a-layout-sider
hide-trigger
style="background-color: transparent; width: 220px"
>
<div class="padding">
<left />
</div>
</a-layout-sider>
<a-layout style="padding: 0 24px; margin-top: 78px">
<a-layout-content class="wrapper">
<router-view />
</a-layout-content>
</a-layout>
</a-layout>
</template>
<script>
import { defineComponent, ref } from 'vue'
import { Message } from '@arco-design/web-vue'
import Left from '@/layout/left.vue'
import xheader from '@/layout/header.vue'
export default defineComponent({
components: {
Left,
xheader
},
setup() {
const collapsed = ref(false)
const onCollapse = () => {
collapsed.value = !collapsed.value
}
return {
collapsed,
onCollapse,
onClickMenuItem(key) {
Message.info({ content: `You select ${key}`, showIcon: true })
}
}
}
})
</script>
<style scoped>
.header {
width: 100vw;
position: fixed;
z-index: 3;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
background-color: white;
}
.other {
position: relative;
top: 60px;
height: calc(100vh - 60px);
}
</style>

98
src/layout/left.vue Normal file
View File

@ -0,0 +1,98 @@
<template>
<div>
<div style="background-color: red">
<div class="menu">
<div class="mt-20" style="margin-top: 48px">
<div v-for="item in list" :key="item.id">
<div
class="row flex flex-center flex-justify-start pointer"
@click="goPath(item)"
>
<div class="flex flex-center">
<icon-home />
<div class="ml-10">
{{ item.name }}
</div>
</div>
</div>
<div v-if="item.children">
<div
v-for="sub in item.children"
class="sub flex flex-center flex-justify-start pointer"
:key="sub.id"
@click="goPath(sub)"
>
<div
class="flex flex-center flex-justify-between"
>
<div class="flex-child-average">
{{ sub.name }}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
user: null,
list: []
}
},
mounted() {
this.fetchMenu()
},
methods: {
fetchMenu() {
this.$api.sys.menus().then(res => {
if (res.code === 200) {
this.list = res.data
}
})
},
goPath(res) {
console.log(res)
this.$router.push(res.value)
}
}
}
</script>
<style lang="scss" scoped>
.menu {
position: fixed;
top: 0;
width: 200px;
padding-top: 32px;
bottom: 200px;
.row {
padding: 16rpx;
font-weight: bold;
}
.row:hover {
color: #ab7630;
font-weight: bold;
background-color: #f0f1f3;
}
.sub {
padding: 16px 62px;
border-bottom: 1px #f0f1f3 solid;
}
.sub:hover {
color: #ab7630;
padding: 16px 62px;
background-color: #f0f1f3;
}
}
</style>

18
src/main.js Normal file
View File

@ -0,0 +1,18 @@
import { createApp } from 'vue'
import './assets/style/main.scss'
import './style.css'
import ArcoVue from '@arco-design/web-vue'
import App from './App.vue'
import '@arco-design/web-vue/dist/arco.css'
import ArcoVueIcon from '@arco-design/web-vue/es/icon'
import router from '@/router/index.js'
import api from '@/api/index.js'
import { Message, Notification } from '@arco-design/web-vue'
import VueCookies from 'vue-cookies'
const app = createApp(App)
app.config.globalProperties.$api = api
app.config.globalProperties.$cookies = VueCookies
Message._context = app._context
Notification._context = app._context
app.use(ArcoVue).use(ArcoVueIcon).use(router).mount('#app')

21
src/router/index.js Normal file
View File

@ -0,0 +1,21 @@
import { createRouter } from 'vue-router'
import * as vueRouter from 'vue-router'
import generatedRoutes from '~pages'
import { setupLayouts } from 'layouts-generated'
const routes = setupLayouts(generatedRoutes)
const router = createRouter({
history: vueRouter.createWebHistory(),
routes
})
console.log(routes)
router.beforeEach((to, from, next) => {
if (to.matched.length) {
next()
} else {
console.log('未找到对应地址')
}
})
export default router

26
src/store/keepAlive.js Normal file
View File

@ -0,0 +1,26 @@
import { defineStore } from 'pinia'
export const keepAliveStore = defineStore('keepAliveStore', {
/** 持久化 **/
persist: true,
state: () => ({ list: [] }),
actions: {
/**
* 添加浏览记录菜单
* @param menu
*/
add(name) {
this.list.includes(name) || this.list.push(name)
},
/**
* 清空浏览记录菜单用户退出时候必须调调用此菜单
*/
remove(name) {
this.list = this.list.filter(v => {
return v !== name
})
}
}
})
export default keepAliveStore

78
src/style.css Normal file
View File

@ -0,0 +1,78 @@
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
background-color: #F8F9FB;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
.card {
padding: 2em;
}
#app {
margin: 0 auto;
text-align: center;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

8
src/utils/index.js Normal file
View File

@ -0,0 +1,8 @@
let timeout = null
function debounce(fn, wait) {
if (timeout !== null) clearTimeout(timeout)
timeout = setTimeout(fn, wait)
}
export default debounce

View File

@ -0,0 +1,148 @@
<template>
<div>
<a-button type="primary" size="small" @click="show = true">
<template #icon>
<icon-plus />
</template>
<template #default> 新增客户 </template>
</a-button>
<a-modal
v-model:visible="show"
:mask-closable="false"
@close="this.$refs.form.resetFields()"
@before-ok="submit"
>
<div>
<a-form auto-label-width :model="form" ref="form">
<a-form-item
label="企业/机构名称"
field="name"
:rules="[
{ required: true, message: '请输入企业/机构名称' }
]"
>
<a-input
placeholder="请输入企业/机构名称"
v-model="form.name"
></a-input>
</a-form-item>
<a-form-item
field="cityCode"
label="所属地区"
:rules="[{ required: true, message: '请选择所属地区' }]"
>
<a-cascader
placeholder="请选择所属地区"
:options="options"
allow-search
v-model="form.cityCode"
:field-names="{
label: 'name',
value: 'code'
}"
/>
</a-form-item>
<a-form-item label="详细地址" field="address">
<a-input
placeholder="请输入详细地址"
v-model="form.address"
></a-input>
</a-form-item>
<a-form-item
field="type"
label="客户类型"
:rules="[{ required: true, message: '请选择类型' }]"
>
<a-select placeholder="请选择类型" v-model="form.type">
<a-option label="业主单位" value="0"></a-option>
<a-option label="实施单位" value="1"></a-option>
<a-option label="其他" value="2"></a-option>
</a-select>
</a-form-item>
<a-form-item
label="业务员"
field="saleManId"
:rules="[{ required: true, message: '请选择业务员' }]"
>
<a-select
:options="userOptions"
allow-search
@search="handleSearch"
v-model="form.saleManId"
placeholder="请选择业务员"
/>
</a-form-item>
<a-form-item label="备注" field="remark">
<a-textarea
placeholder="请输入备注"
v-model="form.remark"
></a-textarea>
</a-form-item>
</a-form>
</div>
</a-modal>
</div>
</template>
<script>
export default {
data() {
return {
show: false,
options: [],
userOptions: [],
form: {
name: '',
cityCode: '',
cityName: '',
address: '',
remark: '',
saleManId: '',
type: ''
}
}
},
mounted() {
this.fetchCity()
},
methods: {
fetchCity() {
this.$api.base.areaTree().then(res => {
if (res.code === 200) {
this.options = res.data
}
})
},
handleSearch(value) {
this.$api.user.list({ name: value }).then(res => {
if (res.code === 200) {
this.userOptions = res.data.map(e => {
return { label: e.name, value: e.id }
})
}
})
},
submit(done) {
this.$refs.form.validate(errors => {
if (errors === undefined) {
console.log(this.form)
this.$api.customer.submit(this.form).then(res => {
if (res.code === 200) {
this.$notification.success(res.msg)
this.$emit('ok')
done()
} else {
this.$notification.error(res.msg)
done(false)
}
})
} else {
done(false)
}
})
}
}
}
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,81 @@
<template>
<div>
<a-button type="text" size="small" @click="show = true">
新增
</a-button>
<a-modal
v-model:visible="show"
@close="this.$refs.form.resetFields()"
@before-ok="submit"
>
<div>
<a-form auto-label-width :model="form" ref="form">
<a-form-item label="名称" required field="name">
<a-input
v-model="form.name"
placeholder="请输入名称"
></a-input>
</a-form-item>
<a-form-item label="排序" required field="sort">
<a-input-number
v-model="form.sort"
placeholder="请输入排序"
></a-input-number>
</a-form-item>
</a-form>
</div>
</a-modal>
</div>
</template>
<script>
export default {
props: {
pid: {
type: String,
default: ''
}
},
watch: {
pid: {
handler(val) {
if (val) {
this.form.pid = val
}
},
immediate: true
}
},
data() {
return {
show: false,
form: {
name: '',
pid: '',
sort: ''
}
}
},
methods: {
submit(done) {
this.$refs.form.validate(errors => {
if (errors === undefined) {
this.$api.base.submit(this.form).then(res => {
if (res.code === 200) {
this.$notification.success(res.msg)
this.$emit('ok')
done()
} else {
this.$notification.error(res.msg)
}
})
} else {
done(false)
}
})
}
}
}
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,94 @@
<template>
<div>
<a-button type="primary" size="small" @click="show = true"
>修改权限
</a-button>
<a-modal v-model:visible="show" @ok="submit">
<div>
<div v-for="(item, index) in roleList">
<div
class="padding flex flex-center flex-justify-between row"
@click="changeRole(index)"
>
<div>{{ item.name }}</div>
<icon-check-circle-fill
v-if="item.checked"
style="color: green"
/>
<icon-check-circle v-else />
</div>
</div>
</div>
</a-modal>
</div>
</template>
<script>
export default {
props: {
userId: {
type: String,
default: ''
},
roleId: {
type: String,
default: ''
}
},
watch: {
show: {
handler(val) {
if (val) {
this.fetchRole()
}
}
}
},
data() {
return {
show: false,
roleList: []
}
},
methods: {
changeRole(index) {
this.roleList = this.roleList.map(e => {
e.checked = false
return e
})
console.log(index)
this.roleList[index].checked = true
},
fetchRole() {
this.$api.sys.roleList({ appId: '7XAp5LZk' }).then(res => {
if (res.code === 200) {
this.roleList = res.data
.filter(e => e.code !== 'super_admin')
.map(e => {
e.checked = e.id === this.roleId
return e
})
}
})
},
submit() {
const role = this.roleList.find(e => e.checked)
const data = { userId: this.userId, roleId: role.id }
this.$api.sys.roleChange(data).then(res => {
if (res.code === 200) {
this.$notification.success(res.msg)
this.$emit('ok')
} else {
this.$notification.error(res.msg)
}
})
}
}
}
</script>
<style lang="scss" scoped>
.row:hover {
background-color: #f0f1f3;
}
</style>

View File

@ -0,0 +1,231 @@
<template>
<div>
<a-button type="text" size="small" @click="show = true"
>{{ info ? '编辑' : '新增' }}
</a-button>
<a-modal
v-model:visible="show"
:mask-closable="false"
width="760px"
@before-ok="submit"
@close="this.$refs.form.resetFields()"
>
<a-form
:model="form"
auto-label-width
ref="form"
:layout="'vertical'"
>
<a-row gutter="16">
<a-col span="12">
<a-form-item
label="工号(登录账号)"
required
field="account"
:disabled="info"
>
<a-input
placeholder="请输入工号(登录账号)"
v-model="form.account"
></a-input>
</a-form-item>
</a-col>
<a-col span="12">
<a-form-item label="登录密码" required field="pwd">
<div class="flex flex-center full-width">
<a-input
placeholder="请输入登录密码"
v-model="form.pwd"
:disabled="info"
></a-input>
<a-button
class="ml-10"
type="primary"
@click="genPwd"
:disabled="info"
>随机密码
</a-button>
</div>
</a-form-item>
</a-col>
</a-row>
<a-row gutter="16">
<a-col span="12">
<a-form-item label="姓名" required field="name">
<a-input
placeholder="请输入姓名"
v-model="form.name"
></a-input>
</a-form-item>
</a-col>
<a-col span="12">
<a-form-item label="性别" required field="sex">
<a-select v-model="form.sex">
<a-option label="男" value="0"></a-option>
<a-option label="女" value="1"></a-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-row gutter="16">
<a-col span="12">
<a-form-item label="联系电话" required field="phone">
<a-input
placeholder="请输入联系电话"
v-model="form.phone"
></a-input>
</a-form-item>
</a-col>
<a-col span="12">
<a-form-item label="所属部门" required field="deptId">
<a-select
v-model="form.deptId"
:options="deptOptions"
>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-row gutter="16">
<a-col span="12">
<a-form-item label="员工岗位" required field="post">
<a-select
placeholder="请选择岗位"
:options="orgPost"
v-model="form.post"
></a-select>
</a-form-item>
</a-col>
<a-col span="12">
<a-form-item label="员工角色" required field="roleId">
<a-select
:options="roleOptions"
v-model="form.roleId"
></a-select>
</a-form-item>
</a-col>
</a-row>
</a-form>
</a-modal>
</div>
</template>
<script>
import { md5 } from 'js-md5'
export default {
props: {
info: {
type: Object,
default: null
},
deptId: {
required: true,
type: String,
default: ''
}
},
watch: {
info: {
handler(val) {
if (val) {
this.form = val
}
},
immediate: true
},
show: {
handler(val) {
if (val) {
let dept = JSON.parse(sessionStorage.getItem('dept'))
this.deptOptions = dept.map(e => {
return { label: e.name, value: e.id }
})
this.fetchDict()
}
}
},
deptId: {
handler(val) {
if (val) {
this.form.deptId = val
}
},
immediate: true
}
},
data() {
return {
show: false,
orgPost: [],
roleOptions: [],
deptOptions: [],
form: {
sex: '0',
appId: '7XAp5LZk',
phone: '',
roleId: '',
account: '',
name: '',
deptId: '',
pwd: '',
orgId: '',
post: ''
}
}
},
mounted() {
const user = JSON.parse(localStorage.getItem('user'))
this.form.orgId = user.orgId
},
methods: {
fetchDict() {
this.$api.sys.dict({ code: 'org_post' }).then(res => {
if (res.code === 200) {
this.orgPost = res.data
}
})
this.$api.sys.roleList({ appId: '7XAp5LZk' }).then(res => {
if (res.code === 200) {
this.roleOptions = res.data.map(e => {
return { label: e.name, value: e.id }
})
}
})
},
genPwd() {
let chars =
'0123456789abcdefghijklmnopqrstuvwxyz!@#$%^&*()ABCDEFGHIJKLMNOPQRSTUVWXYZ'
let passwordLength = 6
let password = ''
for (let i = 0; i <= passwordLength; i++) {
let randomNumber = Math.floor(Math.random() * chars.length)
password += chars.substring(randomNumber, randomNumber + 1)
}
this.form.pwd = password
},
submit(done) {
this.$refs.form.validate(errors => {
if (errors === undefined) {
const data = { ...this.form }
data.pwd = md5(data.pwd)
this.$api.base.userSave(data).then(res => {
if (res.code === 200) {
this.$notification.success(res.msg)
this.$emit('ok')
done()
} else {
this.$notification.error(res.msg)
done(false)
}
})
} else {
done(false)
}
})
}
}
}
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,116 @@
<template>
<div>
<div class="flex flex-center flex-justify-between mb-10">
<div class="flex flex-center flex-justify-start">
<h2 class="bold">
{{ deptName }}
</h2>
<div class="grey-6 ml-10 bold">
共有员工{{ this.page.total }}
</div>
</div>
<add-user :dept-id="deptId" @ok="fetchList" />
</div>
<a-table
:columns="columns"
:data="list"
:pagination="page"
@page-change="pageChange"
>
<template #sex="{ record }">
<a-tag color="blue">
{{ record.sex === '0' ? '男' : '女' }}
</a-tag>
</template>
<template #status="{ record }">
<a-tag color="red" v-if="record.status === 0"> 未激活</a-tag>
<a-tag color="green" v-else-if="record.status === 1">
已激活
</a-tag>
<a-tag color="grey" v-else> 停用/离职</a-tag>
</template>
<template #menu="{ record }">
<div>
<view-user :userId="record.id" @ok="refresh" />
</div>
</template>
</a-table>
</div>
</template>
<script>
import addUser from '@/views/base/components/edit-user.vue'
import viewUser from '@/views/base/components/view-user.vue'
export default {
components: {
addUser,
viewUser
},
props: {
deptName: {
required: true,
type: String,
default: ''
},
deptId: {
required: true,
type: String,
default: ''
}
},
watch: {
deptId: {
handler(val) {
this.fetchAll()
},
immediate: true
}
},
data() {
return {
page: { page: 0, size: 10 },
list: [],
columns: [
{ title: '账号', dataIndex: 'account' },
{ title: '姓名', dataIndex: 'name' },
{ title: '性别', slotName: 'sex' },
{ title: '状态', slotName: 'status' },
{ title: '添加时间', dataIndex: 'createTime' },
{ title: '操作', slotName: 'menu' }
]
}
},
methods: {
refresh() {
this.fetchAll()
},
fetchList() {
this.$api.base
.userList({ deptId: this.deptId, ...this.page })
.then(res => {
if (res.code === 200) {
this.list = res.data.records
this.page.total = res.data.total
}
})
},
fetchAll() {
const data = { deptId: this.deptId, ...this.page }
this.$api.base.userAll(data).then(res => {
if (res.code === 200) {
this.list = res.data.records
this.page.total = res.data.total
}
})
},
pageChange(page) {
this.page.page = page - 1
this.refresh()
}
}
}
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,219 @@
<template>
<div>
<a-button type="text" size="small" @click="show = true">查看</a-button>
<a-drawer v-model:visible="show" width="960px">
<div v-if="user">
<div class="flex flex-center flex-justify-start">
<a-avatar :size="72">
<a-image
alt="avatar"
v-if="user.avatar"
:src="user.avatar"
style="border-radius: 50%"
/>
</a-avatar>
<div class="ml-20">
<div class="flex flex-center flex-justify-between">
<h2>{{ user.name }}</h2>
</div>
<div class="flex flex-justify-start flex-center">
账号状态
<div>
<a-tag color="red" v-if="user.status === 0"
>未激活
</a-tag>
<a-tag
color="green"
v-else-if="user.status === 1"
>正常
</a-tag>
<a-tag color="grey" v-else>停用/离职</a-tag>
</div>
</div>
<div>创建时间{{ user.createTime }}</div>
</div>
</div>
<a-divider />
<div>
<div class="flex flex-center flex-justify-between">
<h2 class="padding-top padding-bottom">基础信息</h2>
<edit-user
:dept-id="user.deptId"
:info="user"
@ok="fetchInfo"
></edit-user>
</div>
<a-descriptions :data="data" bordered />
</div>
<div class="padding-top padding-bottom">
<h2 class="padding-top padding-bottom">修改</h2>
<div class="flex flex-center flex-justify-start">
<a-popconfirm
content="确定重置密码为 000000 "
@ok="restPwd"
>
<a-button type="primary" size="small"
>重置密码
</a-button>
</a-popconfirm>
<a-popconfirm
:content="
`确定` + this.user.status === 1
? '停用'
: '启用' + `当前账号?`
"
@ok="changeStatus"
>
<a-button
class="ml-20"
v-if="user.status === 0"
type="primary"
disabled
size="small"
>
账号未激活
</a-button>
<a-button
v-else
class="ml-20"
type="primary"
size="small"
>
{{
this.user.status === 1
? '停用账号'
: '启用账号'
}}
</a-button>
</a-popconfirm>
<div class="ml-20">
<edit-role
:userId="user.id"
:roleId="user.roleId"
@ok="fetchInfo"
/>
</div>
</div>
</div>
</div>
</a-drawer>
</div>
</template>
<script>
import editUser from '@/views/base/components/edit-user.vue'
import editRole from '@/views/base/components/edit-role.vue'
export default {
components: {
editUser,
editRole
},
props: {
userId: {
type: String,
default: ''
}
},
watch: {
show: {
handler(val) {
if (val) {
this.fetchInfo()
}
}
}
},
data() {
return {
show: false,
user: null,
data: [
{
label: '姓名',
prop: 'name'
},
{
label: '性别',
prop: 'sex'
},
{
label: '电话',
prop: 'phone'
},
{
label: '职务',
prop: 'postName'
},
{
label: '角色',
prop: 'roleName'
}
]
}
},
methods: {
fetchInfo() {
this.$api.user.info({ userId: this.userId }).then(res => {
if (res.code === 200) {
this.user = res.data
this.fetchData(res.data)
} else {
this.$notification.error(res.msg)
}
})
},
fetchData(info) {
//
this.$api.sys.dict({ code: 'org_post' }).then(res => {
if (res.code === 200) {
var tmp = res.data.find(e => e.value === info.post)
if (tmp) {
info.postName = tmp.label
}
this.$api.sys.roleList({ appId: '7XAp5LZk' }).then(res => {
var tmp = res.data.find(e => e.id === info.roleId)
if (tmp) {
info.roleName = tmp.name
}
info.sex = info.sex === '0' ? '男' : '女'
this.data = this.data.map(e => {
const item = {}
item.label = e.label
item.value = info[e.prop]
item.prop = e.prop
return item
})
})
}
})
},
restPwd() {
this.$api.user.retPwd({ userId: this.user.id }).then(res => {
if (res.code === 200) {
this.$notification.success(res.msg)
} else {
this.$notification.error(res.msg)
}
})
},
changeStatus() {
const data = {
userId: this.user.id,
status: this.user.status === 1 ? 2 : 1
}
this.$api.user.changeStatus(data).then(res => {
if (res.code === 200) {
this.$notification.success(res.msg)
this.user.status = data.status
this.$emit('ok')
} else {
this.$notification.error(res.msg)
}
})
}
}
}
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,104 @@
<template>
<div>
<a-table :columns="columns" :data="list">
<template #sex="{ record }">
<a-tag :color="record.sex === '0' ? 'blue' : 'red'"
>{{ record.sex === '0' ? '男' : '女' }}
</a-tag>
</template>
<template #policy="{ record }">
<div>
<a-tag :color="record.policy === '0' ? 'red' : 'green'"
>{{ record.policy === '0' ? '否' : '是' }}
</a-tag>
</div>
</template>
<template #menu="{ record }">
<div>
<edit-contacts
:customer-id="customerId"
:info="record"
@ok="fetchList"
></edit-contacts>
</div>
</template>
</a-table>
</div>
</template>
<script>
import editContacts from '@/views/base/customer/components/edit-contacts.vue'
export default {
components: {
editContacts
},
props: {
customerId: {
required: true,
type: String,
default: ''
}
},
watch: {
customerId: {
handler(val) {
if (val) {
this.fetchList()
}
},
immediate: true
}
},
data() {
return {
page: { page: 0, size: 10, total: 0 },
list: [],
columns: [
{
title: '姓名',
dataIndex: 'name'
},
{
title: '性别',
slotName: 'sex'
},
{
title: '手机号码',
dataIndex: 'phone'
},
{
title: '职务',
dataIndex: 'post'
},
{
title: '关键决策人',
slotName: 'policy'
},
{
title: '备注',
dataIndex: 'remark'
},
{
title: '操作',
slotName: 'menu'
}
]
}
},
methods: {
fetchList() {
const data = { customerId: this.customerId, ...this.page }
this.$api.contacts.page(data).then(res => {
if (res.code === 200) {
this.list = res.data.records
}
})
}
}
}
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,167 @@
<template>
<div>
<a-button type="text" size="small" @click="show = true">查看</a-button>
<a-drawer v-model:visible="show" width="80%" :header="false">
<div v-if="info">
<div class="flex flex-center flex-justify-start">
<h1>{{ info.name }}</h1>
<a-tag size="large" color="red" class="ml-20"
>业务员{{ info.saleMan.name }}
</a-tag>
</div>
<div class="flex flex-center flex-justify-start">
<a-tag color="blue" v-if="city" size="large">
{{ city.provinceName }}{{ city.cityName
}}{{ city.name }}
</a-tag>
<div class="ml-10">
<a-input
v-model="info.address"
size="small"
style="min-width: 680px"
placeholder="详细地址"
:disabled="!editAddr"
>
<template #suffix>
<a-button type="text" size="small" @click="save"
>{{ editAddr ? '保存' : '编辑' }}
</a-button>
</template>
</a-input>
</div>
</div>
<a-divider />
<div class="flex flex-center flex-justify-start">
<edit-contacts
:customer-id="info.id"
@ok="this.$refs.contacts.fetchList()"
/>
<a-button size="small" class="ml-10">
<template #icon>
<icon-code-square />
</template>
创建项目
</a-button>
</div>
<div class="mt-20">
<a-tabs @change="tabChange">
<a-tab-pane
v-for="item in tabs"
:key="item.value"
:title="item.title"
>
</a-tab-pane>
</a-tabs>
<div class="padding" v-if="tabIndex === 0">
<contacts :customer-id="id" ref="contacts" />
</div>
<div
v-else
class="flex flex-center flex-col grey-6"
style="margin-top: 96px"
>
<img
style="width: 100px"
src="https://res.wutongshucloud.com/res/2024/12/09/202412091020938.svg"
/>
<div>正在开发中</div>
</div>
</div>
</div>
</a-drawer>
</div>
</template>
<script>
import Contacts from '@/views/base/customer/components/contacts.vue'
import editContacts from '@/views/base/customer/components/edit-contacts.vue'
export default {
components: { Contacts, editContacts },
props: {
id: {
type: String,
default: ''
}
},
watch: {
show: {
handler(val) {
if (val) {
this.fetchInfo()
}
}
}
},
data() {
return {
info: null,
city: null,
show: false,
editAddr: false,
tabIndex: 0,
tabs: [
{
title: '联系人',
value: 0
},
{
title: '相关项目',
value: 1
},
{
title: '详细信息',
value: 2
}
]
}
},
methods: {
tabChange(res) {
this.tabIndex = res
},
fetchInfo() {
this.$api.customer.info({ customerId: this.id }).then(res => {
if (res.code === 200) {
this.info = res.data
this.fetchCity(this.info.cityCode)
}
})
},
fetchCity(code) {
this.$api.base.areaDetail({ code: code }).then(res => {
if (res.code === 200) {
this.city = res.data
}
})
},
save() {
if (this.editAddr === false) {
this.editAddr = true
} else {
if (this.info.address) {
const data = {
id: this.info.id,
address: this.info.address
}
this.$api.customer.submit(data).then(res => {
if (res.code === 200) {
this.$notification.success(res.msg)
this.editAddr = false
} else {
this.$notification.error(res.msg)
}
})
}
}
}
}
}
</script>
<style lang="scss" scoped>
:deep(.arco-input-wrapper .arco-input[disabled]) {
--color-text-4: #343434;
}
</style>

View File

@ -0,0 +1,154 @@
<template>
<div>
<a-button
v-if="info === null"
size="small"
@click="show = true"
type="primary"
>
<template #icon>
<icon-idcard />
</template>
创建联系人
</a-button>
<a-button v-else size="small" type="text" @click="show = true"
>更新
</a-button>
<a-modal
v-model:visible="show"
@close="this.$refs.form.resetFields()"
@before-ok="submit"
>
<a-form :model="form" ref="form" auto-label-width>
<a-form-item
label="姓名"
field="name"
:rules="[{ required: true, message: '请输入姓名' }]"
>
<a-input
placeholder="请输入姓名"
v-model="form.name"
></a-input>
</a-form-item>
<a-form-item
label="手机"
field="phone"
:rules="[
{ required: true, message: '请输入手机号码' },
{ length: 11, message: '请输入手机号码' }
]"
>
<a-input
placeholder="请输入手机号码"
v-model="form.phone"
></a-input>
</a-form-item>
<a-form-item
label="职务"
field="post"
:rules="[{ required: true, message: '请输入职务' }]"
>
<a-input
placeholder="请输入职务"
v-model="form.post"
></a-input>
</a-form-item>
<a-form-item
label="性别"
field="sex"
:rules="[{ required: true, message: '请选择性别' }]"
>
<a-select placeholder="请选择性别" v-model="form.sex">
<a-option label="男" value="0"></a-option>
<a-option label="女" value="1"></a-option>
</a-select>
</a-form-item>
<a-form-item
label="是否关键决策人"
field="policy"
:rules="[
{ required: true, message: '请选择是否关键决策人' }
]"
>
<a-select
placeolder="请选择"
v-model="form.policy"
placeholder="请选择是否关键决策人"
>
<a-option label="否" value="0"></a-option>
<a-option label="是" value="1"></a-option>
</a-select>
</a-form-item>
<a-form-item label="备注" field="remark">
<a-textarea
v-model="form.remark"
placeholder="请输入备注,例如:爱好"
></a-textarea>
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script>
export default {
props: {
customerId: {
required: true,
type: String,
default: ''
},
info: {
type: Object,
default: null
}
},
watch: {
info: {
handler(val) {
if (val) {
this.form = { ...val }
}
},
immediate: true
}
},
data() {
return {
show: false,
form: {
name: '',
phone: '',
post: '',
sex: '0',
policy: '',
remark: '',
customerId: ''
}
}
},
methods: {
submit(done) {
this.$refs.form.validate(errors => {
if (errors === undefined) {
this.form.customerId = this.customerId
this.$api.contacts.submit(this.form).then(res => {
if (res.code === 200) {
this.$notification.success(res.msg)
this.$emit('ok')
done()
} else {
this.$notification.error(res.msg)
done(false)
}
})
} else {
done(false)
}
})
}
}
}
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,101 @@
<template>
<div>
<navbar title="客户管理" />
<div class="container">
<div class="flex flex-center flex-justify-start">
<add-org @ok="fetchList" />
</div>
<a-table
class="mt-20"
:columns="columns"
:data="list"
:pagination="page"
>
<template #saleMan="{ record }">
<div>
{{ record.saleMan.name }}
</div>
</template>
<template #menu="{ record }">
<div>
<customer-more :id="record.id" />
</div>
</template>
</a-table>
</div>
</div>
</template>
<script>
import navbar from '@/components/navbar/index.vue'
import addOrg from '@/views/base/components/add-org.vue'
import customerMore from '@/views/base/customer/components/customer-more.vue'
export default {
components: {
navbar,
addOrg,
customerMore
},
data() {
return {
page: {
page: 0,
size: 10,
total: 10
},
list: [],
columns: [
{
title: '单位名称',
dataIndex: 'name'
},
{
title: '类型',
dataIndex: 'typeName'
},
{
title: '地区',
dataIndex: 'cityName'
},
{
title: '详细地址',
dataIndex: 'address'
},
{
title: '业务员',
slotName: 'saleMan'
},
{
title: '操作',
slotName: 'menu'
}
]
}
},
mounted() {
this.fetchList()
},
methods: {
fetchList() {
this.$api.customer.page(this.page).then(res => {
if (res.code === 200) {
this.list = res.data.records.map(e => {
e.typeName = '业主单位'
if (e.type === '1') {
e.typeName = '实施单位'
} else if (e.type === '2') {
e.typeName = '其他'
}
return e
})
this.page.tatal = res.data.total
}
})
}
}
}
</script>
<style lang="scss" scoped></style>

101
src/views/base/staff.vue Normal file
View File

@ -0,0 +1,101 @@
<template>
<div>
<navbar title="部门与员工管理" />
<div class="flex flex-center flex-align-start">
<div class="container mr-10" style="flex: 1">
<div
class="flex flex-center flex-justify-between pointer"
v-if="dept"
@click="subDept = null"
>
<h2>
{{ dept.name }}
</h2>
<edit-dept :pid="dept.id" @ok="fetchList" />
</div>
<a-divider />
<div>
<a-empty v-if="list.length === 0"></a-empty>
<div
v-for="item in list"
:key="item.id"
@click="checkDept(item)"
>
<div
class="flex flex-center flex-justify-start padding row pointer"
>
{{ item.name }}
</div>
</div>
</div>
</div>
<div class="container" style="flex: 3" v-if="dept">
<user
:dept-name="subDept ? subDept.name : dept.name"
:dept-id="subDept ? subDept.id : null"
/>
</div>
</div>
</div>
</template>
<script>
import navbar from '@/components/navbar/index.vue'
import editDept from '@/views/base/components/edit-dept.vue'
import user from './components/user.vue'
export default {
components: {
navbar,
editDept,
user
},
data() {
return {
dept: null,
subDept: null,
list: []
}
},
mounted() {
this.fetchDeptInfo()
},
methods: {
fetchDeptInfo() {
const user = JSON.parse(localStorage.getItem('user'))
this.$api.base.info({ orgId: user.deptId }).then(res => {
if (res.code === 200) {
this.dept = res.data
this.fetchList(this.dept.id)
}
})
},
fetchList(pid) {
this.$api.base.list({ pid: pid }).then(res => {
if (res.code === 200) {
this.list = res.data
//
const tmps = [...this.list]
tmps.push(this.dept)
sessionStorage.setItem('dept', JSON.stringify(tmps))
}
})
},
checkDept(item) {
this.subDept = item
console.log(this.subDept)
}
}
}
</script>
<style lang="scss" scoped>
.row {
border-bottom: rgba(0, 0, 0, 0.05) solid 1px;
}
.row:hover {
background-color: #f0f1f3;
}
</style>

View File

@ -0,0 +1,140 @@
<template>
<div>
<a-button
class="mt-20"
type="primary"
size="small"
@click="show = true"
>
<template #icon>
<icon-edit />
</template>
<template #default>创建回款单</template>
</a-button>
<a-modal
v-model:visible="show"
@close="this.$refs.form.resetFields()"
@before-ok="submit"
>
<a-form auto-label-width :model="form" ref="form">
<a-form-item label="回款编号" field="number" required>
<a-input
placeholder="回款编号"
disabled
v-model="form.number"
></a-input>
</a-form-item>
<a-form-item
label="回款金额"
:rules="[{ required: true, message: '请输入回款金额' }]"
field="amount"
>
<a-input-number
v-model="form.amount"
placeholder="请输入回款金额"
></a-input-number>
</a-form-item>
<a-form-item
label="回款日期"
field="payDate"
:rules="[{ required: true, message: '请选择回款日期' }]"
>
<a-date-picker
v-model="form.payDate"
class="full-width"
placeholder="请选择回款日期"
></a-date-picker>
</a-form-item>
<a-form-item label="回款备注">
<a-textarea
v-model="form.remark"
class="full-width"
:auto-size="{ minRows: 5 }"
placeholder="请输入回款备注"
></a-textarea>
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script>
export default {
props: {
info: {
required: true,
type: Object,
default: null
}
},
watch: {
show: {
handler(val) {
if (val) {
this.form.number = this.createordernum()
}
}
}
},
data() {
return {
show: false,
form: {
number: '',
amount: '',
payDate: '',
remark: ''
}
}
},
methods: {
setTimeDateFmt(s) {
//
return s < 10 ? '0' + s : s
},
createordernum() {
const now = new Date()
let month = now.getMonth() + 1
let day = now.getDate()
let hour = now.getHours()
let minutes = now.getMinutes()
let seconds = now.getSeconds()
month = this.setTimeDateFmt(month)
day = this.setTimeDateFmt(day)
hour = this.setTimeDateFmt(hour)
minutes = this.setTimeDateFmt(minutes)
seconds = this.setTimeDateFmt(seconds)
let orderCode =
now.getFullYear().toString() +
month.toString() +
day +
hour +
minutes +
seconds +
Math.round(Math.random() * 1000000).toString()
return orderCode
//+
},
submit(done) {
this.$refs.form.validate(errors => {
if (errors === undefined) {
this.form.contractId = this.info.id
this.$api.contractPay.submit(this.form).then(res => {
if (res.code === 200) {
this.$notification.success(res.msg)
this.$emit('ok')
done()
} else {
this.$notification.error(res.msg)
}
})
} else {
done(false)
}
})
}
}
}
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,137 @@
<template>
<div>
<a-collapse :default-active-key="['1']" accordion>
<a-collapse-item header="基础信息" key="1">
<template #extra>
<edit-contract :info="form" @ok="fetchData" />
</template>
<div v-if="data">
<a-descriptions
:data="data"
bordered
:column="{ xs: 1, md: 3, lg: 4 }"
>>
<a-descriptions-item
v-for="item of data"
:label="item.label"
:key="item.id"
>
<div>{{ item.value }}</div>
</a-descriptions-item>
</a-descriptions>
</div>
</a-collapse-item>
<a-collapse-item header="关联项目" key="2">
<template #extra>
<project-picker :contract-id="info.id" @ok="success" />
</template>
<div>
<a-list :bordered="false" size="small">
<a-list-item v-for="item in projects" :key="item.id">
<div
class="flex flex-center flex-justify-between"
v-if="item"
>
<div>{{ item.name }}</div>
<div>
<more-info :info="item" />
</div>
</div>
</a-list-item>
</a-list>
</div>
</a-collapse-item>
</a-collapse>
</div>
</template>
<script>
import editContract from '@/views/contract/components/edit-contract.vue'
import projectPicker from '@/views/contract/components/project-picker.vue'
import moreInfo from '@/views/project/index/components/more-info.vue'
export default {
components: {
editContract,
projectPicker,
moreInfo
},
props: {
info: {
type: Object,
default: null
}
},
watch: {
info: {
handler(val) {
if (val) {
this.form = val
this.projects = val.projects
}
},
immediate: true
},
form: {
handler(val) {
this.fetchData(val)
},
immediate: true
}
},
data() {
return {
form: null,
projects: [],
data: [
{
label: '合同开始日期',
prop: 'startDate'
},
{
label: '合同结束日期',
prop: 'endDate'
},
{
label: '客户签约人',
prop: 'customerContact'
},
{
label: '公司签约人',
prop: 'signatory'
},
{
label: '合同备注',
prop: 'remark'
}
]
}
},
methods: {
success(projects) {
this.projects = projects
},
fetchData(val) {
console.log('base-ok')
this.data = this.data.map(e => {
if (e.prop === 'customerContact') {
e.value = val.customerContact.name
} else if (e.prop === 'signatory') {
e.value = val.signatory ? val.signatory.name : ''
} else {
e.value = val[e.prop]
}
return e
})
this.form = val
this.$emit('ok', val)
}
}
}
</script>
<style lang="scss" scoped>
:deep(.arco-collapse-item-content) {
background-color: white;
}
</style>

View File

@ -0,0 +1,297 @@
<template>
<div>
<a-button type="primary" @click.stop="show = true"
>{{ info ? '编辑' : '新增合同' }}
</a-button>
<a-modal
v-model:visible="show"
width="960px"
title="新增合同"
title-align="start"
@close="reset"
@before-ok="submit"
>
<a-form auto-label-width :model="form" ref="form">
<a-row :gutter="16">
<a-col :span="12">
<a-form-item
label="合同编号"
field="number"
:rules="[
{ required: true, message: '请输入合同编号' }
]"
>
<a-input
placeholder="请输入合同编号"
v-model="form.number"
></a-input>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item
field="person"
label="负责人"
:rules="[
{ required: true, message: '请选择合同负责人' }
]"
>
<a-select
:options="userOptions"
allow-search
@search="handleSearch"
@focusin="handleFocus"
placeholder="请选择合同负责人"
v-model="form.person"
></a-select>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item
label="合同名称"
field="name"
:rules="[
{ required: true, message: '请输入合同名称' }
]"
>
<a-input
placeholder="请输入合同名称"
v-model="form.name"
></a-input>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item
label="合同客户"
field="customerId"
:rules="[
{ required: true, message: '请选择合同客户' }
]"
>
<a-select
allow-search
:options="customerOptions"
@search="handleCustomer"
v-model="form.customerId"
placeholder="请选择合同客户"
></a-select>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item
label="合同金额"
field="amount"
:rules="[
{ required: true, message: '请输入合同金额' }
]"
>
<a-input-number
v-model="form.amount"
placeholder="请输入合同金额"
></a-input-number>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="签订时间" field="signDate">
<a-date-picker
v-model="form.signDate"
class="full-width"
placeholder="请选择签订时间"
></a-date-picker>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="合同开始日期" field="startDate">
<a-date-picker
class="full-width"
v-model="form.startDate"
placeholder="请选择合同开始日期"
></a-date-picker>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="合同结束日期" field="endDate">
<a-date-picker
v-model="form.endDate"
class="full-width"
placeholder="请选择合同结束日期"
></a-date-picker>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item
label="客户签约人"
field="customerContactId"
>
<a-select
allow-search
:options="customerContactsOptions"
@search="handleContacts"
v-model="form.customerContactId"
placeholder="请选择客户签约人"
></a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="公司签约人" field="signatory">
<a-select
v-model="form.signatory"
allow-search
:options="signOptions"
@search="handleSign"
placeholder="请选择公司签约人"
></a-select>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="24">
<a-form-item label="合同备注" field="remark">
<a-textarea
v-model="form.remark"
:auto-size="{ minRows: 5 }"
placeholder="请输入合同备注"
></a-textarea>
</a-form-item>
</a-col>
</a-row>
</a-form>
</a-modal>
</div>
</template>
<script>
export default {
props: {
info: {
type: Object,
default: null
}
},
watch: {
show: {
handler(val) {
if (val && this.info) {
this.form = { ...this.info }
this.handleSearch(this.form.person.name)
this.form.person = this.form.person.id
this.handleCustomer(this.form.customer.name)
this.form.customerId = this.form.customer.id
if (this.form.signatory) {
this.handleSign(this.form.signatory.name)
this.form.signatory = this.form.signatory.id
}
if (this.form.customerContact) {
this.handleContacts(this.form.customerContact.name)
this.form.customerContactId =
this.form.customerContact.id
}
}
}
}
},
data() {
return {
show: false,
form: {
number: '',
person: '',
name: '',
customerId: '',
amount: '',
signDate: '',
startDate: '',
endDate: '',
customerContactId: '',
signatory: '',
remark: ''
},
userOptions: [],
signOptions: [],
customerOptions: [],
customerContactsOptions: []
}
},
methods: {
//
handleSearch(value) {
this.$api.user.list({ name: value }).then(res => {
if (res.code === 200) {
this.userOptions = res.data.map(e => {
return { label: e.name, value: e.id }
})
}
})
},
handleSign(value) {
this.$api.user.list({ name: value }).then(res => {
if (res.code === 200) {
this.signOptions = res.data.map(e => {
return { label: e.name, value: e.id }
})
}
})
},
handleContacts(value) {
this.$api.contacts
.list({ name: value, customerId: this.form.customerId })
.then(res => {
if (res.code === 200) {
this.customerContactsOptions = res.data.map(item => {
return { label: item.name, value: item.id }
})
}
})
},
handleCustomer(value) {
this.$api.customer.all({ name: value }).then(res => {
if (res.code === 200) {
this.customerOptions = res.data.map(e => {
return { label: e.name, value: e.id }
})
}
})
},
handleFocus() {
this.handleSearch('')
},
reset() {
if (!this.form.id) {
this.$refs.form.resetFields()
}
},
submit(done) {
this.$refs.form.validate(errors => {
if (errors === undefined) {
this.$api.contract.submit(this.form).then(res => {
if (res.code === 200) {
this.$notification.success(res.msg)
this.$emit('ok', res.data)
done()
} else {
this.$notification.error(res.msg)
done(false)
}
})
} else {
done(false)
}
})
}
}
}
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,129 @@
<template>
<div>
<div class="mb-20 flex flex-center flex-justify-between">
<div>
<a-input
style="width: 360px"
placeholder="请输入文件名称"
allow-clear
v-model="name"
@clear="fetchList"
></a-input>
<a-button type="primary" class="ml-20" @click="fetchList"
>搜索</a-button
>
</div>
<upload @ok="uplodSucc" />
</div>
<a-table
:columns="columns"
:data="list"
:pagination="page"
@page-change="pageChange"
>
<template #user="{ record }">
<div>
{{ record.user.name }}
</div>
</template>
<template #menu="{ record }">
<preview :file-id="record.fileId" />
</template>
</a-table>
</div>
</template>
<script>
import upload from '@/components/upload/index.vue'
import preview from '@/components/preview/index.vue'
export default {
props: {
info: {
required: true,
type: Object,
default: null
}
},
components: {
upload,
preview
},
watch: {
info: {
handler(val) {
if (val) {
this.fetchList()
}
},
immediate: true
}
},
data() {
return {
name: '',
page: {
page: 0,
size: 10,
total: 0
},
columns: [
{
title: '文件名称',
dataIndex: 'name'
},
{
title: '上传人',
slotName: 'user'
},
{
title: '上传时间',
dataIndex: 'createTime'
},
{
title: '操作',
slotName: 'menu'
}
],
list: []
}
},
methods: {
pageChange(page) {
this.page.page = page - 1
this.fetchList()
},
fetchList() {
const data = {
contractId: this.info.id,
name: this.name,
...this.page
}
this.$api.contractFile.page(data).then(res => {
if (res.code === 200) {
this.list = res.data.records
this.page.total = res.data.total
}
})
},
uplodSucc(res) {
const data = {
fileId: res.id,
name: res.fileName,
contractId: this.info.id
}
this.$api.contractFile.submit(data).then(res => {
if (res.code === 200) {
this.fetchList()
this.$notification.success(res.msg)
} else {
this.$notification.error(res.msg)
}
})
}
}
}
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,184 @@
<template>
<div>
<a-button type="text" @click="show = true">更多</a-button>
<a-drawer
width="80%"
v-model:visible="show"
:header="false"
:footer="false"
>
<div v-if="form">
<div>
<div class="grey-6 bold-500">
合同编号{{ form.number }}
</div>
<div class="flex flex-center flex-justify-start">
<div class="font-24 bold">{{ form.name }}</div>
<div class="ml-20">
<a-dropdown @select="changeStatus">
<a-button
type="primary"
size="small"
:style="
`background-color:` + form.status.remark
"
>{{ form.status.label }}
</a-button>
<template #content>
<a-doption
v-for="item in dict"
:key="item.id"
:value="item"
>{{ item.label }}
</a-doption>
</template>
</a-dropdown>
</div>
</div>
</div>
<div class="mt-20">
<a-descriptions :data="data" bordered>
<a-descriptions-item
v-for="item of data"
:label="item.label"
:key="item.id"
>
<div>{{ item.value }}</div>
</a-descriptions-item>
</a-descriptions>
</div>
<div>
<a-tabs
class="mt-10"
@change="tabChange"
:active-key="tabIndex"
>
<a-tab-pane title="详细信息" key="0"></a-tab-pane>
<a-tab-pane title="合同附件" key="1"></a-tab-pane>
<a-tab-pane title="回款记录" key="2"></a-tab-pane>
<a-tab-pane title="操作记录" key="5"></a-tab-pane>
</a-tabs>
<xbase v-if="tabIndex === 0" :info="form" @ok="update" />
<files v-else-if="tabIndex === 1" :info="form" />
<pay-log v-else-if="tabIndex === 2" :info="form" />
<div
v-else
class="flex flex-center flex-col grey-6"
style="margin-top: 96px"
>
<img
style="width: 100px"
src="https://res.wutongshucloud.com/res/2024/12/09/202412091020938.svg"
/>
<div>正在开发中</div>
</div>
</div>
</div>
</a-drawer>
</div>
</template>
<script>
import xbase from '@/views/contract/components/base.vue'
import files from '@/views/contract/components/files.vue'
import payLog from '@/views/contract/components/pay-log.vue'
export default {
components: {
xbase,
files,
payLog
},
props: {
info: {
type: Object,
default: null
}
},
watch: {
show: {
handler(val) {
if (val) {
this.form = this.info
this.init()
} else {
this.tabIndex = 0
}
}
}
},
data() {
return {
tabIndex: 0,
show: false,
form: null,
data: [
{
label: '客户名称',
prop: 'customer'
},
{
label: '合同金额(元)',
prop: 'amount'
},
{
label: '签订时间',
prop: 'signDate'
},
{
label: '回款金额',
prop: 'payAmount'
},
{
label: '负责人',
prop: 'person'
}
],
dict: []
}
},
methods: {
changeStatus(res) {
this.form.status = res
const data = { id: this.form.id, status: this.form.status.value }
this.$api.contract.submit(data).then(res => {
if (res.code === 200) {
this.$notification.success(res.msg)
} else {
this.$notification.error(res.msg)
}
})
},
tabChange(index) {
this.tabIndex = Number.parseInt(index)
},
update(val) {
this.form = val
this.init()
},
init() {
const tmp = JSON.parse(
sessionStorage.getItem('dict_contract_status')
)
if (tmp) {
this.dict = tmp
}
// data
this.data = this.data.map(item => {
if (item.prop === 'customer') {
item.value = this.form.customer.name
} else if (item.prop === 'person') {
item.value = this.form.person.name
} else {
item.value = this.form[item.prop]
}
return item
})
console.log(this.data)
}
}
}
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,199 @@
<template>
<div>
<a-button type="text" size="small" @click="show = true">更多</a-button>
<a-drawer v-model:visible="show" width="80%" :header="false">
<div v-if="form">
<div>
<div>
<div>
{{ form.user.name }} 创建于 {{ form.createTime }}
</div>
</div>
<div class="flex flex-center flex-justify-start">
<div class="font-24 bold">{{ form.number }}</div>
<div class="ml-20">
<a-dropdown @select="changeStatus">
<a-button
type="primary"
size="small"
:style="
`background-color:` + form.status.remark
"
>{{ form.status.label }}
</a-button>
<template #content>
<a-doption
v-for="item in dict"
:key="item.id"
:value="item"
>{{ item.label }}
</a-doption>
</template>
</a-dropdown>
</div>
</div>
</div>
<a-descriptions
:data="data"
class="mt-20"
bordered
:column="{ xs: 1, md: 3, lg: 4 }"
>>
<a-descriptions-item
v-for="item of data"
:label="item.label"
:key="item.id"
>
<div>{{ item.value }}</div>
</a-descriptions-item>
</a-descriptions>
</div>
<div class="font-18 bold mt-20">附件</div>
<a-divider />
<div class="mb-20 mt-20">
<upload @ok="success" />
</div>
<a-table :columns="columns" :data="list">
<template #user="{ record }">
<div>
{{ record.user.name }}
</div>
</template>
<template #menu="{ record }">
<div>
<preview :file-id="record.fileId" />
</div>
</template>
</a-table>
</a-drawer>
</div>
</template>
<script>
import upload from '@/components/upload/index.vue'
import preview from '@/components/preview/index.vue'
export default {
components: {
upload,
preview
},
props: {
info: {
required: true,
type: Object,
default: null
}
},
watch: {
show: {
handler(val) {
if (val) {
this.fetchList()
}
}
},
info: {
handler(val) {
if (val) {
this.form = val
this.dict = JSON.parse(
sessionStorage.getItem('contract_pay_log_status')
)
this.data = this.data.map(e => {
if (e.prop === 'user') {
e.value = this.form.user.name
} else {
e.value = this.form[e.prop]
}
return e
})
}
},
immediate: true
}
},
data() {
return {
page: {
page: 0,
size: 10
},
show: false,
form: null,
dict: [],
tabIndex: 0,
list: [],
columns: [
{
title: '文件名称',
dataIndex: 'name'
},
{
title: '上传人',
slotName: 'user'
},
{
title: '上传时间',
dataIndex: 'createTime'
},
{
title: '操作',
slotName: 'menu'
}
],
data: [
{
label: '回款金额',
prop: 'amount'
},
{
label: '回款日期',
prop: 'payDate'
},
{
label: '创建人',
prop: 'user'
},
{
label: '创建日期',
prop: 'createTime'
},
{
label: '备注',
prop: 'remark'
}
]
}
},
methods: {
fetchList() {
const data = { contractId: this.info.id, ...this.page, tag: 1 }
this.$api.contractFile.page(data).then(res => {
if (res.code === 200) {
this.list = res.data.records
this.page.total = res.data.total
}
})
},
success(res) {
const data = {
fileId: res.id,
name: res.fileName,
contractId: this.info.id,
tag: 1
}
this.$api.contractFile.submit(data).then(res => {
if (res.code === 200) {
this.fetchList()
this.$notification.success(res.msg)
} else {
this.$notification.error(res.msg)
}
})
}
}
}
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,138 @@
<template>
<div>
<div>
<add-pay :info="info" @ok="fetchList" />
</div>
<a-table :columns="columns" :data="list" class="mt-20">
<template #user="{ record }">
<div>
{{ record.user.name }}
</div>
</template>
<template #status="{ record }">
<a-tag :color="record.status.remark">
{{ record.status.label }}
</a-tag>
</template>
<template #menu="{ record }">
<div>
<pay-log-info :info="record" />
</div>
</template>
</a-table>
</div>
</template>
<script>
import payLogInfo from '@/views/contract/components/pay-log-info.vue'
import addPay from '@/views/contract/components/add-pay.vue'
export default {
components: {
addPay,
payLogInfo
},
props: {
info: {
required: true,
type: Object,
default: null
}
},
watch: {
info: {
handler(val) {
if (val) {
this.fetchList()
}
},
immediate: true
}
},
data() {
return {
page: {
page: 0,
size: 10,
total: 0
},
columns: [
{
title: '回款编号',
dataIndex: 'number'
},
{
title: '回款金额',
dataIndex: 'amount'
},
{
title: '回款日期',
dataIndex: 'payDate'
},
{
title: '创建人',
slotName: 'user'
},
{
title: '创建时间',
dataIndex: 'createTime'
},
{
title: '备注',
dataIndex: 'remark'
},
{
title: '状态',
slotName: 'status'
},
{
title: '操作',
slotName: 'menu'
}
],
list: []
}
},
methods: {
fetchList() {
const data = { contractId: this.info.id, ...this.page }
this.$api.contractPay.page(data).then(res => {
if (res.code === 200) {
this.list = res.data.records
this.page.total = res.data.total
this.fetchDict()
}
})
},
fetchDict() {
var tmp = sessionStorage.getItem('contract_pay_log_status')
if (tmp) {
const dict = JSON.parse(tmp)
this.list = this.list.map(e => {
e.status = dict.find(sub => sub.value === e.status)
return e
})
return
}
this.$api.sys
.dict({ code: 'contract_pay_log_status' })
.then(res => {
if (res.code === 200) {
const dict = res.data
sessionStorage.setItem(
'contract_pay_log_status',
JSON.stringify(dict)
)
this.list = this.list.map(e => {
e.status = dict.find(sub => sub.value === e.status)
return e
})
}
})
}
}
}
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,96 @@
<template>
<div>
<a-button type="primary" @click.stop="show = true">关联项目</a-button>
<a-modal
v-model:visible="show"
width="780px"
@close="reset"
@before-ok="update"
>
<a-table :columns="columns" :data="list" @row-click="checked">
<template #name="{ record }">
<div class="flex flex-center flex-justify-between">
<div>
{{ record.name }}
</div>
<div>
<icon-check-circle-fill v-if="record.checked" />
<icon-check-circle v-else class="green" />
</div>
</div>
</template>
</a-table>
</a-modal>
</div>
</template>
<script>
export default {
props: {
contractId: {
type: String,
default: ''
}
},
watch: {
show: {
handler(val) {
this.fetchList()
}
}
},
data() {
return {
show: false,
columns: [
{
title: '项目名称',
slotName: 'name'
}
],
list: [],
page: { page: 0, size: 10 }
}
},
methods: {
reset() {
this.list = this.list.map(e => {
e.checked = false
return e
})
},
checked(res) {
const index = this.list.findIndex(item => item.id === res.id)
this.list[index].checked = !this.list[index].checked
},
fetchList() {
this.$api.project.page(this.page).then(res => {
if (res.code === 200) {
this.list = res.data.records.map(e => {
e.checked = false
return e
})
}
})
},
update(done) {
const data = {
id: this.contractId,
projectIds: this.list.map(e => e.id).join(',')
}
this.$api.contract.submit(data).then(res => {
if (res.code === 200) {
this.$notification.success(res.msg)
this.$emit('ok', res.data.projects)
done()
} else {
this.$notification.error(res.msg)
done(false)
}
})
}
}
}
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,145 @@
<template>
<div>
<navbar title="合同库" />
<div class="container">
<div class="flex flex-center flex-justify-between">
<div class="flex flex-center flex-justify-start">
<a-input
placeholder="请输入合同名称"
style="width: 380px"
allow-clear
@clear="fetchList"
v-model="name"
></a-input>
<a-button class="ml-10" type="primary" @click="fetchList"
>搜索
</a-button>
</div>
<div>
<edit-contract @ok="fetchList" />
</div>
</div>
<a-table
class="mt-20"
:columns="columns"
:data="list"
:pagination="page"
@page-change="pageChange"
>
<template #customer="{ record }">
<div>
<div>{{ record.customer.name }}</div>
</div>
</template>
<template #status="{ record }">
<div v-if="record.status.label">
<a-tag :color="record.status.remark">
{{ record.status.label }}
</a-tag>
</div>
</template>
<template #menu="{ record }">
<div>
<more :info="record" />
</div>
</template>
</a-table>
</div>
</div>
</template>
<script>
import navbar from '@/components/navbar/index.vue'
import editContract from '@/views/contract/components/edit-contract.vue'
import more from '@/views/contract/components/more.vue'
export default {
components: {
navbar,
editContract,
more
},
data() {
return {
page: {
page: 0,
pageSize: 10
},
name: '',
list: [],
columns: [
{
title: '合同编号',
dataIndex: 'number'
},
{
title: '合同名称',
dataIndex: 'name'
},
{
title: '合同甲方',
slotName: 'customer'
},
{
title: '合同金额',
dataIndex: 'amountStr'
},
{
title: '合同状态',
slotName: 'status'
},
{
title: '操作',
slotName: 'menu'
}
]
}
},
mounted() {
this.fetchList()
},
methods: {
pageChange(page) {
this.page.page = page - 1
this.fetchList()
},
fetchList() {
const data = { name: this.name, ...this.page }
this.$api.contract.page(data).then(res => {
if (res.code === 200) {
this.list = res.data.records
this.fetchDict()
}
})
},
fetchDict() {
var tmp = sessionStorage.getItem('dict_contract_status')
if (tmp) {
const dict = JSON.parse(tmp)
this.list = this.list.map(item => {
item.status = dict.find(e => e.value === item.status)
item.amountStr = item.amount.toLocaleString()
return item
})
return
}
this.$api.sys.dict({ code: 'contract_status' }).then(res => {
if (res.code === 200) {
const dict = res.data
sessionStorage.setItem(
'dict_contract_status',
JSON.stringify(dict)
)
this.list = this.list.map(item => {
item.status = dict.find(e => e.value === item.status)
item.amountStr = item.amount.toLocaleString()
return item
})
}
})
}
}
}
</script>
<style lang="scss" scoped></style>

96
src/views/home/index.vue Normal file
View File

@ -0,0 +1,96 @@
<template>
<div>
<navbar title="首页" />
<div class="container">
<div class="flex flex-center flex-justify-start">
<icon-notification size="24" style="color: grey" />
<div class="text-left ml-20">
超级管理员给你分配了
普洱市镇沅县城及周边污水综合治理建设项目
实施方案编制的任务请尽快查看并处理
</div>
</div>
</div>
<div class="container mt-20">
<div class="flex flex-center flex-justify-between">
<div class="bold">数据统计</div>
<div class="grey-6 font-12">数据统计时间2024-12-09</div>
</div>
<div
class="flex flex-center flex-justify-around"
style="margin-top: 40px"
>
<a-statistic
title="项目总数"
:value="239"
:value-style="{ color: 'red' }"
>
<template #prefix>
<icon-arrow-rise />
</template>
<template #suffix></template>
</a-statistic>
<a-statistic
title="我参与的"
:value="50.52"
:value-style="{ color: 'red' }"
>
<template #prefix>
<icon-arrow-rise />
</template>
<template #suffix></template>
</a-statistic>
<a-statistic
title="任务总数"
:value="50"
:value-style="{ color: 'green' }"
>
<template #prefix>
<icon-arrow-rise />
</template>
<template #suffix></template>
</a-statistic>
<a-statistic
title="我的任务"
:value="10"
:value-style="{ color: 'red' }"
>
<template #prefix>
<icon-arrow-rise />
</template>
<template #suffix></template>
</a-statistic>
</div>
</div>
<div class="mt-20 flex flex-center">
<div class="container flex-child-average mr-10">
<div class="text-left bold-500">待办事项</div>
</div>
<div class="container flex-child-average ml-10">
<div class="text-left bold-500">日程安排</div>
</div>
</div>
</div>
</template>
<route>
{
path: '/',
name: '首页',
}
</route>
<script>
import navbar from '@/components/navbar/index.vue'
export default {
components: {
navbar
}
}
</script>
<style lang="scss" scoped></style>

117
src/views/login/index.vue Normal file
View File

@ -0,0 +1,117 @@
<template>
<div class="bg full-screen flex flex-center">
<div class="login-windows flex flex-center">
<div>
<img
style="
height: 460px;
border-top-left-radius: 8px;
border-bottom-left-radius: 8px;
"
src="https://res.wutongshucloud.com/res/2024/12/05/202412051152453.png"
/>
</div>
<div style="padding: 32px">
<h1 style="padding-bottom: 32px">👏 欢迎使用梧桐项目云</h1>
<a-form :model="form" auto-label-width>
<a-form-item style="width: 360px" label="用户账号">
<a-input
v-model="form.account"
placeholder="请输入用户账号"
></a-input>
</a-form-item>
<a-form-item style="width: 360px" label="登录密码">
<a-input
v-model="form.pwd"
type="password"
placeholder="请输入登录密码"
@press-enter="login"
></a-input>
</a-form-item>
</a-form>
<a-button
type="primary"
style="width: 360px; margin-top: 16px"
block
@click="login"
>登录
</a-button>
<div class="grey-6 mt-20 font-14">
开通账号及使用问题请咨询15587166921
</div>
</div>
</div>
<div class="bottom">
<div>
Copyright © 2019-{{ new Date().getFullYear() }}
梧凤桐凰规划研究院云南有限公司 版权所有
</div>
<div class="mt-10">All Rights Reserved.</div>
</div>
</div>
</template>
<route>
{
meta: {
layout: 'empty',
}
}
</route>
<script>
export default {
data() {
return {
form: {
account: '',
pwd: ''
},
redirect: ''
}
},
mounted() {
this.redirect = this.$route.query.redirect
},
methods: {
login() {
if (this.form.account && this.form.pwd) {
const data = { ...this.form }
this.$api.user.login(data).then(res => {
if (res.code === 200) {
const user = res.data
localStorage.setItem('user', JSON.stringify(user))
localStorage.setItem("token", user.token)
this.$router.push(this.redirect ? this.redirect : '/')
} else {
this.$notification.error(res.msg)
}
})
} else {
this.$notification.error('请按要求填写内容')
}
}
}
}
</script>
<style lang="scss" scoped>
.bg {
background-image: url('https://wutong-1302848345.cos.ap-chengdu.myqcloud.com/wtzx/7667edec62f44063a50c66e8654eaa87.png');
background-repeat: no-repeat;
background-size: cover;
}
.login-windows {
background-color: white;
border-radius: 8px;
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2);
}
.bottom {
position: fixed;
bottom: 80px;
color: white;
}
</style>

View File

@ -0,0 +1,76 @@
<template>
<div>
<navbar title="通知中心">
<template #right>
<a-button type="text" @click="readAll">全部已读</a-button>
</template>
</navbar>
<div class="container">
<a-list :bordered="false" :paginationProps="paginationProps">
<a-list-item v-for="item in list" class="grey-6">
<div class="flex flex-center flex-justify-start flex-col">
<div
class="flex flex-center full-width flex-justify-between"
>
<div
class="bold"
:class="item.status === 0 ? 'black' : 'grey'"
>
{{ item.title }}
</div>
<div>{{ item.createTime }}</div>
</div>
<div
class="flex flex-center full-width flex-justify-start mt-5"
>
{{ item.content }}
</div>
</div>
</a-list-item>
</a-list>
</div>
</div>
</template>
<script>
import navbar from '@/components/navbar/index.vue'
export default {
components: {
navbar
},
data() {
return {
list: [],
paginationProps: {
defaultPageSize: 10,
total: 0,
page: 0,
pageSize: 10
}
}
},
mounted() {
this.fetchList()
},
methods: {
fetchList() {
this.$api.notice.page(this.paginationProps).then(res => {
if (res.code === 200) {
this.list = res.data.records
this.data.page.total = res.data.total
}
})
},
readAll() {
this.$api.notice.readAll().then(res => {
if (res.code === 200) {
this.fetchList()
}
})
}
}
}
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,114 @@
<template>
<div>
<a-button class="ml-10" type="primary" size="small" @click="show = true"
>选择联系人
</a-button>
<a-modal v-model:visible="show" width="780px" @before-ok="submit">
<div class="flex flex-center flex-justify-between">
<div class="flex flex-center flex-justify-start">
<a-input
placeholder="请输入联系人姓名搜索"
style="width: 280px"
size="small"
></a-input>
<a-button type="primary" class="ml-20" size="small"
>搜索
</a-button>
</div>
<edit-contacts :customer-id="customerId" @ok="fetchList" />
</div>
<a-table
class="mt-20"
:columns="columns"
:data="list"
@row-click="checked"
>
<template #name="{ record }">
<div>{{ record.name }} {{ record.phone }}</div>
</template>
<template #menu="{ record }">
<div>
<icon-check-circle-fill v-if="record.checked" />
<icon-check-circle v-else class="green" />
</div>
</template>
</a-table>
</a-modal>
</div>
</template>
<script>
import editContacts from '@/views/base/customer/components/edit-contacts.vue'
export default {
components: {
editContacts
},
props: {
customerId: {
required: true,
type: String,
default: ''
}
},
watch: {
show: {
handler(val) {
if (val) {
this.fetchList()
}
}
}
},
data() {
return {
page: {
page: 0,
size: 10,
total: 0
},
show: false,
list: [],
checkList: [],
columns: [
{
title: '姓名',
slotName: 'name'
},
{
title: '操作',
slotName: 'menu'
}
]
}
},
methods: {
checked(row) {
row.checked = !row.checked
if (row.checked) {
this.checkList.push(row)
} else {
this.checkList = this.checkList.filter(e => e.id !== row.id)
}
},
fetchList() {
const data = { customerId: this.customerId, ...this.page }
this.$api.contacts.page(data).then(res => {
if (res.code === 200) {
this.list = res.data.records.map(e => {
e.checked = false
return e
})
this.page.total = res.data.total
}
})
},
submit(done) {
this.$emit('ok', this.checkList)
done()
}
}
}
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,206 @@
<template>
<div>
<a-button type="primary" size="small" @click="show = true">
<template #icon>
<icon-upload />
</template>
<template #default> 上传附件</template>
</a-button>
<a-modal
width="860px"
v-model:visible="show"
:mask-closable="false"
:title="`已经选择(` + checkList.length + `)个附件`"
:title-align="'start'"
@before-ok="submit"
@close="reset"
>
<div class="flex flex-center flex-justify-between">
<div class="flex flex-center flex-justify-start">
<a-input
placeholder="请输入文件名称"
style="max-width: 380px"
></a-input>
<a-button class="ml-10" type="primary">搜索</a-button>
</div>
<div>
<a-button-group>
<a-button type="outline" size="small" @click="goHome">
<template #icon>
<icon-home />
</template>
<template #default> 首页</template>
</a-button>
<a-button type="outline" size="small" @click="goBack">
<template #icon>
<icon-nav />
</template>
<template #default> 上一级</template>
</a-button>
<upload @ok="upload" />
</a-button-group>
</div>
</div>
<a-table
class="mt-20"
:columns="columns"
:data="list"
size="small"
:pagination="page"
@page-change="pageChange"
@row-click="rowClick"
>
<template #name="{ record }">
<div class="flex flex-center flex-justify-start">
<img
v-if="record.type === 1"
src="https://res.wutongshucloud.com/res/2024/12/04/202412041635423.svg"
style="width: 30px; height: 30px"
/>
<img
v-else
src="https://res.wutongshucloud.com/res/2024/12/04/202412041641902.svg"
style="width: 20px; height: 20px; padding-left: 5px"
/>
<div class="ml-10">
{{ record.name }}
</div>
</div>
</template>
<template #menu="{ record }">
<div
v-if="record.type === 0"
style="width: 80px"
class="flex flex-center flex-justify-start"
>
<icon-check-circle-fill v-if="record.checked" />
<icon-check-circle v-else class="green" />
</div>
</template>
</a-table>
</a-modal>
</div>
</template>
<script>
import upload from '@/components/upload/index.vue'
export default {
components: {
upload
},
props: {
projectId: {
required: true,
type: String,
default: ''
}
},
watch: {
show: {
handler(val) {
if (val) {
this.fetchList()
}
}
}
},
data() {
return {
show: false,
pid: '0',
page: {
page: 0,
pageSize: 10,
total: 0
},
list: [],
checkList: [],
history: ['0'],
columns: [
{
title: '名称',
slotName: 'name'
},
{
title: '选择',
slotName: 'menu'
}
]
}
},
methods: {
rowClick(row) {
if (row.type === 0) {
row.checked = !row.checked
if (row.checked) {
this.checkList.push(row)
} else {
this.checkList = this.checkList.filter(e => e.id !== row.id)
}
} else {
this.pid = row.id
this.history.push(row.id)
this.fetchList()
}
},
fetchList() {
const data = {
projectId: this.projectId,
pid: this.pid,
...this.page
}
this.$api.projectFile.page(data).then(res => {
if (res.code === 200) {
this.list = res.data.records.map(e => {
e.checked = false
return e
})
this.page.total = res.data.total
}
})
},
submit(done) {
this.$emit('ok', this.checkList)
done()
},
goHome() {
this.history = ['0']
this.pid = '0'
this.fetchList()
},
goBack() {
var size = this.history.length
if (size > 1) {
this.history = this.history.slice(0, size - 1)
this.pid = this.history[this.history.length - 1]
this.fetchList()
} else {
this.$notification.info('已经回到首页')
}
},
pageChange(page) {
this.page.page = page - 1
this.fetchList()
},
upload(file) {
const data = { projectId: this.projectId, pid: this.pid, ...file }
this.$api.projectFile.submit(data).then(res => {
if (res.code === 200) {
this.fetchList()
this.$notification.success(res.msg)
} else {
this.$notification.error(res.msg)
}
})
},
reset() {
this.checkList.length = 0
this.history = ['0']
this.pid = '0'
}
}
}
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,85 @@
<template>
<div>
<a-button type="outline" size="small" @click="showAdd">
<template #icon>
<icon-folder-add />
</template>
<template #default> 创建文件夹</template>
</a-button>
<a-modal
v-model:visible="show"
@close="this.$refs.form.resetFields()"
@before-ok="submit"
>
<a-form ref="form" :model="form">
<a-form-item
label="文件夹名称"
field="fileName"
:rules="[{ required: true, message: '请输入文件夹名称' }]"
>
<a-input
placeholder="请输入文件夹名称"
v-model="form.fileName"
></a-input>
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script>
export default {
props: {
projectId: {
required: true,
type: String,
default: ''
},
pid: {
required: true,
type: String,
default: ''
}
},
data() {
return {
show: false,
form: {
fileName: '',
pid: '',
type: '1',
projectId: ''
}
}
},
methods: {
showAdd() {
console.log(this.pid)
if (this.pid === '0') {
this.$notification.error('当前目录不应许创建文件夹')
return
}
this.show = true
},
submit(done) {
this.$refs.form.validate(errors => {
if (errors === undefined) {
this.form.pid = this.pid
this.form.projectId = this.projectId
this.$api.projectFile.submit(this.form).then(res => {
if (res.code === 200) {
done()
this.$emit('ok')
this.$notificaiton.success(res.msg)
}
})
} else {
done(false)
}
})
}
}
}
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,178 @@
<template>
<div class="mt-20">
<div class="flex flex-center flex-justify-between">
<h2>业主单位{{ info.customer.name }}</h2>
<contacts-picker :customer-id="info.customer.id" @ok="success" />
</div>
<a-divider />
<a-list>
<a-list-item v-for="item in contactlist">
<div class="flex flex-center flex-justify-start">
<div>
{{ item.name }} {{ item.phone }}
<a-tag color="red" class="ml-20"
>{{ (item.sex = '0' ? '男' : '女') }}
</a-tag>
</div>
<div class="ml-20">{{ item.post }}</div>
</div>
</a-list-item>
</a-list>
<div class="flex flex-center flex-justify-between mt-20">
<div class="flex flex-center flex-justify-start">
<div class="flex flex-center flex-justify-start">
<h2>实施单位</h2>
<h2 v-if="edit === false && info.construction">
{{ constructionName }}
</h2>
<a-select
class="ml-10"
v-if="edit"
size="small"
allow-search
:options="options"
v-model="constructionId"
style="width: 380px"
@search="handleSearch"
></a-select>
</div>
<div class="ml-10">
<icon-edit v-if="edit === false" @click="edit = true" />
<icon-save v-else @click="save" />
</div>
</div>
<contacts-picker
v-if="constructionId"
:customer-id="constructionId"
@ok="successConstruction"
/>
</div>
<a-divider />
<a-list size="small">
<a-list-item v-for="item in constructionContactlist">
<div class="flex flex-center flex-justify-start">
<div>
{{ item.name }} {{ item.phone }}
<a-tag color="red" class="ml-20"
>{{ (item.sex = '0' ? '男' : '女') }}
</a-tag>
</div>
<div class="ml-20">{{ item.post }}</div>
</div>
</a-list-item>
</a-list>
</div>
</template>
<script>
import contactsPicker from '@/views/project/components/contacts-picker/index.vue'
import debounce from '@/utils/index.js'
export default {
components: {
contactsPicker
},
props: {
info: {
type: Object,
default: null
}
},
watch: {
info: {
handler(val) {
if (val) {
this.constructionName = val.construction.name
this.constructionId = val.construction.id
this.contactlist = val.contacts
this.constructionContactlist = val.constructionContacts
}
},
immediate: true
}
},
data() {
return {
edit: false,
contactlist: [],
constructionContactlist: [],
constructionId: '',
constructionName: '',
options: []
}
},
mounted() {},
methods: {
success(list) {
this.contactlist = this.contactlist.concat(list)
this.contactlist = Array.from(
new Map(this.contactlist.map(item => [item.id, item])).values()
)
const data = {
id: this.info.id,
contacts: this.contactlist.map(e => e.id).join(',')
}
this.$api.project.submit(data).then(res => {
if (res.code === 200) {
this.$notification.success(res.msg)
} else {
this.$notification.error(res.msg)
}
})
},
successConstruction(list) {
this.constructionContactlist =
this.constructionContactlist.concat(list)
this.constructionContactlist = Array.from(
new Map(
this.constructionContactlist.map(item => [item.id, item])
).values()
)
const data = {
id: this.info.id,
constructionContacts: this.constructionContactlist
.map(e => e.id)
.join(',')
}
this.$api.project.submit(data).then(res => {
if (res.code === 200) {
this.$notification.success(res.msg)
} else {
this.$notification.error(res.msg)
}
})
},
handleSearch(res) {
debounce(() => {
this.$api.customer.all({ name: res }).then(res => {
if (res.code === 200) {
this.options = res.data.map(e => {
return { label: e.name, value: e.id }
})
}
})
}, 800)
},
save() {
if (this.constructionId) {
const data = {
id: this.info.id,
constructionId: this.constructionId
}
this.$api.project.submit(data).then(res => {
if (res.code === 200) {
this.constructionName = res.data.construction.name
this.edit = false
this.notification.success(res.msg)
} else {
this.notification.error(res.msg)
}
})
}
}
}
}
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,92 @@
<template>
<div>
<div class="full-width flex flex-center flex-justify-start">
<h2>项目所属地区</h2>
</div>
<a-cascader
class="mt-20"
placeholder="请选择所属地区"
:options="options"
allow-search
v-model="city"
:field-names="{
label: 'name',
value: 'code'
}"
/>
<div v-if="city === ''">
<div style="height: 500px" class="flex flex-center">
<a-button>请选择地区</a-button>
</div>
</div>
<div class="mt-20">
<div
v-for="(item, index) in customerList"
:key="item.id"
class="padding border-bottom"
:class="activeIndex === index ? 'hover-bg' : ''"
@click="change(index)"
>
<div class="flex flex-center flex-justify-start">
<div
class="font-14 bold text-left flex full-width flex-justify-start"
>
{{ item.name }}
</div>
<div class="flex flex-center">
<a-tag color="blue"
>项目{{ item.projectCount }}
</a-tag>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
watch: {
city(val) {
this.$emit('ok', val)
if (val.length > 0) {
this.fetchCustomerByCity(val)
}
}
},
data() {
return {
options: [],
city: '',
activeIndex: 0,
customerList: []
}
},
mounted() {
this.fetchCity()
},
methods: {
fetchCustomerByCity(code) {
this.$api.project.findByCity({ code: code }).then(res => {
if (res.code === 200) {
this.customerList = res.data
this.$emit('ok', this.customerList[0])
}
})
},
fetchCity() {
this.$api.base.areaTree({ code: '53' }).then(res => {
if (res.code === 200) {
this.options = res.data
}
})
},
change(index) {
this.activeIndex = index
this.$emit('ok', this.customerList[index])
}
}
}
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,178 @@
<template>
<div>
<a-button type="primary" size="small" @click="show = true">
<template #icon>
<icon-plus />
</template>
<!-- Use the default slot to avoid extra spaces -->
<template #default>新建</template>
</a-button>
<a-modal
v-model:visible="show"
width="720px"
@close="this.$refs.form.resetFields()"
@before-ok="submit"
>
<a-form :model="form" auto-label-width ref="form">
<a-form-item
label="项目名称"
field="name"
:rules="[{ required: true, message: '请输入项目名称' }]"
>
<a-input
placeholder="请输入项目名称"
v-model="form.name"
></a-input>
</a-form-item>
<a-form-item
label="项目总投"
field="amount"
:rules="[{ required: true, message: '请输入项目总投' }]"
>
<a-input-number
placeholder="请输入项目总投"
v-model="form.amount"
>
<template #suffix>
<div>万元</div>
</template>
</a-input-number>
</a-form-item>
<a-form-item
field="cityCode"
label="项目地区"
:rules="[{ required: true, message: '请选择项目地区' }]"
>
<a-cascader
placeholder="请选择项目地区"
:options="options"
allow-search
v-model="form.cityCode"
:field-names="{
label: 'name',
value: 'code'
}"
/>
</a-form-item>
<a-form-item
label="业主单位"
field="customerId"
:rules="[{ required: true, message: '请选择业主单位' }]"
>
<a-select
placeholder="请选择业主单位"
:options="customerList"
allow-search
@search="fetchCustomer"
v-model="form.customerId"
></a-select>
</a-form-item>
<a-form-item
label="行业分类"
field="industry"
:rules="[{ required: true, message: '请选择行业分类' }]"
>
<a-cascader
v-model="form.industry"
allow-search
placeholder="请输入项目名称"
:options="industry"
></a-cascader>
</a-form-item>
<a-form-item
label="建设内容及规模"
field="content"
:rules="[
{ required: true, message: '请输入建设内容及规模' }
]"
>
<a-textarea
placeholder="建设内容及规模"
v-model="form.content"
:auto-size="{ minRows: 5 }"
></a-textarea>
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script>
export default {
watch: {
show: {
handler(val) {
if (val) {
this.fetchCity()
this.fetchDict()
}
}
}
},
data() {
return {
industry: [],
options: [],
form: {
name: '',
amount: '',
cityCode: '',
customerId: '',
industry: '',
createUserId: ''
},
customerList: [],
show: false
}
},
mounted() {
var user = JSON.parse(localStorage.getItem('user'))
this.form.createUserId = user.id
},
methods: {
fetchCity() {
this.$api.base.areaTree().then(res => {
if (res.code === 200) {
this.options = res.data
}
})
},
fetchDict() {
this.$api.sys.dict({ code: 'project_industry_type' }).then(res => {
if (res.code === 200) {
this.industry = res.data
}
})
},
fetchCustomer(res) {
this.$api.customer.all({ name: res }).then(res => {
if (res.code === 200) {
this.customerList = res.data.map(e => {
return { label: e.name, value: e.id }
})
}
})
},
submit(done) {
this.$refs.form.validate(errors => {
if (errors === undefined) {
this.$api.project.submit(this.form).then(res => {
if (res.code === 200) {
this.$notification.success(res.msg)
this.$emit('ok')
done()
} else {
this.$notification.error(res.msg)
done(false)
}
})
} else {
done(false)
}
})
}
}
}
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,197 @@
<template>
<div>
<a-button size="small" type="primary" @click="show = true">
<template #icon>
<icon-edit />
</template>
创建任务
</a-button>
<a-modal
v-model:visible="show"
width="760px"
@close="this.$refs.form.resetFields()"
@before-ok="submit"
>
<a-form auto-label-width :model="form" ref="form">
<a-row gutter="16">
<a-col :span="12">
<a-form-item
label="任务标题"
:rules="[
{ required: true, message: '请输入任务标题' }
]"
field="name"
>
<a-input
placeholder="请输入任务标题"
v-model="form.name"
></a-input>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item
label="优先级"
:rules="[
{ required: true, message: '请选择优先级' }
]"
field="level"
>
<a-select
:options="levelOptions"
v-model="form.level"
placeholder="请选择优先级"
/>
</a-form-item>
</a-col>
</a-row>
<a-form-item
label="任务周期"
:rules="[{ required: true, message: '请选择任务周期' }]"
field="time"
>
<a-range-picker style="width: 100%" v-model="form.time" />
</a-form-item>
<a-row gutter="16">
<a-col :span="12">
<a-form-item
label="任务标签"
:rules="[
{ required: true, message: '请选择任务标签' }
]"
field="tag"
>
<a-select
:options="tagsOptions"
v-model="form.tag"
placeholder="请选择任务标签"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item
label="执行人"
:rules="[
{ required: true, message: '请选择执行人' }
]"
field="executors"
>
<a-select
multiple
:options="userOptions"
allow-search
@search="handleSearch"
@focusin="handleFocus"
v-model="form.executors"
placeholder="请选择执行人"
/>
</a-form-item>
</a-col>
</a-row>
<a-form-item label="任务描述" field="remark">
<a-textarea
v-model="form.remark"
placeholder="请添加任务描述"
:auto-size="{ minRows: 4 }"
/>
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script>
export default {
props: {
projectId: {
required: true,
type: String,
default: ''
}
},
watch: {
show: {
handler(val) {
if (val) {
this.form.projectId = this.projectId
this.fetchDict()
}
}
}
},
data() {
return {
levelOptions: [],
tagsOptions: [],
userOptions: [],
show: false,
form: {
projectId: '',
name: '',
level: '2',
time: '',
tag: '',
executors: '',
remark: '',
files: null
}
}
},
methods: {
fetchDict() {
this.$api.sys.dict({ code: 'project_task_level' }).then(res => {
if (res.code === 200) {
this.levelOptions = res.data.map(e => {
return { label: e.label, value: e.value }
})
}
})
this.$api.sys.dict({ code: 'project_task_type' }).then(res => {
if (res.code === 200) {
this.tagsOptions = res.data.map(e => {
return { label: e.label, value: e.value }
})
}
})
},
handleFocus(e) {
this.handleSearch('')
},
handleSearch(value) {
this.$api.user.list({ name: value }).then(res => {
if (res.code === 200) {
this.userOptions = res.data.map(e => {
return { label: e.name, value: e.id }
})
}
})
},
submit(done) {
this.$refs.form.validate(errors => {
if (errors === undefined) {
const data = { ...this.form }
data.startDate = data.time[0]
data.endDate = data.time[1]
data.executorIds = data.executors.join(',')
this.$api.task.submit(data).then(res => {
if (res.code === 200) {
this.$notification.success(res.msg)
this.$emit('ok')
done()
} else {
this.$notification.error(res.msg)
done(false)
}
})
} else {
done(false)
}
})
}
}
}
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,225 @@
<template>
<div>
<div class="flex flex-center flex-justify-between">
<div class="flex flex-center flex-justify-start padding">
<div class="flex flex-center">
<div>搜索文件</div>
<a-input
style="width: 420px"
placeholder="请输入文件关键词"
v-model="name"
@press-enter="fetchList"
allow-clear
@clear="fetchList"
></a-input>
</div>
<a-button class="ml-20" type="primary" @click="fetchList"
>搜索
</a-button>
</div>
<div class="flex flex-center flex-justify-start">
<a-button-group>
<a-button type="outline" size="small" @click="goHome">
<template #icon>
<icon-home />
</template>
<template #default> 首页</template>
</a-button>
<a-button type="outline" size="small" @click="goBack">
<template #icon>
<icon-nav />
</template>
<template #default> 上一级</template>
</a-button>
<upload @ok="upload" />
<add-folder
:project-id="this.projectId"
:pid="this.pid"
@ok="fetchList"
/>
</a-button-group>
</div>
</div>
<a-table
:columns="columns"
:data="list"
:loading="loading"
:pagination="page"
size="small"
@page-change="pageChange"
>
<template #name="{ record }">
<div class="flex flex-center flex-justify-start">
<img
v-if="record.type === 1"
src="https://res.wutongshucloud.com/res/2024/12/04/202412041635423.svg"
style="width: 30px; height: 30px"
/>
<img
v-else
src="https://res.wutongshucloud.com/res/2024/12/04/202412041641902.svg"
style="width: 20px; height: 20px; padding-left: 5px"
/>
<div class="ml-10">{{ record.name }}</div>
</div>
</template>
<template #menu="{ record }">
<div class="flex flex-center">
<preview
v-if="record.type === 0"
:file-id="record.fileId"
/>
<a-button
v-else
type="text"
size="small"
@click="fetchFolder(record.id)"
>查看
</a-button>
<a-button type="text" size="small" @click="remove(record)"
>删除
</a-button>
</div>
</template>
</a-table>
</div>
</template>
<script>
import upload from '@/components/upload/index.vue'
import preview from '@/components/preview/index.vue'
import addFolder from '@/views/project/index/components/add-folder.vue'
export default {
props: {
projectId: {
type: String,
default: ''
}
},
components: {
upload,
preview,
addFolder
},
data() {
return {
name: '',
loading: true,
history: ['0'],
page: {
page: 0,
pageSize: 10,
total: 0
},
list: [],
pid: '0',
columns: [
{
title: '名称',
slotName: 'name'
},
{
title: '创建时间',
dataIndex: 'createTime'
},
{
title: '创建人',
dataIndex: 'createUserName'
},
{
title: '操作',
slotName: 'menu'
}
]
}
},
mounted() {
this.fetchInit()
},
methods: {
goHome() {
this.history = ['0']
this.pid = '0'
this.fetchList()
},
goBack() {
var size = this.history.length
if (size > 1) {
this.history = this.history.slice(0, size - 1)
this.pid = this.history[this.history.length - 1]
this.fetchList()
} else {
this.$notification.info('已经回到首页')
}
},
fetchFolder(id) {
this.pid = id
this.history.push(id)
this.fetchList()
},
pageChange(page) {
this.page.page = page - 1
this.fetchList()
},
fetchInit() {
this.$api.sys.dict({ code: 'project_folder_init' }).then(res => {
if (res.code === 200) {
var folder = res.data.map(e => e.label)
this.$api.projectFile
.initFolder({
projectId: this.projectId,
folders: folder.join(',')
})
.then(() => {
this.fetchList()
this.loading = false
})
}
})
},
fetchList() {
const data = {
name: this.name,
projectId: this.projectId,
pid: this.pid,
...this.page
}
this.$api.projectFile.page(data).then(res => {
if (res.code === 200) {
this.list = res.data.records
this.page.total = res.data.total
}
})
},
upload(file) {
const data = { projectId: this.projectId, pid: this.pid, ...file }
this.$api.projectFile.submit(data).then(res => {
if (res.code === 200) {
this.fetchList()
this.$notification.success(res.msg)
} else {
this.$notification.error(res.msg)
}
})
},
remove(file) {
if (file.pid === '0') {
this.$notification.error('当前内容不支持删除')
return
}
this.$api.projectFile.remove({ id: file.id }).then(res => {
if (res.code === 200) {
this.$notification.success(res.msg)
this.fetchList()
} else {
this.$notification.error(res.msg)
}
})
}
}
}
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,215 @@
<template>
<div>
<a-button type="text" size="small" @click="show = true">更多</a-button>
<a-drawer
v-if="info"
v-model:visible="show"
width="80%"
:mask-closable="false"
:header="false"
:footer="false"
>
<div>
<div class="flex flex-center flex-justify-between">
<div class="flex flex-center">
<div class="font-18 bold" v-if="edit === false">
{{ name }}
</div>
<div v-else>
<a-input
v-model="name"
style="width: 480px"
></a-input>
</div>
<icon-edit
class="padding pointer"
v-if="edit === false"
@click="edit = true"
/>
<icon-save
class="padding pointer"
v-else
@click="updateName"
/>
</div>
<a-button type="outline" size="small" @click="show = false"
>关闭
</a-button>
</div>
</div>
<div class="mt-20 flex flex-center flex-justify-start">
<edit-task
:project-id="info.id"
@ok="this.$refs.task.fetchList()"
/>
</div>
<div class="mt-20">
<a-descriptions
:data="data"
bordered
:column="{ xs: 1, md: 3 }"
>
<a-descriptions-item
v-for="item of data"
:label="item.label"
:span="3"
>
<div>{{ item.value }}</div>
</a-descriptions-item>
</a-descriptions>
</div>
<a-tabs class="mt-10" @change="tabChange" :active-key="tabIndex">
<a-tab-pane title="项目任务" key="0"></a-tab-pane>
<a-tab-pane title="项目文件" key="1"></a-tab-pane>
<a-tab-pane title="跟进记录" key="2"></a-tab-pane>
</a-tabs>
<div v-if="tabIndex === 0">
<task :project-id="info.id" ref="task" />
</div>
<div v-else-if="tabIndex === 1">
<files :project-id="info.id" />
</div>
<div v-else-if="tabIndex === 4">
<base-info :info="info" />
</div>
<div
v-else
class="flex flex-center flex-col grey-6"
style="margin-top: 96px"
>
<img
style="width: 100px"
src="https://res.wutongshucloud.com/res/2024/12/09/202412091020938.svg"
/>
<div>正在开发中</div>
</div>
</a-drawer>
</div>
</template>
<script>
import task from '@/views/project/index/components/task.vue'
import editTask from '@/views/project/index/components/edit-task.vue'
import files from '@/views/project/index/components/files.vue'
import baseInfo from '@/views/project/index/components/base-info.vue'
export default {
components: {
task,
editTask,
files,
baseInfo
},
props: {
info: {
required: true,
type: Object,
default: null
}
},
watch: {
show: {
handler(val) {
if (val) {
this.tabIndex = 0
if (this.$refs.task) {
this.$refs.task.fetchList()
}
} else {
this.tabIndex = 0
}
},
immediate: true
},
info: {
handler(val) {
this.name = val.name
this.fetchData()
},
immediate: true
}
},
data() {
return {
name: '',
edit: false,
show: false,
tabIndex: 0,
data: [
{
label: '项目总投资(万元)',
prop: 'amount'
},
{
label: '项目负责人',
prop: 'createUser'
},
{
label: '项目地区',
prop: 'cityName'
},
{
label: '所属行业',
prop: 'industry'
},
{
label: '业主单位',
prop: 'customer'
},
{
label: '建设内容及规模',
prop: 'content',
span: 3
}
]
}
},
methods: {
fetchData() {
this.data = this.data.map(item => {
const res = {
label: item.label,
value: this.info[item.prop],
prop: item.prop,
span: item.span ?? 1
}
if (item.prop === 'createUser') {
res.value = this.info.createUser.name
}
if (item.prop === 'customer') {
res.value = this.info.customer
? this.info.customer.name
: ''
}
if (item.prop === 'cityName') {
res.value = this.info.customer
? this.info.customer.cityName
: ''
}
if (item.prop === 'industry') {
res.value = this.info.industry
? this.info.industry.label
: ''
}
return res
})
},
tabChange(index) {
this.tabIndex = Number.parseInt(index)
},
updateName() {
const data = { id: this.info.id, name: this.name }
this.edit = false
this.$api.project.submit(data).then(res => {
if (res.code === 200) {
this.$notification.success(res.msg)
} else {
this.$notification.error(res.msg)
}
})
}
}
}
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,263 @@
<template>
<div>
<a-button type="text" size="small" @click="show = true">更多</a-button>
<a-drawer
v-model:visible="show"
width="80%"
:header="false"
:footer="false"
>
<div v-if="task">
<div>{{ task.createUser.name }}创建于{{ task.createTime }}</div>
<div class="flex flex-align-baseline">
<div class="font-24 bold">{{ task.name }}</div>
<div class="ml-20">
<a-dropdown @select="changeStatus">
<a-button
type="primary"
size="small"
:style="
`background-color:` + task.status.remark
"
>{{ task.status.label }}
</a-button>
<template #content>
<a-doption
v-for="item in dict"
:key="item.id"
:value="item"
>{{ item.label }}
</a-doption>
</template>
</a-dropdown>
</div>
</div>
<a-divider />
<div class="flex flex-align-start flex-justify-between">
<div style="flex: 3">
<div>
<div
style="white-space: pre"
class="grey-6-bg padding"
>
{{ task.remark }}
</div>
</div>
<a-divider />
<div class="flex flex-center flex-justify-start">
<div class="font-18 bold">关联附件</div>
<file-picker
class="ml-20"
:project-id="task.projectId"
@ok="success"
/>
</div>
<div class="mt-20">
<a-list size="small">
<a-list-item
v-for="item in fileList"
:key="item.id"
>
<div
class="flex flex-center flex-justify-between"
>
<div>{{ item.name }}</div>
<div class="flex flex-center">
<preview :file-id="item.fileId" />
<a-popconfirm
content="确定取消该关联附件"
@ok="remove(item)"
>
<a-button type="text"
>取消关联
</a-button>
</a-popconfirm>
</div>
</div>
</a-list-item>
</a-list>
</div>
<a-divider />
<div class="flex flex-center flex-justify-start mt-20">
<div class="font-18 bold">任务日志</div>
</div>
<div class="mt-20">
<div
class="flex flex-center flex-justify-start mb-20"
v-for="item in log"
:key="item.id"
>
<a-avatar v-if="item.avatar">
<img alt="avatar" :src="item.avatar" />
</a-avatar>
<a-avatar v-else style="background-color: blue">
<div>{{ item.user.name }}</div>
</a-avatar>
<div class="ml-10">
<div>
{{ item.user.name }}
{{ item.createTime }}
</div>
<div>
{{ item.content }}
</div>
</div>
</div>
</div>
</div>
<div style="flex: 1" class="ml-20">
<a-card>
<div class="font-18 bold">基础信息</div>
<a-divider />
<a-form auto-label-width>
<a-form-item label="任务标题:">
<div class="black bold">
{{ task.name }}
</div>
</a-form-item>
<a-form-item label="优先级:">
<a-tag class="black bold" color="green"
>
</a-tag>
</a-form-item>
<a-form-item label="创建人:">
<div class="black bold">
{{ task.createUser.name }}
</div>
</a-form-item>
<a-form-item label="任务标签:">
<a-tag class="black bold" color="red">
{{ task.tag.label }}
</a-tag>
</a-form-item>
<a-form-item label="任务周期:">
<div class="black bold">
{{ task.startDate }} -
{{ task.endDate }}
</div>
</a-form-item>
<a-form-item label="执行人:">
<div
class="black bold flex flex-justify-start flex-wrap"
>
<div v-for="user in task.executors">
<a-tag class="mr-10" color="blue"
>{{ user.name }}
</a-tag>
</div>
</div>
</a-form-item>
</a-form>
</a-card>
</div>
</div>
</div>
</a-drawer>
</div>
</template>
<script>
import filePicker from '@/views/project/components/file-picker/index.vue'
import preview from '@/components/preview/index.vue'
export default {
components: {
filePicker,
preview
},
props: {
id: {
type: String,
default: ''
}
},
watch: {
show: {
handler(val) {
if (val) {
this.fetchDict()
}
}
}
},
data() {
return {
show: false,
task: null,
fileList: [],
dict: [],
log: []
}
},
methods: {
fetchInfo() {
this.$api.task.info({ id: this.id }).then(res => {
if (res.code === 200) {
this.task = res.data
this.task.status = this.dict.find(
sub => sub.value === this.task.status + ''
)
this.fileList = this.task.files
this.fetchLog()
}
})
},
fetchDict() {
this.$api.sys.dict({ code: 'project_task_status' }).then(res => {
if (res.code === 200) {
this.dict = res.data
this.fetchInfo()
}
})
},
changeStatus(res) {
this.task.status = res
const data = { id: this.task.id, status: this.task.status.value }
this.$api.task.submit(data).then(res => {
if (res.code === 200) {
this.$notification.success(res.msg)
}
})
},
success(files) {
this.fileList = this.fileList.concat(files)
this.fileList = Array.from(
new Map(this.fileList.map(item => [item.id, item])).values()
)
const data = {
id: this.task.id,
files: this.fileList.map(e => e.id).join(',')
}
this.$api.task.submit(data).then(res => {
if (res.code === 200) {
this.$notification.success(res.msg)
} else {
this.$notification.error(res.msg)
}
})
},
remove(file) {
const data = { id: this.task.id, fileId: file.id }
this.$api.task.removeFiles(data).then(res => {
if (res.code === 200) {
this.fileList = this.fileList.filter(
sub => sub.id !== file.id
)
this.$notification.success(res.msg)
} else {
this.$notification.error(res.msg)
}
})
},
fetchLog() {
this.$api.task.allLog({ id: this.task.id }).then(res => {
if (res.code === 200) {
this.log = res.data
}
})
}
}
}
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,140 @@
<template>
<div>
<a-table :columns="columns" :data="list">
<template #createUser="{ record }">
<div>
{{ record.createUser.name }}
</div>
</template>
<template #status="{ record }">
<div>
<a-tag :color="record.status.remark"
>{{ record.status.label }}
</a-tag>
</div>
</template>
<template #tag="{ record }">
<div>
<a-tag color="blue">{{ record.tag.label }}</a-tag>
</div>
</template>
<template #menu="{ record }">
<div>
<task-info :id="record.id" />
</div>
</template>
</a-table>
</div>
</template>
<script>
import taskInfo from '@/views/project/index/components/task-info.vue'
export default {
components: {
taskInfo
},
props: {
projectId: {
required: true,
type: String,
default: ''
}
},
data() {
return {
list: [],
page: { page: 0, size: 10, total: 0 },
dict: [],
level: [],
columns: [
{
title: '任务标题',
dataIndex: 'name'
},
{
title: '优先级',
dataIndex: 'levelName'
},
{
title: '任务标签',
slotName: 'tag'
},
{
title: '任务状态',
slotName: 'status'
},
{
title: '创建时间',
dataIndex: 'createTime'
},
{
title: '创建人',
slotName: 'createUser'
},
{
title: '操作',
slotName: 'menu'
}
]
}
},
mounted() {
this.fetchDict()
},
methods: {
fetchDict() {
const tmpStatus = sessionStorage.getItem('project_task_status')
if (tmpStatus) {
this.dict = JSON.parse(tmpStatus)
} else {
this.$api.sys
.dict({ code: 'project_task_status' })
.then(res => {
if (res.code === 200) {
this.dict = res.data
sessionStorage.setItem(
'project_task_status',
JSON.stringify(this.dict)
)
}
})
}
const tmplevel = sessionStorage.getItem('project_task_level')
if (tmplevel) {
this.level = JSON.parse(tmplevel)
} else {
this.$api.sys.dict({ code: 'project_task_level' }).then(res => {
if (res.code === 200) {
this.level = res.data
sessionStorage.setItem(
'project_task_level',
JSON.stringify(this.level)
)
this.fetchList()
}
})
}
},
fetchList() {
const data = { projectId: this.projectId, ...this.page }
this.$api.task.page(data).then(res => {
if (res.code === 200) {
this.list = res.data.records.map(e => {
var item = { ...e }
item.status = this.dict.find(
e => e.value === item.status.toString()
)
item.levelName = this.level.find(
e => e.value === item.level
).label
return item
})
}
})
}
}
}
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,148 @@
<template>
<div>
<navbar title="项目库" />
<div class="flex">
<div class="container" style="min-width: 320px">
<city-tree @ok="changeCustomer" />
</div>
<div class="container full-width">
<div v-if="city">
<div class="flex flex-center flex-justify-start full-width">
<h2>{{ city.name }}</h2>
</div>
<div
class="mt-10 flex flex-center flex-justify-start full-width"
>
<a-tag color="blue"
>共计项目 {{ city.projectCount }} 待办任务
{{ city.taskCount }}
</a-tag>
</div>
</div>
<div v-else>
<div class="flex flex-center flex-justify-start full-width">
<h2>共计项目{{ page.total }} </h2>
</div>
</div>
<a-table
:columns="columns"
:data="list"
class="mt-20"
:pagination="page"
@page-change="pageChange"
>
<template #user="{ record }">
<div>
{{ record.createUser.name }}
</div>
</template>
<template #menu="{ record }">
<div>
<more-info :info="record" />
</div>
</template>
<template #customer="{ record }">
<div>
{{ record.customer ? record.customer.name : '' }}
</div>
</template>
</a-table>
</div>
</div>
</div>
</template>
<route>
{
path: '/project/index',
}
</route>
<script>
import navbar from '@/components/navbar/index.vue'
import moreInfo from './components/more-info.vue'
import cityTree from '@/views/project/index/components/city-tree.vue'
export default {
components: {
cityTree,
navbar,
moreInfo
},
data() {
return {
list: [],
name: '',
customerId: '',
page: {
page: 0,
size: 10,
total: 0
},
customerList: [],
years: [],
year: '',
city: null,
columns: [
{
title: '项目名称',
dataIndex: 'name'
},
{
title: '创建时间',
dataIndex: 'createTime'
},
{
title: '创建人',
slotName: 'user'
},
{
title: '操作',
slotName: 'menu'
}
]
}
},
mounted() {
this.fetchList()
this.initYear()
},
methods: {
initYear() {
let year = new Date().getFullYear()
let startYear = 2021
for (let i = year; i >= startYear; i--) {
const item = { label: i.toString(), value: i }
this.years.push(item)
}
},
changeCustomer(e) {
this.city = e
this.customerId = e.id
this.fetchList()
},
pageChange(page) {
this.page.page = page - 1
this.fetchList()
},
fetchList() {
const data = {
name: this.name,
customerId: this.customerId,
year: this.year,
...this.page
}
this.$api.project.page(data).then(res => {
if (res.code === 200) {
this.list = res.data.records
this.page.total = res.data.total
}
})
}
}
}
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,123 @@
<template>
<div>
<navbar title="资料库" />
<div class="container flex flex-center flex-align-start">
<div
style="
flex: 1;
border-right: #f2f3f5 solid 1px;
padding: 0 16px;
"
>
<div class="font-14 bold flex-justify-start text-left">
<a-input placeholder="请输入项目名称">
<template #prefix>
<icon-search />
</template>
</a-input>
</div>
<a-divider />
<div>
<div v-for="(item, i) in 10" @click="index = i">
<div
class="text-left lines-1"
:class="index === item ? 'name_check' : 'name'"
>
项目名称项目名称项目名称项目名项目名称项目名称项目名称项目名
</div>
</div>
</div>
</div>
<div style="flex: 3">
<div style="padding: 0 16px">
<div class="flex flex-center flex-justify-between">
<div class="flex flex-center flex-justify-start ml-10">
<a-button-group>
<a-button>
<icon-left />
</a-button>
<a-button>
<icon-right />
</a-button>
</a-button-group>
<a-input
class="ml-10"
style="width: 380px"
placeholder="请输入名称搜索"
></a-input>
<a-button type="primary">搜索</a-button>
</div>
<div>
<a-button-group type="outline">
<a-button>新增</a-button>
<a-button>上传</a-button>
</a-button-group>
</div>
</div>
<a-divider />
<a-list
:bordered="false"
size="small"
:pagination-props="page"
>
<a-list-item v-for="item in 10">
<div>
<div
class="flex flex-center flex-justify-between"
>
<div class="flex flex-center">
<img
src="https://res.wutongshucloud.com/res/2024/12/04/202412041635423.svg"
style="width: 30px; height: 30px"
/>
<div>文件名称</div>
</div>
<div>332MB</div>
<div>修改日期</div>
<div>创建人</div>
</div>
</div>
</a-list-item>
</a-list>
</div>
</div>
</div>
</div>
</template>
<script>
import navbar from '@/components/navbar/index.vue'
export default {
components: {
navbar
},
data() {
return {
index: 1
}
}
}
</script>
<style lang="scss" scoped>
.name {
margin: 16px 1px;
border-radius: 4px;
padding: 4px;
}
.name_check {
border-radius: 4px;
margin: 10px 1px;
padding: 4px;
background-color: #eef2f9;
}
.name:hover {
border-radius: 4px;
margin: 10px 1px;
padding: 4px;
background-color: #eef2f9;
}
</style>

20
src/views/result/401.vue Normal file
View File

@ -0,0 +1,20 @@
<template>
<div class="flex flex-center flex-col full-width full-height">
<img
src="https://res.wutongshucloud.com/res/2024/11/19/202411191459526.svg"
style="width: 400px"
/>
<div class="font-18 grey-6">当前账号无访问权限</div>
</div>
</template>
<route>
{
path: '/401',
}
</route>
<script>
export default {}
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,78 @@
<template>
<div>
<navbar title="应用管理"></navbar>
<div class="container">
<div class="flex flex-center flex-justify-between">
<a-button type="text">新增</a-button>
<div>
<a-button type="outline" shape="circle" @click="fetchList">
<icon-refresh />
</a-button>
</div>
</div>
<a-table :columns="columns" :data="list" size="small" class="mt-20">
<template #menu="{ record }">
<div>
<a-button type="text" size="small">查看</a-button>
</div>
</template>
</a-table>
</div>
</div>
</template>
<script>
import navbar from '@/components/navbar/index.vue'
export default {
components: {
navbar
},
data() {
return {
list: [],
page: {
page: 0,
size: 10
},
columns: [
{
title: '名称',
dataIndex: 'name'
},
{
title: 'appId',
dataIndex: 'appid'
},{
title: 'secret',
dataIndex: 'secret'
},
{
title: '创建时间',
dataIndex: 'createTime'
},
{
title: '操作',
slotName: 'menu'
}
]
}
},
mounted() {
this.fetchList()
},
methods: {
fetchList() {
this.$api.sys.clients(this.page).then(res => {
console.log(res)
if (res.code === 200) {
this.list = res.data
this.$notification.success(res.msg)
}
})
}
}
}
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,141 @@
<template>
<div>
<div>
<a-button
v-if="type === 'add'"
type="primary"
size="small"
@click="show = true"
>
<template #icon>
<icon-plus />
</template>
<template #default> 新增</template>
</a-button>
<a-button
v-if="type === 'new'"
type="primary"
size="small"
@click="show = true"
>
<template #icon>
<icon-plus />
</template>
<template #default> 新增</template>
</a-button>
<a-button
v-if="type === 'edit'"
type="text"
size="small"
@click="show = true"
>编辑
</a-button>
<a-button
v-if="type === 'addSub'"
type="text"
size="small"
@click="show = true"
>新增子项
</a-button>
</div>
<a-modal
:mask-closable="false"
v-model:visible="show"
@close="this.$refs.form.resetFields()"
@before-ok="submit"
>
<a-form auto-label-width :model="form" ref="form">
<a-form-item label="名称" field="label" required>
<a-input v-model="form.label"></a-input>
</a-form-item>
<a-form-item label="code" field="code" required>
<a-input
v-model="form.code"
:disabled="form.pid !== undefined"
></a-input>
</a-form-item>
<a-form-item label="值" field="value">
<a-input v-model="form.value"></a-input>
</a-form-item>
<a-form-item label="排序" field="sort" required>
<a-input-number v-model="form.sort"></a-input-number>
</a-form-item>
<a-form-item label="备注" field="remark">
<a-input v-model="form.remark"></a-input>
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script>
export default {
props: {
type: {
type: String,
default: 'add'
},
info: {
type: Object,
default: null
}
},
watch: {
show: {
handler(val) {
if (val) {
if (this.type === 'add' && this.info) {
delete this.form.id
this.form.pid = this.info.id
this.form.code = this.info.code
} else if (this.type === 'addSub') {
this.form.pid = this.info.id
this.form.code = this.info.code
} else if (this.type === 'edit') {
this.form = this.info
}
}
},
immediate: true
}
},
data() {
return {
show: false,
form: {
label: '',
code: '',
remark: '',
sort: ''
}
}
},
methods: {
submit(done) {
this.$refs.form.validate(errors => {
if (errors === undefined) {
this.$api.sys.dictSave(this.form).then(res => {
if (res.code === 200) {
this.$notification.success(res.msg)
this.form = res.data
setTimeout(() => {
this.$emit('ok')
}, 500)
done()
} else {
this.$notification.error(res.msg)
done(false)
}
})
} else {
done(false)
}
})
}
}
}
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,91 @@
<template>
<div>
<a-button type="text" @click="show = true">新增</a-button>
<a-modal
v-model:visible="show"
:mask-closable="false"
@close="this.$refs.form.resetFields()"
@before-ok="submit"
>
<a-form auto-label-width :model="form" ref="form">
<a-form-item label="名称" field="name" required>
<a-input
placeholder="请输入名称"
v-model="form.name"
></a-input>
</a-form-item>
<a-form-item label="code" field="code" required>
<a-input
placeholder="请输入Code"
v-model="form.code"
></a-input>
</a-form-item>
<a-form-item label="排序" field="sort" required>
<a-input-number
placeholder="请输入排序"
v-model="form.sort"
></a-input-number>
</a-form-item>
<a-form-item label="备注" field="remark" required>
<a-input
placeholder="请输入备注"
v-model="form.remark"
></a-input>
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script>
export default {
props: {
appId: {
type: String,
default: ''
}
},
watch: {
appId: {
handler(val) {
this.form.appId = val
},
immediate: true
}
},
data() {
return {
show: false,
form: {
appId: '',
name: '',
code: '',
sort: '',
remark: ''
}
}
},
methods: {
submit(done) {
this.$refs.form.validate(errors => {
if (errors === undefined) {
this.$api.sys.roleSubmit(this.form).then(res => {
if (res.code === 200) {
this.$notification.success(res.msg)
this.$emit('ok')
done()
} else {
this.$notification.error(res.msg)
done(false)
}
})
} else {
done(false)
}
})
}
}
}
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,147 @@
<template>
<div>
<a-button
v-if="type === 'edit'"
type="text"
size="small"
@click="show = true"
>
编辑
</a-button>
<a-button
v-else-if="type === 'addSub'"
type="text"
size="small"
@click="show = true"
>
新增子项
</a-button>
<a-button v-else type="text" size="small" @click="show = true">
新增
</a-button>
<a-modal
v-model:visible="show"
:mask-closable="false"
@close="this.$refs.form.resetFields()"
@before-ok="ok"
>
<div>
<a-form auto-label-width ref="form" :model="form">
<a-form-item
label="名称"
field="name"
:required="[{ required: true, message: '请填写名称' }]"
>
<a-input
v-model="form.name"
placeholder="请填写名称"
></a-input>
</a-form-item>
<a-form-item
label="路径/code"
field="value"
:required="[
{ message: '请输入路径/code', required: true }
]"
>
<a-input
v-model="form.value"
placeholder="请输入路径/code"
></a-input>
</a-form-item>
<a-form-item
label="类型"
:required="[{ message: '请选择类型', required: true }]"
>
<a-radio-group v-model="form.type">
<a-radio :value="1">菜单</a-radio>
<a-radio :value="2">按钮</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item
label="排序"
field="sort"
:required="[{ message: '请输入排序', required: true }]"
>
<a-input-number
v-model="form.sort"
placeholder="请输入排序"
></a-input-number>
</a-form-item>
</a-form>
</div>
</a-modal>
</div>
</template>
<script>
export default {
props: {
type: {
required: true,
type: String,
default: 'add'
},
info: {
type: Object,
default: null
}
},
watch: {
info: {
handler(val) {
if (val) {
if (this.type === 'edit') {
this.form = val
} else if (this.type === 'addSub') {
this.form.appId = val.appId
this.form.pid = val.id
} else {
this.form.appId = val.appId
this.form.pid = val.pid
}
}
},
immediate: true
}
},
data() {
return {
show: false,
form: {
appId: '',
id: '',
name: '',
value: '',
sort: '',
type: 1,
pid: '0'
}
}
},
methods: {
ok(done) {
this.$refs.form.validate(errors => {
if (errors === undefined) {
console.log(this.form)
this.$api.sys.menuSave(this.form).then(res => {
if (res.code === 200) {
this.$notification.success(res.msg)
this.$emit('ok')
done()
} else {
this.$notification.error(res.msg)
done(false)
}
})
} else {
done(false)
}
})
}
}
}
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,81 @@
<template>
<div>
<a-button type="text" @click="show = true">配置</a-button>
<a-modal mask-closable v-model:visible="show" @before-ok="submit">
<a-tree
:checkable="true"
:data="list"
v-model:checked-keys="defaultKeys"
:field-names="{ key: 'id', title: 'name' }"
/>
</a-modal>
</div>
</template>
<script>
export default {
props: {
id: {
required: true,
type: String,
default: ''
},
appId: {
required: true,
type: String,
default: ''
},
menuIds: {
type: String,
default: ''
}
},
watch: {
show: {
handler(val) {
if (val) {
this.fetchMenu()
if (this.menuIds) {
this.defaultKeys = this.menuIds.split(',')
}
}
}
}
},
data() {
return {
show: false,
defaultKeys: null,
list: []
}
},
methods: {
fetchMenu() {
this.$api.sys.menuTree({ appId: this.appId }).then(res => {
if (res.code === 200) {
this.list = res.data.map(e => {
e.key = e.id
return e
})
}
})
},
submit(done) {
const data = { id: this.id, menuIds: this.defaultKeys.join(',') }
console.log(data)
this.$api.sys.roleSubmit(data).then(res => {
if (res.code === 200) {
this.$notification.success(res.msg)
this.$emit('ok')
done()
} else {
this.$notification.error(res.msg)
done(false)
}
})
}
}
}
</script>
<style lang="scss" scoped></style>

124
src/views/system/dict.vue Normal file
View File

@ -0,0 +1,124 @@
<template>
<div>
<navbar title="业务字典" />
<div class="flex flex-align-start">
<div class="container mr-20" style="flex: 1">
<div class="flex flex-center flex-justify-start">
<add-dict @ok="fetchList" type="new" />
</div>
<a-table
class="mt-20"
:columns="columns"
:data="list"
:pagination="page"
@row-click="rowClick"
@page-change="pageChange"
>
<template #label="{ record }">
<div>{{ record.sort }} {{ record.label }}</div>
</template>
</a-table>
</div>
<div class="container" style="flex: 3" v-if="dict">
<div class="flex flex-center flex-justify-between">
<div class="flex flex-center">
<h2>{{ dict.label }}</h2>
<add-dict class="ml-20" :info="dict" type="edit" />
</div>
<add-dict :info="dict" type="add" @ok="fetchDetail" />
</div>
<a-table class="mt-20" :columns="columns2" :data="list2">
<template #menu="{ record }">
<div class="flex flex-center flex-justify-start">
<add-dict
:info="record"
type="edit"
@ok="fetchDetail"
/>
<add-dict :info="record" type="addSub" />
<a-popconfirm
content="确定删除该数据?"
@ok="remove(record.id)"
>
<a-button type="text" size="small"
>删除
</a-button>
</a-popconfirm>
</div>
</template>
</a-table>
</div>
</div>
</div>
</template>
<script>
import navbar from '@/components/navbar/index.vue'
import addDict from '@/views/system/components/add-dict.vue'
export default {
components: {
navbar,
addDict
},
data() {
return {
page: { page: 0, size: 10, total: 0 },
list: [],
list2: [],
columns: [{ title: '名称', slotName: 'label' }],
columns2: [
{ title: '名称', dataIndex: 'label' },
{ title: '值', dataIndex: 'value' },
{ title: 'code', dataIndex: 'code' },
{ title: '排序', dataIndex: 'sort' },
{ title: '操作', slotName: 'menu' }
],
dict: null,
index: 0
}
},
mounted() {
this.fetchList()
},
methods: {
pageChange(page) {
this.page.page = page - 1
this.fetchList()
},
fetchList() {
this.$api.sys.dictPage(this.page).then(res => {
if (res.code === 200) {
this.list = res.data.records
this.dict = this.list[this.index]
this.fetchDetail()
this.page.total = res.data.total
}
})
},
fetchDetail() {
this.$api.sys.dict({ code: this.dict.code }).then(res => {
if (res.code === 200) {
this.list2 = res.data
}
})
},
rowClick(res) {
this.dict = res
this.index = this.list.findIndex(sub => sub.id === res.id)
this.fetchDetail()
},
remove(id) {
this.$api.sys.dictRemove({ ids: id }).then(res => {
if (res.code === 200) {
this.fetchList()
this.$notification.success(res.msg)
}
})
}
}
}
</script>
<style lang="scss" scoped></style>

105
src/views/system/log.vue Normal file
View File

@ -0,0 +1,105 @@
<template>
<div>
<navbar title="日志中心" />
<div class="container">
<div class="flex flex-center flex-justify-start">
<a-button type="text" size="small" @click="fetchList"
>刷新日志
</a-button>
</div>
<a-table
class="mt-10"
:columns="columns"
:pagination="page"
@page-change="pageChange"
size="small"
:data="list"
>
<template #errorMsg="{ record }">
<div>
<a-tag color="green" v-if="record.errorMsg === null"
>成功
</a-tag>
<a-tag color="red" v-else>失败</a-tag>
</div>
</template>
<template #jsonResult="{ record }">
<div>
<div v-if="record.errorMsg === null">
{{ record.jsonResult }}
</div>
<div v-else class="red">
{{ record.errorMsg }}
</div>
</div>
</template>
</a-table>
</div>
</div>
</template>
<script>
import navbar from '@/components/navbar/index.vue'
export default {
components: {
navbar
},
data() {
return {
page: { page: 0, size: 10 },
columns: [
{
title: '用户',
dataIndex: 'account'
},
{
title: '类型',
dataIndex: 'title'
},
{
title: '客户端',
dataIndex: 'browser'
},
{
title: '地区',
dataIndex: 'location'
},
{
title: '状态',
slotName: 'errorMsg'
},
{
title: '详情',
slotName: 'jsonResult',
width: 680
},
{
title: '时间',
dataIndex: 'createTime'
}
],
list: []
}
},
mounted() {
this.fetchList()
},
methods: {
pageChange(page) {
this.page.page = page - 1
this.fetchList()
},
fetchList() {
this.$api.sys.logPage(this.page).then(res => {
if (res.code === 200) {
this.list = res.data.records
this.page.total = res.data.total
}
})
}
}
}
</script>
<style lang="scss" scoped></style>

129
src/views/system/menu.vue Normal file
View File

@ -0,0 +1,129 @@
<template>
<div>
<navbar title="菜单管理"></navbar>
<div class="container">
<div class="flex flex-center flex-justify-between">
<a-select
style="width: 360px"
:options="options"
:model-value="client"
></a-select>
<edit-menu
:info="{ appId: client, pid: '0' }"
type="add"
@ok="fetchClientList"
/>
</div>
<div class="mt-20">
<a-table :columns="columns" :data="list">
<template #type="{ record }">
<div>
<a-tag color="blue" v-if="record.type === 1"
>菜单
</a-tag>
<a-tag color="red" v-if="record.type === 2"
>按钮
</a-tag>
</div>
</template>
<template #menu="{ record }">
<div class="flex flex-center flex-justify-start">
<edit-menu
:info="record"
type="addSub"
@ok="fetchClientList"
/>
<edit-menu
:info="record"
type="edit"
@ok="fetchClientList"
/>
<div>
<a-popconfirm
content="确认删除?"
@ok="remove(record.id)"
>
<a-button
type="text"
size="small"
:disabled="
record.value === '/' ||
record.value.indexOf('/system') >
-1 ||
record.children
"
>
删除
</a-button>
</a-popconfirm>
</div>
</div>
</template>
</a-table>
</div>
</div>
</div>
</template>
<script>
import navbar from '@/components/navbar/index.vue'
import editMenu from './components/edit-menu.vue'
export default {
components: {
navbar,
editMenu
},
data() {
return {
client: '',
list: [],
options: [],
columns: [
{ title: '名称', dataIndex: 'name' },
{ title: '路径', dataIndex: 'value' },
{ title: '排序', dataIndex: 'sort' },
{ title: '类型', slotName: 'type' },
{ title: '操作', slotName: 'menu', width: 260 }
]
}
},
mounted() {
this.fetchClientList()
},
methods: {
fetchClientList() {
this.$api.sys.clientList().then(res => {
if (res.code === 200) {
this.options = res.data.map(e => {
e.label = e.name
e.value = e.appid
return e
})
this.client = this.options[0].appid
this.fetchMenus()
}
})
},
fetchMenus() {
this.$api.sys.menuTree({ appid: this.client }).then(res => {
if (res.code === 200) {
this.list = res.data
}
})
},
remove(id) {
this.$api.sys.menuRemove({ ids: id }).then(res => {
if (res.code === 200) {
this.$notification.success(res.msg)
this.fetchClientList()
} else {
this.$notification.error(res.msg)
}
})
}
}
}
</script>
<style lang="scss" scoped></style>

84
src/views/system/role.vue Normal file
View File

@ -0,0 +1,84 @@
<template>
<div>
<navbar title="权限管理"></navbar>
<div class="container">
<div class="flex flex-center flex-justify-between">
<a-select
:options="options"
:model-value="client"
style="width: 360px"
></a-select>
<add-role :app-id="client" @ok="fetchClientList" />
</div>
<div class="mt-20">
<a-table :columns="columns" :data="list">
<template #menu="{ record }">
<div>
<edit-role
:app-id="client"
:id="record.id"
:menu-ids="record.menuIds"
@ok="fetchRole"
/>
</div>
</template>
</a-table>
</div>
</div>
</div>
</template>
<script>
import navbar from '@/components/navbar/index.vue'
import addRole from '@/views/system/components/add-role.vue'
import editRole from '@/views/system/components/edit-role.vue'
export default {
components: {
navbar,
addRole,
editRole
},
data() {
return {
client: '',
options: [],
list: [],
columns: [
{ title: '名称', dataIndex: 'name' },
{ title: 'code', dataIndex: 'code' },
{ title: '排序', dataIndex: 'sort' },
{ title: '备注', dataIndex: 'remark' },
{ title: '操作', slotName: 'menu' }
]
}
},
mounted() {
this.fetchClientList()
},
methods: {
fetchClientList() {
this.$api.sys.clientList().then(res => {
if (res.code === 200) {
this.options = res.data.map(e => {
e.label = e.name
e.value = e.appId
return e
})
this.client = this.options[0].appId
this.fetchRole()
}
})
},
fetchRole() {
this.$api.sys.roleList({ appId: this.client }).then(res => {
if (res.code === 200) {
this.list = res.data
}
})
}
}
}
</script>
<style lang="scss" scoped></style>

56
vite.config.js Normal file
View File

@ -0,0 +1,56 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
import Pages from 'vite-plugin-pages'
import Layouts from 'vite-plugin-vue-layouts'
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
Layouts({
layoutsDirs: 'src/layout',
defaultLayout: 'index'
}),
Pages({
dirs: [
{
dir: 'src/pages',
baseRoute: ''
},
{
dir: 'src/views',
baseRoute: ''
}
],
exclude: ['**/components/*.vue']
})
],
resolve: {
alias: {
'@': path.resolve(path.resolve(), 'src')
}
},
css: {
preprocessorOptions: {
scss: {
api: 'modern-compiler' // or 'modern'
}
}
},
server: {
hmr: true,
watch: {
usePolling: true
},
proxy: {
'/api': {
// 正式环境地址
// target: 'https://www.zkfgcloud.com',
target: 'http://127.0.0.1:3000',
changeOrigin: true,
rewrite: path => path.replace(/^\/api/, '')
}
}
}
})

3351
yarn.lock Normal file

File diff suppressed because it is too large Load Diff