Merge branch 'dev'

This commit is contained in:
dataeaseShu
2025-07-16 12:12:28 +08:00
52 changed files with 3773 additions and 149 deletions
+1
View File
@@ -37,6 +37,7 @@
"vue-dompurify-html": "^5.3.0",
"vue-i18n": "^9.14.4",
"vue-router": "^4.5.0",
"vue-types": "^6.0.0",
"web-storage-cache": "^1.1.1"
},
"devDependencies": {
+27
View File
@@ -0,0 +1,27 @@
import { request } from '@/utils/request'
export const userImportApi = {
downExcelTemplateApi: () => request.post('/user/excelTemplate', {}, { responseType: 'blob' }),
importUserApi: (data: any) =>
request.post('/user/batchImport', data, {
headers: {
'Content-Type': 'multipart/form-data',
},
}),
downErrorRecordApi: (key: string) =>
request.get(`/user/errorRecord/${key}`, { responseType: 'blob' }),
clearErrorApi: (key: string) => {
request.get(`/user/clearErrorRecord/${key}`)
},
}
export const userApi = {
pager: (params: any, pageNumber: number, pageSize: number) =>
request.get(`/user/pager/${pageNumber}/${pageSize}${params}`),
add: (data: any) => request.post('/user', data),
edit: (data: any) => request.put('/user', data),
clearErrorApi: (key: string) => request.get(`/user/clearErrorRecord/${key}`),
delete: (key: string) => request.delete(`/user/${key}`),
get: (key: string) => request.get(`/user/${key}`),
pwd: (data: any) => request.put('/user/pwd', data),
}
+9
View File
@@ -0,0 +1,9 @@
import { request } from '@/utils/request'
export const workspaceUserList = (params: any, pageNum: number, pageSize: number) =>
request.get(`/system/workspace/uws/pager/${pageNum}/${pageSize}`, { params })
export const workspaceCreate = (data: any) => request.post('/system/workspace/uws', data)
export const workspaceDelete = (data: any) => request.delete('/system/workspace/uws', data)
export const workspaceList = () => request.get('/system/workspace')
export const workspaceDetail = (id: any) => request.get(`/system/workspace/${id}`)
+11
View File
@@ -0,0 +1,11 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1617_1677)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.666748 7.99996C0.666748 12.0505 3.94956 15.3333 8.00008 15.3333C12.0506 15.3333 15.3334 12.0505 15.3334 7.99996C15.3334 3.94944 12.0506 0.666626 8.00008 0.666626C3.94956 0.666626 0.666748 3.94944 0.666748 7.99996Z" fill="#34C724"/>
<path d="M10.7621 5.98431L7.22639 9.51998L5.49305 7.78665C5.43054 7.72416 5.34578 7.68905 5.25739 7.68905C5.169 7.68905 5.08423 7.72416 5.02172 7.78665L4.55072 8.25831C4.48823 8.32082 4.45312 8.40559 4.45312 8.49398C4.45312 8.58237 4.48823 8.66714 4.55072 8.72964L6.99072 11.17C7.05323 11.2325 7.138 11.2676 7.22639 11.2676C7.31477 11.2676 7.39954 11.2325 7.46205 11.17L11.7047 6.92731C11.7672 6.8648 11.8023 6.78003 11.8023 6.69165C11.8023 6.60326 11.7672 6.51849 11.7047 6.45598L11.2334 5.98431C11.1709 5.92182 11.0861 5.88672 10.9977 5.88672C10.9093 5.88672 10.8246 5.92182 10.7621 5.98431Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_1617_1677">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,3 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.33317 6.66807V12.5416C9.33317 12.7027 9.20259 12.8333 9.0415 12.8333H8.45817C8.29709 12.8333 8.1665 12.7027 8.1665 12.5416V6.41663C8.1665 6.3309 8.185 6.24949 8.21821 6.17618C8.26253 6.06615 8.34187 5.96924 8.45144 5.90383L11.6665 3.98466V2.33329H2.33317V4.00501L5.45111 5.86895C5.6741 5.95092 5.83317 6.1652 5.83317 6.41663V10.5C5.83317 10.661 5.70259 10.7916 5.5415 10.7916H4.95817C4.79709 10.7916 4.6665 10.661 4.6665 10.5V6.68798L1.4429 4.76088C1.37054 4.71762 1.31141 4.66061 1.26688 4.59514C1.20538 4.54167 1.1665 4.46286 1.1665 4.37496V1.74996C1.1665 1.42779 1.42767 1.16663 1.74984 1.16663H12.2498C12.4109 1.16663 12.5568 1.23192 12.6623 1.33748C12.7679 1.44304 12.8332 1.58888 12.8332 1.74996V4.19593C12.8377 4.2369 12.8376 4.27801 12.8332 4.31851V4.37496C12.8332 4.4732 12.7846 4.5601 12.7102 4.61294C12.6698 4.66245 12.6203 4.70587 12.5624 4.74046L9.33317 6.66807Z" fill="#1F2329"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

@@ -4,12 +4,12 @@
<path d="M11.5556 11.1111C11.5556 13.5657 13.5454 15.5556 16 15.5556C18.4546 15.5556 20.4444 13.5657 20.4444 11.1111C20.4444 8.65653 18.4546 6.66669 16 6.66669C13.5454 6.66669 11.5556 8.65653 11.5556 11.1111Z" fill="url(#paint1_linear_477_6741)"/>
<defs>
<linearGradient id="paint0_linear_477_6741" x1="16" y1="6.66669" x2="16" y2="24.8889" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
<stop stop-color="#ffffff"/>
<stop offset="1" stop-color="#ffffff" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint1_linear_477_6741" x1="16" y1="6.66669" x2="16" y2="24.8889" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
<stop stop-color="#ffffff"/>
<stop offset="1" stop-color="#ffffff" stop-opacity="0"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.3334 4.33333C10.3334 2.49233 8.84004 1 7.00004 1C5.16004 1 3.66671 2.49233 3.66671 4.33333C3.66671 6.17433 5.16004 7.66667 7.00004 7.66667C8.84004 7.66667 10.3334 6.17433 10.3334 4.33333ZM7.00004 6.33333C5.89671 6.33333 5.00004 5.438 5.00004 4.33333C5.00004 3.22867 5.89671 2.33333 7.00004 2.33333C8.10337 2.33333 9.00004 3.22867 9.00004 4.33333C9.00004 5.438 8.10337 6.33333 7.00004 6.33333Z" fill="#646A73"/>
<path d="M2.66671 12.6667C2.66671 12.4363 2.69671 12.213 2.75004 12C3.04671 10.85 4.09004 10 5.33337 10H8.01668C8.20077 10 8.35001 9.85076 8.35001 9.66667V9C8.35001 8.81591 8.20077 8.66667 8.01668 8.66667H5.33337C3.12337 8.66667 1.33337 10.4577 1.33337 12.6667V14C1.33337 14.3683 1.63337 14.6667 2.00004 14.6667H8.01668C8.20077 14.6667 8.35001 14.5174 8.35001 14.3333V13.6667C8.35001 13.4826 8.20077 13.3333 8.01668 13.3333H2.66671V12.6667Z" fill="#646A73"/>
<path d="M12.1312 10.3738L13.11 11.3526H9.66671C9.48261 11.3526 9.33337 11.5019 9.33337 11.686V12.3526C9.33337 12.5367 9.48261 12.686 9.66671 12.686H13.1188L12.1312 13.6736C12.001 13.8038 12.001 14.0148 12.1312 14.145L12.6026 14.6164C12.7328 14.7466 12.9438 14.7466 13.074 14.6164L15.431 12.2594C15.5612 12.1292 15.5612 11.9182 15.431 11.788L13.074 9.43096C12.9438 9.30079 12.7328 9.30079 12.6026 9.43096L12.1312 9.90237C12.001 10.0325 12.001 10.2436 12.1312 10.3738Z" fill="#646A73"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.99992 0.333252C3.76574 0.333252 0.333252 3.76574 0.333252 7.99992C0.333252 12.2341 3.76574 15.6666 7.99992 15.6666C12.2341 15.6666 15.6666 12.2341 15.6666 7.99992C15.6666 3.76574 12.2341 0.333252 7.99992 0.333252ZM12.6514 11.2374L4.76246 3.34847C5.68019 2.70844 6.79587 2.33325 7.99992 2.33325C11.1295 2.33325 13.6666 4.87031 13.6666 7.99992C13.6666 9.20397 13.2914 10.3196 12.6514 11.2374ZM2.33325 7.99992C2.33325 6.79599 2.70837 5.68041 3.34829 4.76272L11.2371 12.6516C10.3194 13.2915 9.20385 13.6666 7.99992 13.6666C4.87031 13.6666 2.33325 11.1295 2.33325 7.99992Z" fill="#8F959E"/>
</svg>

After

Width:  |  Height:  |  Size: 701 B

@@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.11117 3.11117V12.8889H5.33339V14.2223H2.66672C2.21895 14.2223 1.77783 13.8036 1.77783 13.3334V2.66672C1.77783 2.19651 2.21895 1.77783 2.66672 1.77783H13.3334C13.7812 1.77783 14.2223 2.19651 14.2223 2.66672V13.3334C14.2223 13.8036 13.7812 14.2223 13.3334 14.2223H10.6667V12.8889H12.8889V3.11117H3.11117Z" fill="#1F2329"/>
<path d="M7.33339 13.7778C7.33339 14.0233 7.53237 14.2223 7.77783 14.2223H8.22228C8.46774 14.2223 8.66672 14.0233 8.66672 13.7778V7.14731L10.5237 9.00427C10.6973 9.17784 10.9787 9.17784 11.1522 9.00427L11.4665 8.69001C11.6401 8.51644 11.6401 8.23503 11.4665 8.06147L8.3238 4.91877C8.28396 4.87893 8.23844 4.84823 8.18984 4.82668C8.02382 4.74494 7.81738 4.77314 7.67924 4.91128L4.53655 8.05397C4.36298 8.22754 4.36298 8.50895 4.53655 8.68251L4.85082 8.99678C5.02438 9.17035 5.30579 9.17035 5.47936 8.99678L7.33339 7.14275V13.7778Z" fill="#1F2329"/>
</svg>

After

Width:  |  Height:  |  Size: 984 B

@@ -0,0 +1,3 @@
<svg width="12" height="12" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
<path d="M5.99993 5.29289L9.85868 1.43413C9.95632 1.3365 10.1146 1.3365 10.2122 1.43413L10.5658 1.78769C10.6634 1.88532 10.6634 2.04361 10.5658 2.14124L6.70703 6L10.5658 9.85875C10.6634 9.95639 10.6634 10.1147 10.5658 10.2123L10.2122 10.5659C10.1146 10.6635 9.95632 10.6635 9.85868 10.5659L5.99993 6.7071L2.14117 10.5659C2.04354 10.6635 1.88525 10.6635 1.78762 10.5659L1.43406 10.2123C1.33643 10.1147 1.33643 9.95639 1.43406 9.85875L5.29282 6L1.43406 2.14124C1.33643 2.04361 1.33643 1.88532 1.43406 1.78769L1.78762 1.43413C1.88525 1.3365 2.04354 1.3365 2.14117 1.43413L5.99993 5.29289Z"/>
</svg>

After

Width:  |  Height:  |  Size: 680 B

@@ -0,0 +1,12 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1515_53088)">
<path d="M7.99984 15.3334C12.0499 15.3334 15.3332 12.0502 15.3332 8.00008C15.3332 3.94999 12.0499 0.666748 7.99984 0.666748C3.94975 0.666748 0.666504 3.94999 0.666504 8.00008C0.666504 12.0502 3.94975 15.3334 7.99984 15.3334Z" fill="#1CBA90"/>
<path d="M7.53927 5.64121C7.57971 5.65796 7.62306 5.66659 7.66683 5.66659H8.3335C8.37727 5.66659 8.42062 5.65796 8.46106 5.64121C8.5015 5.62446 8.53825 5.59991 8.5692 5.56895C8.60015 5.538 8.62471 5.50126 8.64146 5.46081C8.65821 5.42037 8.66683 5.37703 8.66683 5.33325V4.66658C8.66683 4.62281 8.65821 4.57947 8.64146 4.53902C8.62471 4.49858 8.60015 4.46184 8.5692 4.43088C8.53825 4.39993 8.5015 4.37538 8.46106 4.35863C8.42062 4.34187 8.37727 4.33325 8.3335 4.33325H7.66683C7.62306 4.33325 7.57971 4.34187 7.53927 4.35863C7.49883 4.37538 7.46208 4.39993 7.43113 4.43088C7.40017 4.46184 7.37562 4.49858 7.35887 4.53902C7.34212 4.57947 7.3335 4.62281 7.3335 4.66658V5.33325C7.3335 5.37703 7.34212 5.42037 7.35887 5.46081C7.37562 5.50126 7.40017 5.538 7.43113 5.56895C7.46208 5.59991 7.49883 5.62446 7.53927 5.64121Z" fill="white"/>
<path d="M7.3335 7.66659V10.3333H6.66683C6.62306 10.3333 6.57971 10.3419 6.53927 10.3586C6.49883 10.3754 6.46208 10.3999 6.43113 10.4309C6.40017 10.4618 6.37562 10.4986 6.35887 10.539C6.34212 10.5795 6.3335 10.6228 6.3335 10.6666V11.3333C6.3335 11.377 6.34212 11.4204 6.35887 11.4608C6.37562 11.5013 6.40017 11.538 6.43113 11.569C6.46208 11.5999 6.49883 11.6245 6.53927 11.6412C6.57971 11.658 6.62306 11.6666 6.66683 11.6666H9.3335C9.37727 11.6666 9.42062 11.658 9.46106 11.6412C9.5015 11.6245 9.53825 11.5999 9.5692 11.569C9.60015 11.538 9.62471 11.5013 9.64146 11.4608C9.65821 11.4204 9.66683 11.377 9.66683 11.3333V10.6666C9.66683 10.6228 9.65821 10.5795 9.64146 10.539C9.62471 10.4986 9.60015 10.4618 9.5692 10.4309C9.53825 10.3999 9.5015 10.3754 9.46106 10.3586C9.42062 10.3419 9.37727 10.3333 9.3335 10.3333H8.66683V6.66659C8.66696 6.62277 8.65843 6.57937 8.64173 6.53887C8.62502 6.49837 8.60047 6.46157 8.56949 6.43059C8.53851 6.39961 8.50172 6.37506 8.46121 6.35836C8.42071 6.34165 8.37731 6.33312 8.3335 6.33325H7.00016C6.95639 6.33325 6.91304 6.34187 6.8726 6.35863C6.83216 6.37538 6.79541 6.39993 6.76446 6.43088C6.73351 6.46184 6.70895 6.49858 6.6922 6.53902C6.67545 6.57947 6.66683 6.62281 6.66683 6.66659V7.33325C6.66683 7.37703 6.67545 7.42037 6.6922 7.46081C6.70895 7.50126 6.73351 7.538 6.76446 7.56895C6.79541 7.59991 6.83216 7.62446 6.8726 7.64121C6.91304 7.65796 6.95639 7.66659 7.00016 7.66659H7.3335Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_1515_53088">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

+4
View File
@@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 8.66667H6C4.15905 8.66667 2 9.95432 2 12.2667V14.0667C2 14.398 2.29848 14.6667 2.66667 14.6667H13.3333C13.7015 14.6667 14 14.398 14 14.0667V12.2667C14 9.95237 11.841 8.66667 10 8.66667Z" fill="#8F959E"/>
<path d="M4.66667 4.66667C4.66667 6.50762 6.15905 8 8 8C9.84095 8 11.3333 6.50762 11.3333 4.66667C11.3333 2.82572 9.84095 1.33333 8 1.33333C6.15905 1.33333 4.66667 2.82572 4.66667 4.66667Z" fill="#8F959E"/>
</svg>

After

Width:  |  Height:  |  Size: 527 B

@@ -0,0 +1,4 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.25 9.75H6.75C4.67893 9.75 2.25 11.1986 2.25 13.8V15.825C2.25 16.1978 2.58579 16.5 3 16.5H15C15.4142 16.5 15.75 16.1978 15.75 15.825V13.8C15.75 11.1964 13.3211 9.75 11.25 9.75Z" fill="#1CBA90"/>
<path d="M5.25 5.25C5.25 7.32107 6.92893 9 9 9C11.0711 9 12.75 7.32107 12.75 5.25C12.75 3.17893 11.0711 1.5 9 1.5C6.92893 1.5 5.25 3.17893 5.25 5.25Z" fill="#1CBA90"/>
</svg>

After

Width:  |  Height:  |  Size: 478 B

@@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.35399 11.2488H7.62467L14.3787 4.4946C14.6108 4.26253 14.6113 3.88643 14.3798 3.65375L12.3342 1.59735C12.1017 1.36485 11.7247 1.36485 11.4922 1.59735L11.0723 2.01722L11.074 2.01887L4.75864 8.3828V10.6535C4.75864 10.9823 5.02519 11.2488 5.35399 11.2488ZM11.8943 2.84351L13.1 4.05552L12.0797 5.07678L10.8803 3.87736L11.8943 2.84351ZM11.2382 5.91913L7.1225 10.0388H7.12068L5.9687 8.88498L10.0465 4.72746L11.2382 5.91913Z" fill="#646A73"/>
<path d="M8.76313 2.316H2.44339C2.07805 2.316 1.78189 2.61216 1.78189 2.9775V13.5615C1.78189 13.9268 2.07805 14.223 2.44339 14.223H13.0274C13.3927 14.223 13.6889 13.9268 13.6889 13.5615V7.1614L12.3576 8.50072V12.8982H3.11313V3.6408H7.46239L8.76313 2.316Z" fill="#646A73"/>
</svg>

After

Width:  |  Height:  |  Size: 823 B

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14 12.3333V14C14 14.3682 13.7015 14.6667 13.3333 14.6667H2.66667C2.29848 14.6667 2 14.3682 2 14V12.3333C2 12.1492 2.14924 12 2.33333 12H3C3.18409 12 3.33333 12.1492 3.33333 12.3333V13.3333H12.6667V12.3333C12.6667 12.1492 12.8159 12 13 12H13.6667C13.8508 12 14 12.1492 14 12.3333ZM8.66667 4.33329L10.6736 6.34027C10.8038 6.47045 11.0149 6.47045 11.1451 6.34027L11.6165 5.86887C11.7466 5.73869 11.7466 5.52764 11.6165 5.39746L8.31663 2.09763C8.25154 2.03254 8.16623 2 8.08092 2C7.99562 2 7.91031 2.03254 7.84522 2.09763L4.54539 5.39746C4.41521 5.52764 4.41521 5.73869 4.54539 5.86887L5.01679 6.34027C5.14697 6.47045 5.35802 6.47045 5.4882 6.34027L7.33334 4.49513V11.6904C7.33334 11.8745 7.48257 12.0238 7.66667 12.0238H8.33334C8.51743 12.0238 8.66667 11.8745 8.66667 11.6904V4.33329Z" fill="#2B2F36"/>
</svg>

After

Width:  |  Height:  |  Size: 913 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,3 @@
import DrawerFilter from './src/DrawerFilter.vue'
export { DrawerFilter }
@@ -0,0 +1,115 @@
<script setup lang="ts">
import { propTypes } from '@/utils/propTypes'
import { reactive } from 'vue'
import { useEmitt } from '@/utils/useEmitt'
const props = defineProps({
optionList: propTypes.arrayOf(
propTypes.shape({
id: propTypes.string,
name: propTypes.string,
})
),
index: propTypes.number,
title: propTypes.string,
})
const state = reactive<{ activeStatus: any[]; optionList: any[] }>({
activeStatus: [],
optionList: [],
})
const nodeChange = (id: string | number) => {
const len = state.activeStatus.indexOf(id)
if (len >= 0) {
state.activeStatus.splice(len, 1)
} else {
state.activeStatus.push(id)
}
emits('filter-change', state.activeStatus)
}
const emits = defineEmits(['filter-change'])
const clear = (index: number) => {
if (index !== props.index) return
state.activeStatus = []
}
useEmitt({
name: 'clear-drawer_main',
callback: clear,
})
</script>
<template>
<div class="draw-filter_enum">
<span>{{ title }}</span>
<div class="filter-item">
<span
v-for="ele in props.optionList"
:key="ele.id"
class="item"
:class="[state.activeStatus.includes(ele.id) ? 'active' : '']"
@click="nodeChange(ele.id)"
>{{ ele.name }}</span
>
</div>
</div>
</template>
<style lang="less" scope>
.draw-filter_enum {
margin-bottom: 4px;
> :nth-child(1) {
color: var(--deTextSecondary, #1f2329);
font-family: var(--de-custom_font, 'PingFang');
font-style: normal;
font-weight: 400;
font-size: 14px;
line-height: 22px;
white-space: nowrap;
}
.filter-item {
margin-top: 8px;
.item,
.more {
font-family: var(--de-custom_font, 'PingFang');
white-space: nowrap;
font-size: 14px;
font-weight: 400;
line-height: 24px;
margin-right: 12px;
text-align: center;
padding: 1px 6px;
background: var(--deTextPrimary5, #f5f6f7);
color: var(--deTextPrimary, #1f2329);
border-radius: 2px;
cursor: pointer;
display: inline-block;
margin-bottom: 12px;
}
.active,
.more:hover {
background: var(--primary10, var(--ed-color-primary-1a, #1cba9033));
color: var(--primaryselect, #0c296e);
}
.more {
white-space: nowrap;
display: inline-flex;
align-items: center;
i {
margin-right: 5px;
}
}
}
}
</style>
<style lang="less">
.filter-popper {
padding: 0 !important;
background: #fff !important;
}
</style>
@@ -0,0 +1,91 @@
<script setup lang="ts">
import { propTypes } from '@/utils/propTypes'
import { ElSelect, ElOption } from 'element-plus-secondary'
import { computed, reactive } from 'vue'
import { useEmitt } from '@/utils/useEmitt'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const props = defineProps({
optionList: propTypes.arrayOf(
propTypes.shape({
id: propTypes.string,
name: propTypes.string,
})
),
index: propTypes.number,
title: propTypes.string,
property: {
type: Object,
default: () => ({}),
},
})
const state = reactive({
activeStatus: [],
})
const emits = defineEmits(['filter-change'])
const selectStatus = (ids: any[]) => {
emits(
'filter-change',
ids.map((item) => item.id || item.value)
)
}
const optionListNotSelect = computed(() => {
return [...(props.optionList as any[])]
})
const clear = (index: number) => {
if (index !== props.index) return
state.activeStatus = []
}
useEmitt({
name: 'clear-drawer_main',
callback: clear,
})
</script>
<template>
<div class="draw-filter_base">
<span>{{ title }}</span>
<div class="filter-item">
<el-select
v-model="state.activeStatus"
:teleported="false"
style="width: 100%"
value-key="id"
filterable
:placeholder="t('datasource.Please_select') + props.property.placeholder"
multiple
@change="selectStatus"
>
<el-option
v-for="item in optionListNotSelect"
:key="item.name"
:label="item.name"
:value="item"
/>
</el-select>
</div>
</div>
</template>
<style lang="less" scope>
.draw-filter_base {
margin-bottom: 16px;
> :nth-child(1) {
color: var(--deTextSecondary, #1f2329);
font-family: var(--de-custom_font, 'PingFang');
font-style: normal;
font-weight: 400;
font-size: 14px;
line-height: 22px;
white-space: nowrap;
}
.filter-item {
margin-top: 8px;
}
}
</style>
@@ -0,0 +1,3 @@
import DrawerMain from './src/DrawerMain.vue'
export { DrawerMain }
@@ -0,0 +1,163 @@
<script lang="ts" setup>
import { ref, reactive, computed } from 'vue'
import { ElDrawer, ElButton } from 'element-plus-secondary'
import { propTypes } from '@/utils/propTypes'
import DrawerFilter from '@/components/drawer-filter/src/DrawerFilter.vue'
import DrawerEnumFilter from '@/components/drawer-filter/src/DrawerEnumFilter.vue'
import { useEmitt } from '@/utils/useEmitt'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const props = defineProps({
filterOptions: propTypes.arrayOf(
propTypes.shape({
type: propTypes.string,
field: propTypes.string,
option: propTypes.array,
title: propTypes.string,
property: propTypes.shape({}),
})
),
title: propTypes.string,
})
const componentList = computed(() => {
return props.filterOptions as any[]
})
const state = reactive<{ conditions: any[] }>({
conditions: [],
})
const userDrawer = ref(false)
const init = () => {
userDrawer.value = true
}
const clear = (index: any) => {
useEmitt().emitter.emit('clear-drawer_main', index)
}
const cleanrInnerValue = (index: number) => {
const field = componentList.value[index]?.field
if (!field) {
return
}
clear(index)
for (let i = 0; i < state.conditions.length; i++) {
if (state.conditions[i].field === field) {
state.conditions[i].value = []
}
}
}
const clearInnerTag = (index?: number) => {
if (isNaN(index as number)) {
for (let i = 0; i < componentList.value.length; i++) {
clear(i)
}
return
}
const condition = state.conditions[index as number]
const field = condition?.field
for (let i = 0; i < componentList.value.length; i++) {
if (componentList.value[i].field === field) {
clear(i)
}
}
}
const clearFilter = (id?: number) => {
clearInnerTag(id)
if (isNaN(id as number)) {
const len = state.conditions.length
state.conditions.splice(0, len)
} else {
state.conditions.splice(id as number, 1)
}
trigger()
}
const filterChange = (value: any, field: any, operator: any) => {
let exits = false
let len = state.conditions.length
while (len--) {
const condition = state.conditions[len]
if (condition.field === field) {
exits = true
condition['value'] = value
}
if (!condition?.value?.length) {
state.conditions.splice(len, 1)
}
}
if (!exits && value?.length) {
state.conditions.push({ field, value, operator })
}
treeFilterChange(value, field, operator)
}
const reset = () => {
clearFilter()
userDrawer.value = false
}
const close = () => {
userDrawer.value = false
}
const emits = defineEmits(['trigger-filter', 'tree-filter-change'])
const trigger = () => {
emits('trigger-filter', state.conditions)
}
const treeFilterChange = (value: any, field: any, operator: any) => {
emits('tree-filter-change', {
value,
field,
operator,
})
}
defineExpose({
init,
clearFilter,
close,
cleanrInnerValue,
})
</script>
<template>
<el-drawer
v-model="userDrawer"
:title="t('user.filter_conditions')"
size="600px"
modal-class="drawer-main-container"
direction="rtl"
>
<div v-for="(component, index) in componentList" :key="index">
<drawer-filter
v-if="component.type === 'select'"
:option-list="component.option"
:title="component.title"
:index="index"
:property="component.property"
@filter-change="(v) => filterChange(v, component.field, 'in')"
/>
<drawer-enum-filter
v-if="component.type === 'enum'"
:option-list="component.option"
:title="component.title"
:index="index"
@filter-change="(v) => filterChange(v, component.field, 'in')"
/>
</div>
<template #footer>
<el-button secondary @click="reset">{{ t('common.reset') }}</el-button>
<el-button type="primary" @click="trigger">{{ t('common.search') }}</el-button>
</template>
</el-drawer>
</template>
<style lang="less">
.drawer-main-container {
.ed-drawer__body {
padding: 16px 24px 80px !important;
}
.ed-drawer__footer {
padding: 16px 24px;
height: 64px;
}
}
</style>
@@ -0,0 +1,91 @@
import FilterText from './src/FilterText.vue'
export { FilterText }
const fieldText = (field: string, options: any[]) => {
for (let index = 0; index < options.length; index++) {
const element = options[index]
if (field === element.field) {
return element.title
}
}
}
const valueTextFormTree = (val: string, options: any[]) => {
let result = null
const stack = [...options]
while (stack.length) {
const item = stack.pop()
if (item.value === val) {
result = item.label
break
}
if (item.children?.length) {
item.children.forEach((kid: any) => stack.push(kid))
}
}
return result || val
}
const timestampFormatDate = (value: string) => {
if (!value) {
return '-'
}
return new Date(value).toLocaleString()
}
const valueText = (field: string, val: string, options: any[]) => {
for (let index = 0; index < options.length; index++) {
const element = options[index]
if (field === element.field) {
let isTree = false
const selectOption = element.option
for (let i = 0; i < selectOption.length; i++) {
const item = selectOption[i] as any
if (item.children !== undefined) {
isTree = true
break
}
if (item.id === val || item.value === val) {
return item.name || item.label
}
}
if (element.type === 'time') {
return timestampFormatDate(val)
}
if (isTree) {
return valueTextFormTree(val, selectOption)
}
}
}
return val
}
export const convertFilterText = (conditions: any[], options: any[]) => {
console.log('conditions', conditions, options)
const result: any[] = []
conditions.forEach((condition) => {
const field = condition.field
const ope = condition.operator
const value = condition.value
const fieldName = fieldText(field, options)
const textOpe = ':'
let separator = ','
if (ope === 'in') {
separator = '、'
}
if (ope === 'between') {
separator = '~'
}
let valText = value
if (value) {
if (Array.isArray(value)) {
const valTextArray = value.map((val) => valueText(field, val, options))
valText = valTextArray.join(separator)
}
if (valText?.length) {
const obj = fieldName + textOpe + valText
result.push(obj)
}
}
})
return result
}
@@ -0,0 +1,206 @@
<script lang="ts" setup>
import icon_left_outlined from '@/assets/svg/common-back.svg'
import icon_close_outlined from '@/assets/svg/icon_close_outlined.svg'
import icon_deleteTrash_outlined from '@/assets/svg/icon_delete.svg'
import icon_right_outlined from '@/assets/svg/icon_right_outlined.svg'
import { nextTick, ref, watch } from 'vue'
import { Icon } from '@/components/icon-custom'
import { ElButton, ElDivider, ElIcon } from 'element-plus-secondary'
import { propTypes } from '@/utils/propTypes'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const props = defineProps({
filterTexts: {
type: Array<string>,
default: () => [],
},
total: propTypes.number.def(0),
})
const emits = defineEmits(['clearFilter'])
const container = ref<any>(null)
const showScroll = ref(false)
const scrollPre = () => {
container.value!.scrollLeft -= 10
if (container.value.scrollLeft <= 0) {
container.value.scrollLeft = 0
}
}
const scrollNext = () => {
container.value.scrollLeft += 10
const width = container.value.scrollWidth - container.value.offsetWidth
if (container.value.scrollLeft > width) {
container.value.scrollLeft = width
}
}
const clearFilter = (index?: number) => {
emits('clearFilter', index)
}
const clearFilterAll = () => {
emits('clearFilter', 'empty')
}
watch(
() => props.filterTexts,
() => {
nextTick(() => {
showScroll.value = container.value?.scrollWidth > container.value?.offsetWidth
})
},
{ deep: true }
)
</script>
<template>
<div v-if="filterTexts.length" class="filter-texts">
<span class="sum">{{ total }}</span>
<span class="title">{{ t('common.result_count') }}</span>
<el-divider direction="vertical" />
<el-icon v-if="showScroll" class="arrow-left arrow-filter" @click="scrollPre">
<Icon name="icon_left_outlined"><icon_left_outlined class="svg-icon" /></Icon>
</el-icon>
<div ref="container" class="filter-texts-container">
<p v-for="(ele, index) in filterTexts" :key="ele" class="text">
<el-tooltip effect="dark" :content="ele" placement="top-start">
{{ ele }}
</el-tooltip>
<el-icon @click="clearFilter(index)">
<Icon name="icon_close_outlined"><icon_close_outlined class="svg-icon" /></Icon>
</el-icon>
</p>
<el-button
v-if="!showScroll"
type="text"
class="clear-btn clear-btn-inner"
@click="clearFilterAll"
>
<template #icon>
<Icon name="icon_delete-trash_outlined"
><icon_deleteTrash_outlined class="svg-icon"
/></Icon>
</template>
{{ t('common.clear_filter') }}</el-button
>
</div>
<el-icon v-if="showScroll" class="arrow-right arrow-filter" @click="scrollNext">
<Icon name="icon_right_outlined"><icon_right_outlined class="svg-icon" /></Icon>
</el-icon>
<el-button
v-if="showScroll"
type="text"
class="clear-btn"
style="height: 24px; line-height: 24px"
@click="clearFilterAll"
>
<template #icon>
<Icon name="icon_delete-trash_outlined"
><icon_deleteTrash_outlined class="svg-icon"
/></Icon>
</template>
{{ t('common.clear_filter') }}</el-button
>
</div>
</template>
<style lang="less" scoped>
.filter-texts {
display: flex;
align-items: center;
margin: 16px 0;
font-weight: 400;
.sum {
color: #1f2329;
}
.title {
color: #999999;
margin-left: 8px;
}
.text {
max-width: 280px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding: 1px 22px 1px 6px;
display: inline-block;
align-items: center;
color: #0c296e;
font-size: 14px;
line-height: 22px;
background: var(--ed-color-primary-1a, rgba(51, 112, 255, 0.1));
border-radius: 2px;
margin: 0;
margin-right: 8px;
position: relative;
i {
position: absolute;
right: 6px;
top: 50%;
font-size: 12px;
transform: translateY(-50%);
cursor: pointer;
}
}
.clear-btn {
color: #646a73;
padding: 0 4px;
border-radius: 6px;
height: 24px;
line-height: 24px;
}
.clear-btn:hover {
background: #1f23291a;
}
.clear-btn:active {
background: #1f232933;
}
.filter-texts-container::-webkit-scrollbar {
display: none;
}
.arrow-filter {
font-size: 16px;
width: 24px;
height: 24px;
cursor: pointer;
color: #646a73;
display: flex;
justify-content: center;
align-items: center;
}
.arrow-filter:hover {
background: rgba(31, 35, 41, 0.1);
border-radius: 4px;
}
.ed-icon-arrow-right.arrow-filter {
margin-left: 5px;
}
.ed-icon-arrow-left.arrow-filter {
margin-right: 5px;
}
.filter-texts-container {
flex: 1;
overflow-x: auto;
white-space: nowrap;
height: 24px;
.clear-btn-inner {
margin-top: -16px;
}
}
}
</style>
+24 -4
View File
@@ -9,11 +9,22 @@ import icon_moments_categories_outlined from '@/assets/svg/icon_moments-categori
import icon_side_fold_outlined from '@/assets/svg/icon_side-fold_outlined.svg'
import icon_side_expand_outlined from '@/assets/svg/icon_side-expand_outlined.svg'
import { useRoute, useRouter } from 'vue-router'
const router = useRouter()
const collapse = ref(false)
import { useEmitt } from '@/utils/useEmitt'
const handleCollapseChange = (val: any = true) => {
collapse.value = val
}
useEmitt({
name: 'collapse-change',
callback: handleCollapseChange,
})
const handleFoldExpand = () => {
collapse.value = !collapse.value
}
const toWorkspace = () => {
router.push('/')
}
@@ -31,11 +42,16 @@ const showSysmenu = computed(() => {
<Workspace v-if="!showSysmenu" :collapse="collapse"></Workspace>
<Menu :collapse="collapse"></Menu>
<div class="bottom">
<div v-if="showSysmenu" class="back-to_workspace" @click="toWorkspace">
<div
v-if="showSysmenu"
class="back-to_workspace"
:class="collapse && 'collapse'"
@click="toWorkspace"
>
<el-icon size="16">
<icon_moments_categories_outlined></icon_moments_categories_outlined>
</el-icon>
{{ collapse ? '' : '返回工作空间' }}
{{ collapse ? '' : $t('workspace.return_to_workspace') }}
</div>
<div class="personal-info">
<Person :collapse="collapse" :in-sysmenu="showSysmenu"></Person>
@@ -85,14 +101,17 @@ const showSysmenu = computed(() => {
line-height: 22px;
width: calc(100% - 32px);
.back-to_workspace {
background-color: #1f23290a;
border: 1px solid #d0d3d6;
display: flex;
align-items: center;
justify-content: center;
border-radius: 6px;
height: 40px;
cursor: pointer;
&:not(.collapse) {
background: #1f23290a;
border: 1px solid #d9dcdf;
}
&:hover {
background-color: #1f23291a;
}
@@ -152,6 +171,7 @@ const showSysmenu = computed(() => {
flex: 1;
padding: 8px 8px 8px 0;
max-height: 100vh;
overflow-x: auto;
.content {
width: 100%;
+17 -2
View File
@@ -1,19 +1,22 @@
<script lang="ts">
import { h, defineComponent } from 'vue'
import { ElMenuItem, ElSubMenu, ElIcon } from 'element-plus-secondary'
import { useRouter } from 'vue-router'
import user from '@/assets/svg/icon_member_filled.svg'
import workspace from '@/assets/svg/icon_moments-categories_outlined.svg'
import model from '@/assets/svg/icon_dataset_filled.svg'
import ds from '@/assets/svg/ds.svg'
import dashboard from '@/assets/svg/dashboard.svg'
import chat from '@/assets/svg/chat.svg'
import icon_user from '@/assets/svg/icon_user.svg'
import { useRouter } from 'vue-router'
const iconMap = {
icon_ai: model,
ds,
dashboard,
chat,
user,
icon_user: icon_user,
workspace,
} as { [key: string]: any }
const titleWithIcon = (props: any) => {
@@ -73,6 +76,9 @@ const MenuItem = defineComponent({
},
})
/* const MenuItem = (props: any) => {
const MenuItem = (props: any) => {
const router = useRouter()
const { children, hidden, path } = props.menu
if (hidden) {
return null
@@ -81,6 +87,9 @@ const MenuItem = defineComponent({
return h(
ElSubMenu,
{ index: path, onClick: (e: any) => handleMenuClick(e) },
{
index: path,
},
{
title: () => titleWithIcon(props),
default: () => children.map((ele: any) => h(MenuItem, { menu: ele })),
@@ -92,6 +101,12 @@ const MenuItem = defineComponent({
return h(
ElMenuItem,
{ index: path, onClick: (e: any) => handleMenuClick(e) },
{
index: path,
onClick: () => {
router.push(path)
},
},
{
title: h('span', null, { default: () => title }),
default: h(iconCom, null, {
+32 -23
View File
@@ -1,6 +1,6 @@
<script lang="ts" setup>
import { computed, ref } from 'vue'
import Default_avatar from '@/assets/svg/icon-member-default.svg'
import { ref, computed } from 'vue'
import Default_avatar from '@/assets/workspace/default_avatar.png'
import icon_admin_outlined from '@/assets/svg/icon_admin_outlined.svg'
import icon_maybe_outlined from '@/assets/svg/icon-maybe_outlined.svg'
import icon_key_outlined from '@/assets/svg/icon-key_outlined.svg'
@@ -34,11 +34,11 @@ const languageList = [
value: 'en',
},
{
name: '中文',
name: t('common.simplified_chinese'),
value: 'zh-CN',
} /* ,
{
name: '繁體中文',
name: t('common.traditional_chinese'),
value: 'zh-CN',
}, */,
]
@@ -74,18 +74,14 @@ const logout = () => {
<el-popover popper-class="system-person" :placement="collapse ? 'right' : 'top'">
<template #reference>
<button class="person" :class="collapse && 'collapse'">
<el-icon size="32" class="default-avatar">
<Default_avatar></Default_avatar>
</el-icon>
<img class="default-avatar" :src="Default_avatar" width="32px" height="32px" />
<span v-if="!collapse" class="name">{{ name }}</span>
</button></template
>
<div class="popover">
<div class="popover-content">
<div class="info">
<el-icon style="transform: scale(1.25)" size="32">
<Default_avatar></Default_avatar>
</el-icon>
<img :src="Default_avatar" width="40px" height="40px" />
<div class="top">{{ name }}</div>
<div class="bottom">{{ account }}</div>
</div>
@@ -93,13 +89,13 @@ const logout = () => {
<el-icon size="16">
<icon_admin_outlined></icon_admin_outlined>
</el-icon>
<div class="datasource-name">系统管理</div>
<div class="datasource-name">{{ $t('common.system_manage') }}</div>
</div>
<div class="popover-item" @click="openPwd">
<el-icon size="16">
<icon_key_outlined></icon_key_outlined>
</el-icon>
<div class="datasource-name">修改密码</div>
<div class="datasource-name">{{ $t('user.change_password') }}</div>
</div>
<el-popover :teleported="false" popper-class="system-language" placement="right">
<template #reference>
@@ -107,7 +103,7 @@ const logout = () => {
<el-icon size="16">
<icon_translate_outlined></icon_translate_outlined>
</el-icon>
<div class="datasource-name">语言</div>
<div class="datasource-name">{{ $t('common.language') }}</div>
<el-icon style="transform: scale(1.33)" class="right" size="16">
<icon_right_outlined></icon_right_outlined>
</el-icon>
@@ -133,13 +129,13 @@ const logout = () => {
<el-icon size="16">
<icon_maybe_outlined></icon_maybe_outlined>
</el-icon>
<div class="datasource-name">帮助</div>
<div class="datasource-name">{{ $t('common.help') }}</div>
</div>
<div class="popover-item mr4" @click="logout">
<el-icon size="16">
<icon_logout_outlined></icon_logout_outlined>
</el-icon>
<div class="datasource-name">退出登录</div>
<div class="datasource-name">{{ $t('common.logout') }}</div>
</div>
</div>
</div>
@@ -158,7 +154,6 @@ const logout = () => {
<style lang="less" scoped>
.person {
border-radius: 6px;
padding: 0 8px;
display: flex;
align-items: center;
@@ -167,6 +162,7 @@ const logout = () => {
height: 40px;
border: none;
background-color: transparent;
position: relative;
&.collapse {
min-width: 48px;
@@ -190,15 +186,28 @@ const logout = () => {
margin-left: 8px;
}
&::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 100%;
height: 100%;
border-radius: 6px;
}
&:hover,
&:active {
background: #1f23291a;
border: 1px solid #1f23291a;
&:focus {
&::after {
background: #1f23291a;
}
}
&:active {
background: #1f232926;
border: 1px solid #1f23291a;
&::after {
background: #1f232926;
}
}
}
</style>
@@ -236,9 +245,9 @@ const logout = () => {
height: 62px;
padding: 8px;
.ed-icon {
margin: 6px 8px 0 7px;
img {
float: left;
margin: 3px 8px 0 7px;
}
.top {
@@ -128,6 +128,8 @@ onMounted(async () => {
&.collapse {
width: 40px;
background: none;
border: none;
}
.name {
+87 -1
View File
@@ -17,7 +17,14 @@
"save_success": "Save Successful",
"next": "Next",
"save": "Save",
"logout": "Logout"
"logout": "Logout",
"result_count": "result",
"clear_filter": "Clear conditions",
"reset": "Reset",
"simplified_chinese": "Simplified Chinese",
"traditional_chinese": "Traditional Chinese",
"help": "Help",
"language": "Language"
},
"dashboard": {
"select_dashboard": "Please Select Dashboard",
@@ -75,6 +82,7 @@
"actions": "Actions",
"pieces_in_total": "Showing {ms} records out of {msg}",
"test_connection": "Test Connection",
"check": "check",
"connection_success": "Connection Successful",
"connection_failed": "Connection Failed, Please Check Configuration",
"Search Datasource": "Search Datasource",
@@ -168,6 +176,7 @@
"field_name": "Field name",
"relevant_content_found": "No relevant content found",
"please_enter": "Please enter",
"Please_select": "Please select",
"table_notes": "Table notes",
"field_notes": "Field notes",
"select_all": "Select all",
@@ -208,6 +217,83 @@
"parameter_type": "Parameter type",
"verification_successful": "Verification successful"
},
"user": {
"workspace": "Workspace",
"creation_time": "Creation time",
"user_source": "User source",
"phone_number": "Phone number",
"email": "Email",
"user_status": "User status",
"account": "Account",
"name": "Name",
"selected_2_items": "Selected {msg} items",
"name_account_email": "Search name, account, email",
"user_management": "User management",
"filter": "Filter",
"batch_import": "Batch import",
"add_users": "Add users",
"selected_2_users": "Do you want to delete the selected 2 users?",
"filter_conditions": "Filter conditions",
"enable": "Enable",
"disable": "Disable",
"local_creation": "Local creation",
"feishu": "Feishu",
"dingtalk": "DingTalk",
"wechat_for_business": "WeChat for business",
"1240_results": "{msg} results",
"clear_conditions": "Clear conditions",
"disabled": "Disabled",
"enabled": "Enabled",
"edit_user": "Edit User",
"del_user": "Do you want to delete user: {msg}?",
"please_first": "Please first",
"download_the_template": "download the template",
"required_and_upload": ", fill in as required and upload",
"file": "file",
"upload_file": "Upload file",
"xls_format_files": "Only supports xlsx and xls format files",
"import": "Import",
"change_file": "Change file",
"data_import_completed": "Data import completed",
"imported_100_data": "Successfully imported {msg} data",
"return_to_view": "Return to view",
"continue_importing": "Continue importing",
"import_20_can": "Successfully imported {msg} data, failed to import {loss}, can",
"download_error_report": "Download error report",
"modify_and_re_import": ", modify and re-import",
"data_import_failed": "Data import failed",
"failed_100_can": "Import failed {msg}, can",
"change_password": "Change password",
"new_password": "New password",
"confirm_password": "Confirm password",
"old_password": "Old password"
},
"workspace": {
"add_workspace": "Add workspace",
"administrator": "Administrator",
"ordinary_member": "Ordinary member",
"people": "People",
"name_username_email": "Search name, username, email",
"add_member": "Add member",
"member_type": "Member type",
"workspace_name": "Workspace name",
"no_user": "No user",
"remove": "Remove",
"selected_2_members": "Do you want to remove the selected {msg} members?",
"removed_successfully": "Removed successfully",
"workspace_de_workspace": "Do you want to delete the workspace: {msg}?",
"member_feng_yibudao": "Do you want to remove the member: {msg}?",
"select_member": "Select member",
"selected_2_people": "Selected: {msg} people",
"clear": "Clear",
"historical_dialogue": "No historical dialogue",
"rename_a_workspace": "Rename a workspace",
"return_to_workspace": "Return to workspace",
"there_are": "There are",
"2_dashboards": "{msg} dashboards",
"smart_data_centers": "{msg} smart data centers",
"confirm_to_delete": "This data source is in use. Deleting it will cause these resources to not work properly. Confirm to delete?"
},
"chat": {
"chart_type": {
"table": "Table",
+95 -11
View File
@@ -19,7 +19,14 @@
"save": "保存",
"logout": "退出登录",
"please_input": "请输入{msg}",
"switch_success": "切换成功"
"switch_success": "切换成功",
"result_count": "个结果",
"clear_filter": "清空条件",
"reset": "重置",
"simplified_chinese": "简体中文",
"traditional_chinese": "繁體中文",
"help": "帮助",
"language": "语言"
},
"dashboard": {
"select_dashboard": "请选择仪表板",
@@ -83,6 +90,7 @@
"pieces_in_total": "显示 {ms} 条数据,共 {msg} 条",
"actions": "操作",
"test_connection": "测试连接",
"check": "校验",
"connection_success": "连接成功",
"connection_failed": "连接失败,请检查配置",
"Search Datasource": "搜索数据源",
@@ -176,6 +184,7 @@
"field_name": "字段名称",
"relevant_content_found": "没有找到相关内容",
"please_enter": "请输入",
"Please_select": "请选择",
"table_notes": "表备注",
"field_notes": "字段备注",
"select_all": "全选",
@@ -219,6 +228,90 @@
"del_default_warn": "该模型为系统默认模型,请先设置其他模型为系统默认模型,再删除此模型。",
"del_warn_tip": "是否删除模型: {msg}?"
},
"user": {
"workspace": "工作空间",
"creation_time": "创建时间",
"user_source": "用户来源",
"phone_number": "手机号",
"email": "邮箱",
"user_status": "用户状态",
"account": "账号",
"name": "姓名",
"selected_2_items": "已选 {msg} 条",
"name_account_email": "搜索姓名、账号、邮箱",
"user_management": "用户管理",
"filter": "筛选",
"batch_import": "批量导入",
"add_users": "添加用户",
"selected_2_users": "是否删除选中的 {msg} 个用户?",
"filter_conditions": "筛选条件",
"enable": "启用",
"disable": "禁用",
"local_creation": "本地创建",
"feishu": "飞书",
"dingtalk": "钉钉",
"wechat_for_business": "企业微信",
"1240_results": "{msg} 个结果 ",
"clear_conditions": "清空条件",
"disabled": "已禁用",
"enabled": "已启用",
"edit_user": "编辑用户",
"del_user": "是否删除用户: {msg}?",
"please_first": "请先",
"download_the_template": "下载模板",
"required_and_upload": ",按要求填写后上传",
"file": "文件",
"upload_file": "上传文件",
"xls_format_files": "仅支持xlsx、xls格式的文件",
"import": "导入",
"change_file": "更换文件",
"data_import_completed": "数据导入完成",
"imported_100_data": "成功导入数据 {msg} 条",
"return_to_view": "返回查看",
"continue_importing": "继续导入",
"import_20_can": "成功导入数据 {msg} 条,导入失败 {loss} 条,可 ",
"download_error_report": "下载错误报告",
"modify_and_re_import": ",修改后重新导入",
"data_import_failed": "数据导入失败",
"failed_100_can": "导入失败 {msg} 条,可",
"change_password": "修改密码",
"new_password": "新密码",
"confirm_password": "确认密码",
"old_password": "旧密码",
"title": "用户管理",
"upgrade_pwd": {
"title": "修改密码",
"old_pwd": "旧密码",
"new_pwd": "新密码",
"confirm_pwd": "确认密码"
}
},
"workspace": {
"add_workspace": "添加工作空间",
"administrator": "管理员",
"ordinary_member": "普通成员",
"people": "人",
"name_username_email": "搜索姓名、用户名、邮箱",
"add_member": "添加成员",
"member_type": "成员类型",
"workspace_name": "工作空间名称",
"no_user": "暂无用户",
"remove": "移除",
"selected_2_members": "是否移除选中的 {msg} 个成员?",
"removed_successfully": "移除成功",
"workspace_de_workspace": "是否删除工作空间:{msg}?",
"member_feng_yibudao": "是否移除成员:{msg}?",
"select_member": "选择成员",
"selected_2_people": "已选:{msg} 人",
"clear": "清空",
"historical_dialogue": "暂无历史对话",
"rename_a_workspace": "重命名工作空间",
"return_to_workspace": "返回工作空间",
"there_are": "有 ",
"2_dashboards": "{msg} 个仪表板",
"smart_data_centers": "{msg} 个智能问数",
"confirm_to_delete": "正在使用此数据源,删除后会导致这些资源无法正常使用,确认删除?"
},
"chat": {
"chart_type": {
"table": "明细表",
@@ -254,14 +347,5 @@
"remark": "备注",
"back_community": "还原至社区版",
"confirm_tips": "确定还原至社区版?"
},
"user": {
"title": "用户管理",
"upgrade_pwd": {
"title": "修改密码",
"old_pwd": "旧密码",
"new_pwd": "新密码",
"confirm_pwd": "确认密码"
}
}
}
}
+14
View File
@@ -12,6 +12,8 @@ import Dashboard from '@/views/dashboard/index.vue'
import Model from '@/views/system/model/Model.vue'
import assistant from '@/views/embedded/assistant.vue'
// import User from "@/views/system/user/index.vue";
import User from '@/views/system/user/User.vue'
import Workspace from '@/views/system/workspace/index.vue'
import { watchRouter } from './watch'
const router = createRouter({
history: createWebHashHistory(),
@@ -121,6 +123,18 @@ const router = createRouter({
component: LayoutDsl,
redirect: '/system/model',
children: [
{
path: 'user',
name: 'user',
component: User,
meta: { title: '用户管理', icon: 'user' },
},
{
path: 'workspace',
name: 'workspace',
component: Workspace,
meta: { title: '工作空间', icon: 'workspace' },
},
{
path: 'model',
name: 'model',
+27
View File
@@ -26,6 +26,7 @@
--shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
--ed-color-primary: #1cba90 !important;
--ed-border-radius-base: 6px !important;
}
a {
@@ -149,3 +150,29 @@ body {
align-items: center;
justify-content: center;
}
.ed-input__prefix-inner .ed-icon {
color: #646a73;
}
.confirm-no_icon {
border-radius: 12px;
padding: 24px;
.tip {
margin-top: 24px;
}
}
.form-content_error {
.ed-form-item--default {
margin-bottom: 16px;
&.is-error {
margin-bottom: 40px;
}
}
}
.ed-dialog {
border-radius: 12px !important;
}
+27
View File
@@ -0,0 +1,27 @@
import { toType, createTypes, type VueTypesInterface, type VueTypeValidableDef } from 'vue-types'
import type { CSSProperties } from 'vue'
// 自定义扩展vue-types
type PropTypes = VueTypesInterface & {
readonly style: VueTypeValidableDef<CSSProperties>
}
const ProjectTypes = createTypes({
func: undefined,
bool: undefined,
string: undefined,
number: undefined,
object: undefined,
integer: undefined,
}) as PropTypes
class propTypes extends ProjectTypes {
style() {
return toType('style', {
type: [String, Object],
default: undefined,
})
}
}
export { propTypes }
+5 -4
View File
@@ -12,8 +12,8 @@ const props = withDefaults(
defineProps<{
name: string
type: string
type_name: string
rate?: string
typeName: string
num: string
description?: string
id?: string
}>(),
@@ -22,7 +22,7 @@ const props = withDefaults(
type: '-',
description: '-',
id: '-',
type_name: '-',
typeName: '-',
}
)
@@ -58,7 +58,7 @@ const onClickOutside = () => {
<img :src="icon" width="32px" height="32px" />
<div class="info">
<div class="name">{{ name }}</div>
<div class="type">{{ type_name }}</div>
<div class="type">{{ typeName }}</div>
</div>
</div>
<div class="type-value">
@@ -70,6 +70,7 @@ const onClickOutside = () => {
<el-icon class="form-icon" size="16">
<icon_form_outlined></icon_form_outlined>
</el-icon>
{{ num }}
</div>
<div click.stop class="methods">
<el-button type="primary" style="margin-right: 8px" @click.stop="handleQuestion">
+29 -2
View File
@@ -165,12 +165,17 @@ const changeStatus = (row: any) => {
})
})
}
const emits = defineEmits(['back'])
const back = () => {
emits('back')
}
</script>
<template>
<div class="data-table no-padding">
<div class="info">
{{ $t('ds.title') }}
<el-button text @click="back">{{ $t('ds.title') }}</el-button>
<el-icon size="12">
<icon_right_outlined></icon_right_outlined>
</el-icon>
@@ -206,6 +211,7 @@ const changeStatus = (row: any) => {
:key="ele.table_name"
class="model"
:class="currentTable.table_name === ele.table_name && 'isActive'"
:title="ele.table_name"
@click="clickTable(ele)"
>
<el-icon size="16">
@@ -353,8 +359,24 @@ const changeStatus = (row: any) => {
color: #646a73;
border-bottom: 1px solid #1f232926;
.ed-button {
height: 22px;
line-height: 22px;
color: #646a73;
&:hover {
background: #1cba901a;
color: #1cba90;
}
&:active {
color: #189e7a;
background: #1cba9033;
}
}
.name {
color: #1f2329;
margin-left: 4px;
}
}
.content {
@@ -372,7 +394,7 @@ const changeStatus = (row: any) => {
justify-content: space-between;
padding: 8px;
.ed-icon:hover {
.ed-icon {
cursor: pointer;
color: var(--ed-color-primary);
}
@@ -392,11 +414,16 @@ const changeStatus = (row: any) => {
padding-left: 8px;
border-radius: 4px;
cursor: pointer;
.name {
margin-left: 8px;
font-weight: 500;
font-size: 14px;
line-height: 22px;
max-width: 80%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
&:hover {
background: #1f23291a;
+84 -53
View File
@@ -1,5 +1,5 @@
<script lang="ts" setup>
import { ref, computed, shallowRef, nextTick } from 'vue'
import { ref, computed, shallowRef, nextTick, h } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus-secondary'
import icon_searchOutline_outlined from '@/assets/svg/icon_search-outline_outlined.svg'
import arrow_down from '@/assets/svg/arrow-down.svg'
@@ -13,12 +13,15 @@ import DatasourceList from './DatasourceList.vue'
import DatasourceListSide from './DatasourceListSide.vue'
import DatasourceForm from './DatasourceForm.vue'
import { datasourceApi } from '@/api/datasource'
import { useEmitt } from '@/utils/useEmitt'
import Card from './Card.vue'
import { dsTypeWithImg } from './js/ds-type'
import DelMessageBox from './DelMessageBox.vue'
import { dsTypeWithImgSort } from './js/ds-type'
import { useI18n } from 'vue-i18n'
interface Datasource {
name: string
num: string
type_name: string
type: string
img: string
@@ -34,20 +37,24 @@ const datasourceConfigvVisible = ref(false)
const editDatasource = ref(false)
const activeStep = ref(0)
const activeName = ref('')
const activeType = ref('')
const datasourceFormRef = ref()
const datasourceList = shallowRef([] as Datasource[])
const defaultDatasourceList = shallowRef(dsTypeWithImg as (Datasource & { img: string })[])
const defaultDatasourceList = shallowRef(dsTypeWithImgSort as (Datasource & { img: string })[])
const currentDefaultDatasource = ref('')
const datasourceListWithSearch = computed(() => {
if (!keywords.value) return datasourceList.value
return datasourceList.value.filter((ele) =>
ele.name.toLowerCase().includes(keywords.value.toLowerCase())
if (!keywords.value && !currentDatasourceType.value) return datasourceList.value
return datasourceList.value.filter(
(ele) =>
ele.name.toLowerCase().includes(keywords.value.toLowerCase()) &&
(ele.type === currentDatasourceType.value || !currentDatasourceType.value)
)
})
const beforeClose = () => {
datasourceConfigvVisible.value = false
activeStep.value = 0
}
const defaultDatasourceListWithSearch = computed(() => {
if (!defaultDatasourceKeywords.value) return defaultDatasourceList.value
@@ -56,8 +63,16 @@ const defaultDatasourceListWithSearch = computed(() => {
)
})
const currentDatasourceType = ref('')
const handleDefaultDatasourceChange = (item: any) => {
currentDefaultDatasource.value = item.name
if (currentDatasourceType.value === item.type) {
currentDefaultDatasource.value = ''
currentDatasourceType.value = ''
} else {
currentDefaultDatasource.value = item.name
currentDatasourceType.value = item.type
}
}
const formatKeywords = (item: string) => {
@@ -92,43 +107,72 @@ const handleAddDatasource = () => {
datasourceConfigvVisible.value = true
}
const refresh = () => {
activeName.value = ''
activeStep.value = 0
activeType.value = ''
datasourceConfigvVisible.value = false
search()
}
const panelClick = () => {
console.log('panelClick')
}
const smartClick = () => {
console.log('smartClick')
}
const deleteHandler = (item: any) => {
ElMessageBox.confirm(t('datasource.data_source', { msg: item.name }), {
ElMessageBox.confirm('', {
confirmButtonType: 'danger',
tip: t('datasource.operate_with_caution'),
confirmButtonText: t('dashboard.delete'),
cancelButtonText: t('common.cancel'),
customClass: 'confirm-no_icon',
autofocus: false,
dangerouslyUseHTMLString: true,
message: h(
DelMessageBox,
{
name: item.name,
panelNum: 1,
smartNum: 4,
onPanelClick: panelClick,
onSmartClick: smartClick,
t,
},
''
),
}).then(() => {
datasourceApi.delete(item.id).then(() => {
ElMessage({
type: 'success',
message: t('dashboard.delete_success'),
})
search()
})
})
.then(() => {
datasourceApi.delete(item.id).then(() => {
ElMessage({
type: 'success',
message: t('dashboard.delete_success'),
})
search()
})
})
.catch(() => {
ElMessageBox.confirm(t('datasource.data_source_de', { msg: item.name }), {
tip: t('datasource.cannot_be_deleted'),
cancelButtonText: t('datasource.got_it'),
showConfirmButton: false,
customClass: 'confirm-no_icon',
autofocus: false,
})
})
// .catch(() => {
// ElMessageBox.confirm(t('datasource.data_source_de', { msg: item.name }), {
// tip: t('datasource.cannot_be_deleted'),
// cancelButtonText: t('datasource.got_it'),
// showConfirmButton: false,
// customClass: 'confirm-no_icon',
// autofocus: false,
// })
// })
}
const clickDatasource = (ele: any) => {
activeStep.value = 1
activeName.value = ele.name
activeType.value = ele.type
}
const clickDatasourceSide = (ele: any) => {
activeName.value = ele.name
activeStep.value = 1
activeType.value = ele.type
}
const search = () => {
@@ -138,32 +182,15 @@ const search = () => {
}
search()
const submit = (item: any) => {
if (!item.id) {
datasourceApi.add(item).then(() => {
beforeClose()
search()
ElMessage({
type: 'success',
message: 'Add completed',
})
})
return
}
datasourceApi.edit(item).then(() => {
beforeClose()
search()
ElMessage({
type: 'success',
message: 'Edit completed',
})
})
}
const currentDataTable = ref()
const dataTableDetail = (ele: any) => {
useEmitt().emitter.emit('collapse-change')
currentDataTable.value = ele
}
const back = () => {
currentDataTable.value = null
}
</script>
<template>
@@ -187,7 +214,7 @@ const dataTableDetail = (ele: any) => {
<el-popover popper-class="system-default_datasource" placement="bottom">
<template #reference>
<el-button secondary>
{{ $t('datasource.all_types') }}
{{ currentDefaultDatasource || $t('datasource.all_types') }}
<el-icon style="margin-left: 8px">
<arrow_down></arrow_down>
</el-icon> </el-button
@@ -247,7 +274,8 @@ const dataTableDetail = (ele: any) => {
:key="ele.id"
:name="ele.name"
:type="ele.type"
:type_name="ele.type_name"
:type-name="ele.type_name"
:num="ele.num"
:description="ele.description"
@question="handleQuestion"
@edit="handleEditDatasource(ele)"
@@ -298,12 +326,13 @@ const dataTableDetail = (ele: any) => {
ref="datasourceFormRef"
:active-step="activeStep"
:active-name="activeName"
@submit="submit"
:active-type="activeType"
@refresh="refresh"
@change-active-step="(val: number) => (activeStep = val)"
></DatasourceForm>
</el-drawer>
</div>
<DataTable v-if="currentDataTable" :info="currentDataTable"></DataTable>
<DataTable v-if="currentDataTable" :info="currentDataTable" @back="back"></DataTable>
</template>
<style lang="less" scoped>
@@ -324,6 +353,8 @@ const dataTableDetail = (ele: any) => {
.card-content {
display: flex;
flex-wrap: wrap;
max-height: calc(100% - 40px);
overflow-y: auto;
}
}
</style>
+16 -17
View File
@@ -16,17 +16,19 @@ import EmptyBackground from '@/views/dashboard/common/EmptyBackground.vue'
const props = withDefaults(
defineProps<{
activeName: string
activeType: string
activeStep: number
}>(),
{
activeName: '',
activeType: '',
activeStep: 0,
}
)
const { wsCache } = useCache()
const dsFormRef = ref<FormInstance>()
const emit = defineEmits(['refresh', 'submit', 'changeActiveStep'])
const emit = defineEmits(['refresh', 'changeActiveStep'])
const isCreate = ref(true)
const isEditTable = ref(false)
const checkList = ref<any>([])
@@ -62,7 +64,7 @@ const dialogVisible = ref<boolean>(false)
const form = ref<any>({
name: '',
description: '',
type: 'mysql',
type: props.activeType,
configuration: '',
driver: '',
host: '',
@@ -330,14 +332,6 @@ onMounted(() => {
}, 100)
})
const submitModle = () => {
dsFormRef.value!.validate((res: any) => {
if (res) {
emit('submit')
}
})
}
const keywords = ref('')
const tableListWithSearch = computed(() => {
if (!keywords.value) return tableList.value
@@ -354,6 +348,13 @@ watch(keywords, () => {
isIndeterminate.value = checkedCount > 0 && checkedCount < tableListWithSearch.value.length
})
watch(
() => props.activeType,
(val) => {
form.value.type = val
}
)
const checkAll = ref(false)
const isIndeterminate = ref(false)
let checkTableList = [] as any[]
@@ -390,7 +391,6 @@ const tableListSave = () => {
defineExpose({
initForm,
submitModle,
tableListSave,
})
</script>
@@ -555,15 +555,15 @@ defineExpose({
</div>
<div class="draw-foot">
<el-button @click="close">{{ t('common.cancel') }}</el-button>
<el-button v-show="!isCreate && !isEditTable && form.type !== 'excel'" @click="check">
{{ t('ds.test_connection') }}
<el-button v-show="form.type !== 'excel'" @click="check">
{{ t('ds.check') }}
</el-button>
<el-button v-show="activeStep !== 0 && isCreate" secondary @click="preview">
{{ t('ds.previous') }}
</el-button>
<el-button v-show="activeStep === 1 && isCreate" type="primary" @click="next(dsFormRef)">
{{ t('common.next') }}
</el-button>
<el-button v-show="activeStep === 2 && isCreate" @click="preview">
{{ t('ds.previous') }}
</el-button>
<el-button
v-show="activeStep === 2 || !isCreate"
:loading="saveLoading"
@@ -642,7 +642,6 @@ defineExpose({
}
.container {
height: 524px;
overflow-y: auto;
border: 1px solid #dee0e3;
border-radius: 4px;
+3 -3
View File
@@ -26,7 +26,7 @@ const handleModelClick = (item: any) => {
</script>
<template>
<div class="model-list">
<div class="datasouce-list">
<div class="title">{{ $t('qa.select_datasource') }}</div>
<el-input
v-model="keywords"
@@ -61,7 +61,7 @@ const handleModelClick = (item: any) => {
</template>
<style lang="less" scoped>
.model-list {
.datasouce-list {
width: 800px;
margin: 0 auto;
max-height: 100%;
@@ -91,7 +91,7 @@ const handleModelClick = (item: any) => {
padding-left: 16px;
margin-bottom: 16px;
border: 1px solid #dee0e3;
border-radius: 4px;
border-radius: 12px;
margin-left: 16px;
cursor: pointer;
&:hover {
+67
View File
@@ -0,0 +1,67 @@
<script lang="ts" setup>
defineProps({
name: {
type: String,
default: '-',
},
panelNum: {
type: Number,
default: 0,
},
smartNum: {
type: Number,
default: 0,
},
t: {
type: Function,
default: () => {},
},
})
const emits = defineEmits(['panelClick', 'smartClick'])
const panelClick = () => {
emits('panelClick')
}
const smartClick = () => {
emits('smartClick')
}
</script>
<template>
<div class="del-box_message">
<div class="del-title">
{{ t('datasource.data_source', { msg: name }) }}
</div>
<div class="use-panel">
{{ t('workspace.there_are') }}
<span @click="panelClick">{{ t('workspace.2_dashboards', { msg: panelNum }) }}</span
>、<span @click="smartClick">{{ t('workspace.smart_data_centers', { msg: smartNum }) }}</span>
{{ t('workspace.confirm_to_delete') }}
</div>
</div>
</template>
<style lang="less" scoped>
.del-box_message {
.del-title {
font-weight: 500;
font-size: 16px;
line-height: 24px;
}
.use-panel {
font-weight: 400;
font-size: 14px;
line-height: 22px;
margin-top: 24px;
span {
text-decoration: underline;
text-decoration-style: solid;
cursor: pointer;
color: #1cba90;
}
}
}
</style>
+1 -1
View File
@@ -135,7 +135,7 @@
<div style="display: flex; justify-content: flex-end; margin-top: 20px">
<el-button @click="close">{{ t('common.cancel') }}</el-button>
<el-button v-show="!isCreate && !isEditTable && form.type !== 'excel'" @click="check">
{{ t('ds.test_connection') }}
{{ t('ds.check') }}
</el-button>
<el-button v-show="active === 0 && isCreate" type="primary" @click="next(dsFormRef)">
{{ t('common.next') }}
+12 -4
View File
@@ -13,11 +13,19 @@ export const dsType = [
]
export const dsTypeWithImg = [
{ name: 'MySQL', type: 'mysql', img: mysql_ds },
{ name: 'PostgreSQL', type: 'pg', img: pg },
{ name: 'Microsoft SQL Server', type: 'sqlServer', img: sqlServer },
{ name: 'Oracle', type: 'oracle', img: oracle },
{ name: '本地 Excel/CSV', type: 'excel', img: excel },
{ name: 'MySQL', type: 'mysql', img: mysql_ds },
{ name: 'SQL Server', type: 'sqlServer', img: sqlServer },
{ name: 'Oracle', type: 'oracle', img: oracle },
{ name: 'PostgreSQL', type: 'pg', img: pg },
]
export const dsTypeWithImgSort = [
{ name: '本地 Excel/CSV', type: 'excel', img: excel },
{ name: 'PostgreSQL', type: 'pg', img: pg },
{ name: 'Oracle', type: 'oracle', img: oracle },
{ name: 'MySQL', type: 'mysql', img: mysql_ds },
{ name: 'SQL Server', type: 'sqlServer', img: sqlServer },
]
export const haveSchema = ['sqlServer', 'pg', 'oracle']
+1 -1
View File
@@ -30,7 +30,7 @@ const current_supplier = computed(() => {
const emits = defineEmits(['edit', 'del'])
const handleEdit = () => {
emits('edit', props.id)
emits('edit')
}
const handleDel = () => {
+10 -3
View File
@@ -32,6 +32,7 @@ const modelConfigvVisible = ref(false)
const editModel = ref(false)
const activeStep = ref(0)
const activeName = ref('')
const activeType = ref('')
const modelFormRef = ref()
reactive({
@@ -102,9 +103,11 @@ const handleAddModel = () => {
activeStep.value = 0
modelConfigvVisible.value = true
}
const handleEditModel = (id: any) => {
const handleEditModel = (row: any) => {
activeStep.value = 1
modelApi.query(id).then((res: any) => {
activeType.value = row.supplier
activeName.value = row.supplier_item.name
modelApi.query(row.id).then((res: any) => {
modelConfigvVisible.value = true
nextTick(() => {
modelFormRef.value.initForm({ ...res })
@@ -287,7 +290,7 @@ const submit = (item: any) => {
:modle-type="getModelTypeName(ele['model_type'])"
:base-modle="ele['base_model']"
:is-default="ele['default_model']"
@edit="handleEditModel"
@edit="handleEditModel(ele)"
@del="deleteHandler"
></card>
</div>
@@ -320,12 +323,14 @@ const submit = (item: any) => {
<ModelListSide
v-if="activeStep === 1"
:active-name="activeName"
:active-type="activeType"
@click-model="supplierChang"
></ModelListSide>
<ModelForm
v-if="activeStep === 1"
ref="modelFormRef"
:active-name="activeName"
:active-type="activeType"
@submit="submit"
></ModelForm>
<template #footer>
@@ -354,6 +359,8 @@ const submit = (item: any) => {
.card-content {
display: flex;
flex-wrap: wrap;
max-height: calc(100% - 40px);
}
}
</style>
+22 -11
View File
@@ -37,7 +37,7 @@ const modelForm = reactive({
api_domain: '',
config_list: [],
})
let isCreate = false
const isCreate = ref(false)
const modelRef = ref()
const paramsFormRef = ref()
const advancedSetting = ref([] as ParamsFormData[])
@@ -51,7 +51,7 @@ const modelList = computed(() => {
return base_model_options(modelForm.supplier, modelForm.model_type)
})
const handleParamsEdite = (ele?: any) => {
isCreate = false
isCreate.value = false
paramsFormDrawer.value = true
nextTick(() => {
paramsFormRef.value.initForm(ele)
@@ -59,7 +59,7 @@ const handleParamsEdite = (ele?: any) => {
}
const handleParamsCreate = () => {
isCreate = true
isCreate.value = true
paramsFormDrawer.value = true
nextTick(() => {
paramsFormRef.value.initForm()
@@ -67,7 +67,7 @@ const handleParamsCreate = () => {
}
const handleParamsDel = (item: any) => {
advancedSetting.value = advancedSetting.value.filter((ele) => ele.key !== item.key)
advancedSetting.value = advancedSetting.value.filter((ele) => ele.id !== item.id)
}
const rules = {
@@ -107,9 +107,9 @@ const addParams = () => {
}
const submit = (item: any) => {
if (isCreate) {
if (isCreate.value) {
advancedSetting.value.push({ ...item })
cancel()
beforeClose()
return
}
for (const key in advancedSetting.value) {
@@ -119,16 +119,18 @@ const submit = (item: any) => {
}
}
cancel()
beforeClose()
}
const cancel = () => {
const beforeClose = () => {
paramsFormRef.value.close()
paramsFormDrawer.value = false
}
const supplierChang = (supplier: any) => {
modelForm.supplier = supplier.id
const config = supplier.model_config[modelForm.model_type || 0]
modelForm.api_domain = config.api_domain
modelForm.base_model = ''
if (!modelForm.id) {
modelForm.name = supplier.name
}
@@ -283,12 +285,19 @@ defineExpose({
<el-drawer
v-model="paramsFormDrawer"
:size="600"
:title="$t('model.add') + $t('common.empty') + $t('model.parameters')"
:before-close="beforeClose"
:title="
isCreate
? $t('model.add') + $t('common.empty') + $t('model.parameters')
: $t('datasource.edit') + $t('common.empty') + $t('model.parameters')
"
>
<ParamsForm ref="paramsFormRef" @submit="submit"></ParamsForm>
<template #footer>
<el-button secondary @click="cancel"> {{ $t('common.cancel') }} </el-button>
<el-button type="primary" @click="addParams"> {{ t('model.add') }} </el-button>
<el-button secondary @click="beforeClose"> {{ $t('common.cancel') }} </el-button>
<el-button type="primary" @click="addParams">
{{ isCreate ? t('model.add') : t('common.save') }}
</el-button>
</template>
</el-drawer>
</div>
@@ -316,6 +325,8 @@ defineExpose({
width: 800px;
margin: 0 auto;
padding-top: 24px;
overflow-y: auto;
height: calc(100% - 180px);
.ed-form-item--default {
margin-bottom: 16px;
@@ -67,9 +67,8 @@ const handleModelClick = (item: any) => {
.list-content {
margin-top: 16px;
display: flex;
justify-content: space-between;
height: calc(100% - 40px);
gap: 16px;
flex-wrap: wrap;
.model:nth-child(odd) {
margin-left: 0;
}
@@ -81,10 +80,9 @@ const handleModelClick = (item: any) => {
align-items: center;
padding-left: 16px;
margin-bottom: 16px;
margin-left: 16px;
flex-wrap: wrap;
border: 1px solid #dee0e3;
border-radius: 4px;
border-radius: 12px;
margin-left: 16px;
cursor: pointer;
&:hover {
box-shadow: 0px 6px 24px 0px #1f232914;
@@ -8,6 +8,7 @@ const paramsForm = reactive({
name: '',
key: '',
val: '',
id: '',
})
const rules = {
@@ -38,6 +39,9 @@ const initForm = (item: any) => {
if (item) {
Object.assign(paramsForm, { ...item })
}
if (!paramsForm.id) {
paramsForm.id = `${+new Date()}`
}
paramsRef.value.clearValidate()
}
@@ -50,9 +54,17 @@ const submit = () => {
}
})
}
const close = () => {
paramsForm.name = ''
paramsForm.key = ''
paramsForm.val = ''
paramsForm.id = ''
}
defineExpose({
initForm,
submit,
close,
})
</script>
+773
View File
@@ -0,0 +1,773 @@
<template>
<div class="sqlbot-table-container professional-container">
<div class="tool-left">
<span class="page-title">{{ $t('user.user_management') }}</span>
<div class="search-bar">
<el-input
v-model="keyword"
style="width: 240px; margin-right: 12px"
:placeholder="$t('user.name_account_email')"
@keyup.enter="handleSearch"
@blur="handleSearch"
>
<template #prefix>
<el-icon>
<icon_searchOutline_outlined />
</el-icon>
</template>
</el-input>
<el-button secondary @click="drawerMainOpen">
<template #icon>
<iconFilter></iconFilter>
</template>
{{ $t('user.filter') }}
</el-button>
<el-button secondary @click="handleUserImport">
<template #icon>
<ccmUpload></ccmUpload>
</template>
{{ $t('user.batch_import') }}
</el-button>
<el-button type="primary" @click="editHandler(null)">
<template #icon>
<icon_add_outlined></icon_add_outlined>
</template>
{{ $t('user.add_users') }}
</el-button>
</div>
</div>
<div class="sqlbot-table" :class="state.filterTexts.length && 'is-filter'">
<filter-text
:total="state.pageInfo.total"
:filter-texts="state.filterTexts"
@clear-filter="clearFilter"
/>
<el-table
ref="multipleTableRef"
:data="state.tableData"
style="width: 100%"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" />
<el-table-column prop="name" :label="$t('user.name')" width="280" />
<el-table-column prop="account" :label="$t('user.account')" width="280" />
<el-table-column prop="status" :label="$t('user.user_status')" width="180">
<template #default="scope">
<div class="user-status-container" :class="[scope.row.status ? 'active' : 'disabled']">
<el-icon size="16">
<SuccessFilled v-if="scope.row.status" />
<CircleCloseFilled v-else />
</el-icon>
<span>{{ $t(`user.${scope.row.status ? 'enabled' : 'disabled'}`) }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="email" :label="$t('user.email')" width="280" />
<!-- <el-table-column prop="phone" :label="$t('user.phone_number')" width="280" /> -->
<!-- <el-table-column prop="user_source" :label="$t('user.user_source')" width="280" /> -->
<el-table-column prop="space_name" :label="$t('user.workspace')" width="280" />
<el-table-column prop="create_time" width="180" sortable :label="$t('user.creation_time')">
<template #default="scope">
<span>{{ formatTimestamp(scope.row.create_time) }}</span>
</template>
</el-table-column>
<el-table-column fixed="right" width="150" :label="$t('ds.actions')">
<template #default="scope">
<div class="table-operate">
<el-switch
v-model="scope.row.status"
:active-value="1"
:inactive-value="0"
size="small"
@change="statusHandler(scope.row)"
/>
<div class="line"></div>
<el-tooltip
:offset="14"
effect="dark"
:content="$t('datasource.edit')"
placement="top"
>
<el-icon class="action-btn" size="16" @click="editHandler(scope.row)">
<IconOpeEdit></IconOpeEdit>
</el-icon>
</el-tooltip>
<el-tooltip
:offset="14"
effect="dark"
:content="$t('user.change_password')"
placement="top"
>
<el-icon class="action-btn" size="16" @click="handleEditPassword(scope.row.id)">
<IconLock></IconLock>
</el-icon>
</el-tooltip>
<el-tooltip
:offset="14"
effect="dark"
:content="$t('dashboard.delete')"
placement="top"
>
<el-icon class="action-btn" size="16" @click="deleteHandler(scope.row)">
<IconOpeDelete></IconOpeDelete>
</el-icon>
</el-tooltip>
</div>
</template>
</el-table-column>
</el-table>
<div class="pagination-container">
<el-pagination
v-model:current-page="state.pageInfo.currentPage"
v-model:page-size="state.pageInfo.pageSize"
:page-sizes="[10, 20, 30]"
:background="true"
layout="total, sizes, prev, pager, next, jumper"
:total="state.pageInfo.total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</div>
<div v-if="multipleSelectionAll.length" class="bottom-select">
<el-checkbox
v-model="checkAll"
:indeterminate="isIndeterminate"
@change="handleCheckAllChange"
>
{{ $t('datasource.select_all') }}
</el-checkbox>
<button class="danger-button" @click="deleteBatchUser">{{ $t('dashboard.delete') }}</button>
<span class="selected">{{
$t('user.selected_2_items', { msg: multipleSelectionAll.length })
}}</span>
<el-button text @click="cancelDelete">
{{ $t('common.cancel') }}
</el-button>
</div>
</div>
<el-drawer
v-model="dialogFormVisible"
:title="dialogTitle"
destroy-on-close
size="600px"
modle-class="sqlbot-drawer"
@close="onFormClose"
>
<el-form
ref="termFormRef"
:model="state.form"
label-width="180px"
label-position="top"
:rules="rules"
class="form-content_error"
>
<el-form-item prop="name" :label="t('user.name')">
<el-input
v-model="state.form.name"
:placeholder="$t('datasource.please_enter') + $t('common.empty') + $t('user.name')"
autocomplete="off"
/>
</el-form-item>
<el-form-item prop="account" :label="t('user.account')">
<el-input
v-model="state.form.account"
:placeholder="$t('datasource.please_enter') + $t('common.empty') + $t('user.account')"
autocomplete="off"
/>
</el-form-item>
<el-form-item prop="email" :label="$t('user.email')">
<el-input
v-model="state.form.email"
:placeholder="$t('datasource.please_enter') + $t('common.empty') + $t('user.email')"
autocomplete="off"
/>
</el-form-item>
<!-- <el-form-item :label="$t('user.phone_number')">
<el-input
v-model="state.form.phoneNumber"
:placeholder="
$t('datasource.please_enter') + $t('common.empty') + $t('user.phone_number')
"
autocomplete="off"
/>
</el-form-item> -->
<el-form-item :label="$t('user.workspace')">
<el-select
v-model="state.form.oid"
:placeholder="$t('datasource.Please_select') + $t('common.empty') + $t('user.workspace')"
>
<el-option v-for="item in options" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item :label="$t('user.user_status')">
<el-switch v-model="state.form.status" :active-value="1" :inactive-value="0" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="closeForm">{{ $t('common.cancel') }}</el-button>
<el-button type="primary" @click="saveHandler"> {{ $t('model.add') }} </el-button>
</div>
</template>
</el-drawer>
<el-dialog
v-model="dialogVisiblePassword"
:title="$t('user.change_password')"
width="500"
:before-close="handleClosePassword"
>
<el-form
ref="passwordRef"
:model="password"
label-width="180px"
label-position="top"
:rules="passwordRules"
class="form-content_error"
>
<el-form-item prop="new" :label="t('user.new_password')">
<el-input
v-model="password.new"
:placeholder="
$t('datasource.please_enter') + $t('common.empty') + $t('user.new_password')
"
autocomplete="off"
/>
</el-form-item>
<el-form-item prop="old" :label="t('user.confirm_password')">
<el-input
v-model="password.old"
:placeholder="
$t('datasource.please_enter') + $t('common.empty') + $t('user.confirm_password')
"
autocomplete="off"
/>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleClosePassword">{{ $t('common.cancel') }}</el-button>
<el-button type="primary" @click="handleConfirmPassword">
{{ $t('common.save') }}
</el-button>
</div>
</template>
</el-dialog>
<UserImport ref="userImportRef"></UserImport>
<drawer-main
ref="drawerMainRef"
:filter-options="filterOption"
@trigger-filter="searchCondition"
/>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, nextTick } from 'vue'
import UserImport from './UserImport.vue'
import SuccessFilled from '@/assets/svg/gou_icon.svg'
import CircleCloseFilled from '@/assets/svg/icon_ban_filled.svg'
import icon_searchOutline_outlined from '@/assets/svg/icon_search-outline_outlined.svg'
import { useI18n } from 'vue-i18n'
import { convertFilterText, FilterText } from '@/components/filter-text'
import IconLock from '@/assets/svg/icon-key_outlined.svg'
import IconOpeEdit from '@/assets/svg/icon_edit_outlined.svg'
import IconOpeDelete from '@/assets/svg/icon_delete.svg'
import iconFilter from '@/assets/svg/icon-filter_outlined.svg'
import ccmUpload from '@/assets/svg/icon_ccm-upload_outlined.svg'
import icon_add_outlined from '@/assets/svg/icon_add_outlined.svg'
import { userApi } from '@/api/user'
import { workspaceList } from '@/api/workspace'
import { formatTimestamp } from '@/utils/date'
const { t } = useI18n()
const keyword = ref('')
const dialogFormVisible = ref(false)
const termFormRef = ref()
const checkAll = ref(false)
const dialogVisiblePassword = ref(false)
const isIndeterminate = ref(true)
const drawerMainRef = ref()
const userImportRef = ref()
const filterOption = ref<any[]>([
{
type: 'enum',
option: [
{ id: 1, name: t('user.enable') },
{ id: 0, name: t('user.disable') },
],
field: 'status',
title: t('user.user_status'),
operate: 'in',
},
{
type: 'enum',
option: [
{ id: '0', name: t('user.local_creation') },
{ id: 1, name: 'LDAP' },
{ id: 2, name: 'OIDC' },
{ id: 3, name: 'CAS' },
{ id: 9, name: 'OAuth2' },
{ id: 4, name: t('user.feishu') },
{ id: 5, name: t('user.dingtalk') },
{ id: 6, name: t('user.wechat_for_business') },
],
field: 'origins',
title: t('user.user_source'),
operate: 'in',
},
{
type: 'select',
option: [],
field: 'oidist',
title: t('user.workspace'),
operate: 'in',
property: { placeholder: t('common.empty') + t('user.workspace') },
},
])
const options = ref<any[]>([])
const state = reactive<any>({
tableData: [],
filterTexts: [],
conditions: [],
form: {
id: '',
name: '',
account: '',
oid: '',
email: '',
status: '',
phoneNumber: '',
},
pageInfo: {
currentPage: 1,
pageSize: 20,
total: 0,
},
})
const rules = {
name: [
{
required: true,
message: t('datasource.please_enter') + t('common.empty') + t('user.name'),
trigger: 'blur',
},
],
account: [
{
required: true,
message: t('datasource.please_enter') + t('common.empty') + t('user.account'),
trigger: 'blur',
},
],
email: [
{
required: true,
message: t('datasource.please_enter') + t('common.empty') + t('user.email'),
trigger: 'blur',
},
],
}
const passwordRules = {
new: [
{
required: true,
message: t('datasource.please_enter') + t('common.empty') + t('user.new_password'),
trigger: 'blur',
},
],
old: [
{
required: true,
message: t('datasource.please_enter') + t('common.empty') + t('user.confirm_password'),
trigger: 'blur',
},
],
}
const multipleTableRef = ref()
const multipleSelectionAll = ref<any[]>([])
const dialogTitle = ref('')
const passwordRef = ref()
const password = ref({
new: '',
old: '',
id: '',
})
const handleClosePassword = () => {
dialogVisiblePassword.value = false
}
const handleEditPassword = (id: any) => {
password.value.id = id
dialogVisiblePassword.value = true
}
const handleUserImport = () => {
userImportRef.value.showDialog()
}
const handleConfirmPassword = () => {
passwordRef.value.validate((val: any) => {
if (val) {
console.log(val)
}
})
dialogVisiblePassword.value = false
}
const handleSelectionChange = (val: any[]) => {
const ids = state.tableData.map((ele: any) => ele.id)
multipleSelectionAll.value = [
...multipleSelectionAll.value.filter((ele) => !ids.includes(ele.id)),
...val,
]
isIndeterminate.value = !(val.length === 0 || val.length === state.tableData.length)
checkAll.value = val.length === state.tableData.length
}
const handleCheckAllChange = (val: any) => {
isIndeterminate.value = false
handleSelectionChange(val ? state.tableData : [])
if (val) {
handleToggleRowSelection()
} else {
multipleTableRef.value.clearSelection()
}
}
const handleToggleRowSelection = (check: boolean = true) => {
let i = 0
const ids = multipleSelectionAll.value.map((ele: any) => ele.id)
for (const key in state.tableData) {
if (ids.includes((state.tableData[key] as any).id)) {
i += 1
multipleTableRef.value.toggleRowSelection(state.tableData[key], check)
}
}
checkAll.value = i === state.tableData.length
isIndeterminate.value = !(i === 0 || i === state.tableData.length)
}
const handleSearch = () => {
search()
}
const fillFilterText = () => {
const textArray = state.conditions?.length
? convertFilterText(state.conditions, filterOption.value)
: []
state.filterTexts = [...textArray]
Object.assign(state.filterTexts, textArray)
}
const clearFilter = (params?: number) => {
let index = params ? params : 0
if (isNaN(index)) {
state.filterTexts = []
} else {
state.filterTexts.splice(index, 1)
}
drawerMainRef.value.clearFilter(index)
}
const searchCondition = (conditions: any) => {
state.conditions = conditions
fillFilterText()
search()
drawerMainClose()
}
const drawerMainOpen = async () => {
drawerMainRef.value.init()
}
const drawerMainClose = () => {
drawerMainRef.value.close()
}
const editHandler = (row: any) => {
if (row) {
state.form = { ...row }
}
dialogFormVisible.value = true
dialogTitle.value = row?.id ? t('user.edit_user') : t('user.add_users')
}
const statusHandler = (row: any) => {
state.form = { ...row }
editTerm()
}
const cancelDelete = () => {
multipleSelectionAll.value = []
checkAll.value = false
isIndeterminate.value = false
}
const deleteBatchUser = () => {
ElMessageBox.confirm(t('user.selected_2_users', { msg: multipleSelectionAll.value.length }), {
confirmButtonType: 'danger',
confirmButtonText: t('dashboard.delete'),
cancelButtonText: t('common.cancel'),
customClass: 'confirm-no_icon',
autofocus: false,
callback: (val: string) => {
console.log(val)
},
})
}
const deleteHandler = (row: any) => {
ElMessageBox.confirm(t('user.del_user', { msg: row.name }), {
confirmButtonType: 'danger',
confirmButtonText: t('dashboard.delete'),
cancelButtonText: t('common.cancel'),
customClass: 'confirm-no_icon',
autofocus: false,
})
.then(() => {
userApi.delete(row.id).then(() => {
ElMessage({
type: 'success',
message: 'Delete completed',
})
search()
})
})
.catch(() => {
ElMessage({
type: 'info',
message: 'Delete canceled',
})
})
}
/* const openDialog = () => {
dialogFormVisible.value = true
} */
const resetForm = () => {
if (!termFormRef.value) return
Object.keys(state.form).forEach((key) => {
state.form[key as keyof typeof state.form] = ''
})
}
const closeForm = () => {
dialogFormVisible.value = false
}
const onFormClose = () => {
resetForm()
dialogFormVisible.value = false
}
const configParams = () => {
let str = ''
if (keyword.value) {
str += `keyword=${keyword.value}`
}
state.conditions.forEach((ele: any) => {
ele.value.forEach((itx: any) => {
str += str ? `&${ele.field}=${itx}` : `${ele.field}=${itx}`
})
})
if (str.length) {
str = `?${str}`
}
return str
}
const search = () => {
userApi
.pager(configParams(), state.pageInfo.currentPage, state.pageInfo.pageSize)
.then((res: any) => {
state.tableData = res.items
state.pageInfo.total = res.total
nextTick(() => {
handleToggleRowSelection()
})
})
}
const addTerm = () => {
userApi.add(state.form).then(() => {
dialogFormVisible.value = false
search()
ElMessage({
type: 'success',
message: t('common.save_success'),
})
})
}
const editTerm = () => {
userApi.edit(state.form).then(() => {
dialogFormVisible.value = false
search()
ElMessage({
type: 'success',
message: t('common.save_success'),
})
})
}
const saveHandler = () => {
if (state.form.id) {
editTerm()
} else {
addTerm()
}
}
const handleSizeChange = (val: number) => {
state.pageInfo.pageSize = val
search()
}
const handleCurrentChange = (val: number) => {
state.pageInfo.currentPage = val
search()
}
onMounted(() => {
workspaceList().then((res) => {
options.value = res || []
filterOption.value[2].option = [...options.value]
})
search()
})
</script>
<style lang="less" scoped>
.sqlbot-table-container {
width: 100%;
height: 100%;
position: relative;
.bottom-select {
position: absolute;
height: 64px;
width: calc(100% + 48px);
left: -24px;
bottom: 0;
border-top: 1px solid #1f232926;
display: flex;
align-items: center;
padding-left: 24px;
.danger-button {
border: 1px solid var(--ed-color-danger);
color: var(--ed-color-danger);
border-radius: var(--ed-border-radius-base);
min-width: 80px;
height: 32px;
line-height: 32px;
text-align: center;
cursor: pointer;
margin: 0 16px;
background-color: transparent;
}
.selected {
font-weight: 400;
font-size: 14px;
line-height: 22px;
color: #646a73;
margin-right: 12px;
}
}
.tool-left {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
.page-title {
font-weight: 500;
font-size: 20px;
line-height: 28px;
}
}
.sqlbot-table {
border-radius: 6px;
width: 100%;
max-height: calc(100vh - 200px);
&.is-filter {
max-height: calc(100vh - 256px);
}
:deep(.ed-table) {
--el-table-header-bg-color: #f5f7fa;
--el-table-border-color: #ebeef5;
--el-table-header-text-color: #606266;
th {
font-weight: 600;
height: 48px;
}
td {
height: 52px;
}
}
.table-operate {
display: flex;
align-items: center;
height: 24px;
line-height: 24px;
.ed-icon + .ed-icon {
margin-left: 12px;
}
.line {
margin: 0 10px 0 12px;
height: 16px;
width: 1px;
background-color: #1f232926;
}
.ed-icon {
position: relative;
cursor: pointer;
color: #646a73;
&::after {
content: '';
background-color: #1f23291a;
position: absolute;
border-radius: 6px;
width: 24px;
height: 24px;
transform: translate(-50%, -50%);
top: 50%;
left: 50%;
display: none;
}
&:hover {
&::after {
display: block;
}
}
}
}
.pagination-container {
display: flex;
justify-content: end;
align-items: center;
margin-top: 16px;
}
}
}
.user-status-container {
display: flex;
align-items: center;
font-weight: 400;
font-size: 14px;
line-height: 22px;
.ed-icon {
margin-right: 8px;
}
}
</style>
@@ -0,0 +1,475 @@
<script lang="ts" setup>
import icon_warning_filled from '@/assets/svg/icon_info_colorful.svg'
import icon_upload_outlined from '@/assets/svg/icon_upload_outlined.svg'
import icon_fileExcel_colorful from '@/assets/datasource/icon_excel.png'
import IconOpeDelete from '@/assets/svg/icon_delete.svg'
import { ref, reactive, h } from 'vue'
import {
ElMessage,
ElMessageBox,
ElLoading,
type UploadRequestOptions,
type UploadProps,
ElButton,
} from 'element-plus-secondary'
import { useI18n } from 'vue-i18n'
import { userImportApi } from '@/api/user'
const { t } = useI18n()
const defaultTip = t('user.xls_format_files')
const loadingInstance = ref<any>(null)
const dialogShow = ref(false)
const form = ref({})
const file = ref<any>(null)
const fileName = ref('')
const errorFileKey = ref(null)
const emits = defineEmits(['refresh-grid'])
const state = reactive({
errList: [],
filesTmp: [],
})
const showLoading = () => {
loadingInstance.value = ElLoading.service({ target: '.user-import-class' })
}
const closeLoading = () => {
loadingInstance.value?.close()
}
const showDialog = () => {
file.value = null
fileName.value = ''
errorFileKey.value = null
dialogShow.value = true
}
const closeDialog = () => {
dialogShow.value = false
}
const handleExceed: UploadProps['onExceed'] = () => {
ElMessage.warning(t('userimport.exceedMsg'))
}
const handleError = () => {
ElMessage.warning(t('user.contact_the_administrator'))
}
const uploadValidate = (file: any) => {
const suffix = file.name.substring(file.name.lastIndexOf('.') + 1)
if (suffix !== 'xlsx' && suffix !== 'xls') {
ElMessage.warning(t('userimport.suffixMsg'))
return false
}
if (file.size / 1024 / 1024 > 10) {
ElMessage.warning(t('userimport.limitMsg'))
return false
}
state.errList = []
return true
}
const fileSize = ref('-')
const setFile = (options: UploadRequestOptions) => {
file.value = options.file
fileName.value = options.file.name
fileSize.value = setSize(options.file.size)
}
const buildFormData = (file: any, files: any, param: any) => {
const formData = new FormData()
if (file) {
formData.append('file', file)
}
if (files) {
files.forEach((f: any) => {
formData.append('files', f)
})
}
if (param) {
formData.append('request', new Blob([JSON.stringify(param)], { type: 'application/json' }))
}
return formData
}
const downExcel = () => {
showLoading()
userImportApi
.downExcelTemplateApi()
.then((res) => {
const blobData = res.data
const blob = new Blob([blobData], { type: 'application/vnd.ms-excel' })
const link = document.createElement('a')
link.style.display = 'none'
link.href = URL.createObjectURL(blob)
link.download = 'user.xlsx' // 下载的文件名
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
closeLoading()
})
.catch(() => {
closeLoading()
})
}
const setSize = (size: any) => {
let data = ''
const _size = Number.parseFloat(size)
if (_size < 1 * 1024) {
//如果小于0.1KB转化成B
data = _size.toFixed(2) + 'B'
} else if (_size < 1 * 1024 * 1024) {
//如果小于0.1MB转化成KB
data = (_size / 1024).toFixed(2) + 'KB'
} else if (_size < 1 * 1024 * 1024 * 1024) {
//如果小于0.1GB转化成MB
data = (_size / (1024 * 1024)).toFixed(2) + 'MB'
} else {
//其他转化成GB
data = (_size / (1024 * 1024 * 1024)).toFixed(2) + 'GB'
}
const size_str = data + ''
const len = size_str.indexOf('.')
const dec = size_str.substr(len + 1, 2)
if (dec == '00') {
//当小数点后为00时 去掉小数部分
return size_str.substring(0, len) + size_str.substr(len + 3, 2)
}
return size_str
}
const toGrid = () => {
file.value = null
fileName.value = ''
dialogShow.value = false
emits('refresh-grid')
}
const clearFile = () => {
file.value = null
fileName.value = ''
}
const sure = () => {
const param = buildFormData(file.value, null, null)
showLoading()
userImportApi
.importUserApi(param)
.then((res) => {
closeLoading()
const data = res.data
errorFileKey.value = data.dataKey
closeDialog()
showTips(data.successCount, data.errorCount)
})
.catch(() => {
closeLoading()
})
}
const downErrorExcel = () => {
if (errorFileKey.value) {
showLoading()
userImportApi
.downErrorRecordApi(errorFileKey.value)
.then((res) => {
const blobData = res.data
const blob = new Blob([blobData], { type: 'application/vnd.ms-excel' })
const link = document.createElement('a')
link.style.display = 'none'
link.href = URL.createObjectURL(blob)
link.download = 'error.xlsx' // 下载的文件名
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
closeLoading()
// closeDialog()
})
.catch(() => {
closeLoading()
})
}
}
const showTips = (successCount: any, errorCount: any) => {
let title = !errorCount
? t('user.data_import_successful')
: successCount
? t('user.data_import_failed')
: t('user.data_import_failed_de')
const childrenDomList = [
h('strong', null, title),
h('br', {}, {}),
h('span', null, t('user.imported_1_data', { msg: successCount })),
]
if (errorCount) {
const errorCountDom = h('span', null, t('user.import_1_data', { msg: errorCount }))
const errorDom = h('div', { class: 'error-record-tip flex-align-center' }, [
h('span', null, t('user.can')),
h(
ElButton,
{
onClick: downErrorExcel,
type: 'primary',
text: true,
class: 'down-button',
},
t('user.download_error_report')
),
h('span', null, t('user.modify_and_re_import')),
])
childrenDomList.push(errorCountDom)
childrenDomList.push(errorDom)
}
ElMessageBox.confirm('', {
confirmButtonType: 'primary',
type: !errorCount ? 'success' : successCount ? 'warning' : 'error',
autofocus: false,
dangerouslyUseHTMLString: true,
message: h('div', { class: 'import-tip-box' }, childrenDomList),
showClose: false,
cancelButtonText: t('user.return_to_view'),
confirmButtonText: t('user.continue_importing'),
})
.then(() => {
clearErrorRecord()
showDialog()
emits('refresh-grid')
})
.catch(() => {
clearErrorRecord()
toGrid()
})
}
const clearErrorRecord = () => {
if (errorFileKey.value) {
userImportApi.clearErrorApi(errorFileKey.value)
}
}
const rules = {
file: [
{
required: true,
message: t('datasource.please_enter') + t('common.empty') + t('user.file'),
trigger: 'blur',
},
],
}
defineExpose({
showDialog,
})
</script>
<template>
<el-dialog
v-model="dialogShow"
:title="t('user.batch_import')"
width="600px"
model-class="user-import-class"
@before-close="closeDialog"
>
<div class="down-template">
<span class="icon-span">
<el-icon>
<Icon name="icon_warning_filled"><icon_warning_filled class="svg-icon" /></Icon>
</el-icon>
</span>
<div class="down-template-content">
<span>{{ t('user.please_first') }}</span>
<el-button type="primary" text class="down-button" @click="downExcel">{{
t('user.download_the_template')
}}</el-button>
<span>{{ t('user.required_and_upload') }}</span>
</div>
</div>
<el-form
ref="form"
label-position="top"
:rules="rules"
class="form-content_error import-form"
:model="form"
label-width="0px"
>
<el-form-item prop="file" :label="t('user.file')" style="margin-bottom: 0px">
<div v-if="fileName" class="pdf-card">
<img :src="icon_fileExcel_colorful" width="40px" height="40px" />
<div class="file-name">
<div class="name">{{ fileName }}</div>
<div class="size">{{ fileName.split('.')[1] }} - {{ fileSize }}</div>
</div>
<el-icon class="action-btn" size="16" @click="clearFile">
<IconOpeDelete></IconOpeDelete>
</el-icon>
</div>
<el-upload
v-if="fileName"
class="upload-user"
action=""
accept=".xlsx,.xls"
:on-exceed="handleExceed"
:before-upload="uploadValidate"
:on-error="handleError"
:show-file-list="false"
:file-list="state.filesTmp"
:http-request="setFile"
>
<el-button text style="line-height: 22px; height: 22px">
{{ $t('user.change_file') }}
</el-button>
</el-upload>
<el-upload
v-else
class="upload-user"
action=""
accept=".xlsx,.xls"
:on-exceed="handleExceed"
:before-upload="uploadValidate"
:on-error="handleError"
:show-file-list="false"
:file-list="state.filesTmp"
:http-request="setFile"
>
<el-button secondary>
<el-icon size="16" style="margin-right: 4px">
<icon_upload_outlined></icon_upload_outlined>
</el-icon>
{{ t('user.upload_file') }}</el-button
>
</el-upload>
<el-link
v-if="!fileName"
style="width: 100%; line-height: 22px; height: 22px; justify-content: start"
class="font12"
type="info"
disabled
>
{{ defaultTip }}
</el-link>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="closeDialog">{{ t('common.cancel') }}</el-button>
<el-button
:type="file && fileName ? 'primary' : 'info'"
:disabled="!file || !fileName"
@click="sure"
>{{ t('user.import') }}</el-button
>
</span>
</template>
</el-dialog>
</template>
<style lang="less">
.upload-user {
height: 32px;
.ed-upload {
width: 100% !important;
}
}
.color-danger {
:deep(.el-link--inner) {
color: var(--deDanger, #f54a45) !important;
}
}
.font12 {
color: #8f959e !important;
font-family: var(--de-custom_font, 'PingFang');
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 22px;
}
.down-template {
display: flex;
flex-direction: row;
width: 100%;
height: 40px;
line-height: 40px;
background: #d2f1e9;
border-radius: 4px;
padding-left: 10px;
.icon-span {
color: var(--ed-color-primary);
font-size: 18px;
i {
top: 3px;
}
}
.down-template-content {
font-size: 14px;
display: flex;
flex-direction: row;
margin-left: 10px;
.down-button {
height: 40px;
}
}
}
.import-form {
margin-top: 16px;
.pdf-card {
width: 100%;
height: 58px;
display: flex;
align-items: center;
padding: 0 16px 0 12px;
border: 1px solid #dee0e3;
border-radius: 6px;
.file-name {
margin-left: 8px;
.name {
font-weight: 400;
font-size: 14px;
line-height: 22px;
}
.size {
font-weight: 400;
font-size: 12px;
line-height: 20px;
color: #8f959e;
}
}
.action-btn {
margin-left: auto;
}
.ed-icon {
position: relative;
cursor: pointer;
color: #646a73;
&::after {
content: '';
background-color: #1f23291a;
position: absolute;
border-radius: 6px;
width: 24px;
height: 24px;
transform: translate(-50%, -50%);
top: 50%;
left: 50%;
display: none;
}
&:hover {
&::after {
display: block;
}
}
}
}
}
.import-tip-box {
strong {
font-size: 16px;
}
span {
font-size: 13px;
}
.error-record-tip {
font-size: 13px;
flex-flow: wrap;
}
}
</style>
@@ -0,0 +1,212 @@
<template>
<el-dialog
v-model="centerDialogVisible"
:title="$t('workspace.add_member')"
modal-class="authorized-workspace"
width="840"
>
<p class="mb-8 lighter">{{ $t('workspace.member_type') }}</p>
<el-radio-group v-model="listType">
<el-radio value="0">{{ $t('workspace.ordinary_member') }}</el-radio>
<el-radio value="1">{{ $t('workspace.administrator') }}</el-radio>
</el-radio-group>
<p class="mb-8 lighter mt-16">{{ $t('workspace.select_member') }}</p>
<div v-loading="loading" class="flex border" style="height: 428px; border-radius: 6px">
<div class="p-16 border-r">
<el-input
v-model="search"
:validate-event="false"
:placeholder="$t('datasource.search')"
style="width: 364px"
clearable
>
<template #prefix>
<el-icon>
<Search></Search>
</el-icon>
</template>
</el-input>
<div class="mt-8">
<el-checkbox
v-if="!search"
v-model="checkAll"
class="mb-8"
:indeterminate="isIndeterminate"
@change="handleCheckAllChange"
>
{{ $t('datasource.select_all') }}
</el-checkbox>
<el-checkbox-group
v-model="checkedWorkspace"
class="checkbox-group-block"
@change="handleCheckedWorkspaceChange"
>
<el-checkbox
v-for="space in workspaceWithKeywords"
:key="space.id"
:label="space.name"
:value="space"
>
<div class="flex">
<img :src="avatar_personal" width="28px" height="28px" />
<span class="ml-4"> {{ space.name }}</span>
</div>
</el-checkbox>
</el-checkbox-group>
</div>
</div>
<div class="p-16 w-full">
<div class="flex-between mb-16">
<span class="lighter">
{{ $t('workspace.selected_2_people', { msg: checkedWorkspace.length }) }}
</span>
<el-button text @click="clearWorkspaceAll">
{{ $t('workspace.clear') }}
</el-button>
</div>
<div v-for="ele in checkedWorkspace" :key="ele.name" class="flex-between">
<div class="flex align-center">
<img :src="avatar_personal" width="28px" height="28px" />
<span class="ml-4 lighter">{{ ele.name }}</span>
</div>
<el-button text>
<el-icon :size="18" @click="clearWorkspace(ele)"><Close /></el-icon>
</el-button>
</div>
</div>
</div>
<template #footer>
<el-button @click="centerDialogVisible = false"> {{ $t('common.cancel') }}</el-button>
<el-button v-if="!checkedWorkspace.length" disabled type="info">{{
$t('model.add')
}}</el-button>
<el-button v-else type="primary" @click="handleConfirm"> {{ $t('model.add') }} </el-button>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue'
import avatar_personal from '@/assets/workspace/avatar_personal.png'
import Close from '@/assets/svg/icon_close_outlined.svg'
import Search from '@/assets/svg/icon_search-outline_outlined.svg'
import type { CheckboxValueType } from 'element-plus-secondary'
const checkAll = ref(false)
const isIndeterminate = ref(true)
const checkedWorkspace = ref<any[]>([])
const workspace = ref<any[]>([])
const listType = ref('0')
const search = ref('')
const loading = ref(false)
const centerDialogVisible = ref(false)
const workspaceWithKeywords = computed(() => {
return workspace.value.filter((ele: any) => (ele.name as string).includes(search.value))
})
const handleCheckAllChange = (val: CheckboxValueType) => {
checkedWorkspace.value = val ? workspace.value : []
isIndeterminate.value = false
if (!val) {
clearWorkspaceAll()
}
}
const handleCheckedWorkspaceChange = (value: CheckboxValueType[]) => {
const checkedCount = value.length
checkAll.value = checkedCount === workspace.value.length
isIndeterminate.value = checkedCount > 0 && checkedCount < workspace.value.length
}
const open = async () => {
loading.value = true
// const [authList, systemWorkspaceList] = await Promise.all([
// authorizationApi[`getSharedAuthorization${type}`](id),
// workspaceApi.getSystemWorkspaceList(),
// ])
// workspace.value = systemWorkspaceList.data as any
// listType.value = (authList.data || {}).authentication_type || 'WHITE_LIST'
// let workspace_id_list = (authList.data || {}).workspace_id_list || []
// checkedWorkspace.value = workspace.value.filter((ele) => workspace_id_list.includes(ele.id))
// handleCheckedWorkspaceChange(checkedWorkspace.value)
// loading.value = false
// centerDialogVisible.value = true
}
const handleConfirm = () => {
// authorizationApi[`postSharedAuthorization${currentType}`](knowledge_id, {
// workspace_id_list: checkedWorkspace.value.map((ele: any) => ele.id),
// authentication_type: listType.value,
// }).then(() => {
// centerDialogVisible.value = false
// })
}
const clearWorkspace = (val: any) => {
checkedWorkspace.value = checkedWorkspace.value.filter((ele: any) => ele.id !== val.id)
}
const clearWorkspaceAll = () => {
checkedWorkspace.value = []
handleCheckedWorkspaceChange([])
}
defineExpose({
open,
})
</script>
<style lang="less">
.authorized-workspace {
.mb-8 {
margin-bottom: 8px;
}
.mt-16 {
margin-top: 16px;
}
.p-16 {
padding: 16px;
}
.lighter {
font-weight: 400;
font-size: 14px;
line-height: 22px;
}
.border {
border: 1px solid #dee0e3;
}
.w-full {
width: 100%;
}
.mt-8 {
margin-top: 8px;
}
.align-center {
align-items: center;
}
.flex-between {
display: flex;
justify-content: space-between;
}
.ml-4 {
margin-left: 4px;
}
.flex {
display: flex;
}
.border-r {
border-right: 1px solid #dee0e3;
}
}
</style>
@@ -0,0 +1,108 @@
<script lang="ts" setup>
import { ref, reactive } from 'vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const paramsRef = ref()
const paramsForm = reactive({
name: '',
key: '',
val: '',
})
const rules = {
name: [
{
required: true,
message: t('datasource.please_enter') + t('common.empty') + t('model.display_name'),
trigger: 'blur',
},
],
key: [
{
required: true,
message: t('datasource.please_enter') + t('common.empty') + t('model.parameters'),
trigger: 'blur',
},
],
val: [
{
required: true,
message: t('datasource.please_enter') + t('common.empty') + t('model.parameter_value'),
trigger: 'blur',
},
],
}
const initForm = (item: any) => {
if (item) {
Object.assign(paramsForm, { ...item })
}
paramsRef.value.clearValidate()
}
const emits = defineEmits(['submit'])
const submit = () => {
paramsRef.value.validate((res: any) => {
if (res) {
emits('submit', paramsForm)
}
})
}
defineExpose({
initForm,
submit,
})
</script>
<template>
<div class="params-form">
<el-form
ref="paramsRef"
:rules="rules"
label-position="top"
:model="paramsForm"
style="width: 100%"
>
<el-form-item prop="key" :label="$t('model.parameters')">
<el-input
v-model="paramsForm.key"
:placeholder="$t('datasource.please_enter') + $t('common.empty') + $t('model.parameters')"
/>
</el-form-item>
<el-form-item prop="name" :label="$t('model.display_name')">
<el-input
v-model="paramsForm.name"
:placeholder="
$t('datasource.please_enter') + $t('common.empty') + $t('model.display_name')
"
/>
</el-form-item>
<el-form-item prop="val" :label="$t('model.parameter_value')">
<el-input
v-model="paramsForm.val"
:placeholder="
$t('datasource.please_enter') + $t('common.empty') + $t('model.parameter_value')
"
/>
</el-form-item>
</el-form>
</div>
</template>
<style lang="less" scoped>
.params-form {
.ed-form-item--default {
margin-bottom: 16px;
&.is-error {
margin-bottom: 40px;
}
}
.ed-input-number {
width: 100%;
}
}
</style>
@@ -0,0 +1,846 @@
<script lang="ts" setup>
import { ref, computed, onMounted, reactive } from 'vue'
import { workspaceList, workspaceUserList } from '@/api/workspace'
import icon_add_outlined from '@/assets/svg/icon_add_outlined.svg'
import arrow_down from '@/assets/svg/arrow-down.svg'
import icon_searchOutline_outlined from '@/assets/svg/icon_search-outline_outlined.svg'
import EmptyBackground from '@/views/dashboard/common/EmptyBackground.vue'
import icon_form_outlined from '@/assets/svg/icon_moments-categories_outlined.svg'
import assigned from '@/assets/svg/icon_assigned_outlined.svg'
import AuthorizedWorkspaceDialog from './AuthorizedWorkspaceDialog.vue'
import rename from '@/assets/svg/icon_rename_outlined.svg'
import icon_member from '@/assets/svg/icon_member.svg'
import SuccessFilled from '@/assets/svg/gou_icon.svg'
import CircleCloseFilled from '@/assets/svg/icon_ban_filled.svg'
import { useI18n } from 'vue-i18n'
import delIcon from '@/assets/svg/icon_delete.svg'
import icon_more_outlined from '@/assets/svg/icon_more_outlined.svg'
import icon_done_outlined from '@/assets/svg/icon_done_outlined.svg'
const { t } = useI18n()
const multipleSelectionAll = ref<any[]>([])
const tableList = ref([] as any[])
const keywords = ref('')
const keywordsMember = ref('')
const workspaceFormRef = ref()
const authorizedWorkspaceDialog = ref()
const tableListWithSearch = computed(() => {
if (!keywords.value) return tableList.value
return tableList.value.filter((ele) =>
ele.name.toLowerCase().includes(keywords.value.toLowerCase())
)
})
const workspaceForm = reactive({
name: '',
id: '',
})
const userTypeList = [
{
name: t('workspace.administrator'),
value: '0',
},
{
name: t('workspace.ordinary_member'),
value: '1',
},
]
const handleUserTypeChange = (val: any, row: any) => {
row.user_source = val
}
const rules = {
name: [
{
required: true,
message: t('datasource.please_enter') + t('common.empty') + t('workspace.workspace_name'),
trigger: 'blur',
},
],
}
const fieldListWithSearch = computed(() => {
if (!keywords.value) return fieldList.value
return fieldList.value.filter((ele: any) =>
ele.name.toLowerCase().includes(keywordsMember.value.toLowerCase())
)
})
const currentTable = ref<any>({})
const init = () => {
workspaceList().then((res) => {
tableList.value = res
})
}
onMounted(() => {
init()
})
const fieldDialog = ref<boolean>(false)
const multipleTableRef = ref()
const isIndeterminate = ref(true)
const checkAll = ref(false)
const fieldList = ref<any>([])
const pageInfo = reactive({
currentPage: 1,
pageSize: 1,
total: 0,
})
const clickTable = (table: any) => {
currentTable.value = table
pageInfo.currentPage = 1
search()
}
const cancelDelete = () => {
multipleSelectionAll.value = []
checkAll.value = false
isIndeterminate.value = false
}
const deleteBatchUser = () => {
ElMessageBox.confirm(
t('workspace.selected_2_members', { msg: multipleSelectionAll.value.length }),
{
confirmButtonType: 'danger',
confirmButtonText: t('dashboard.delete'),
cancelButtonText: t('common.cancel'),
customClass: 'confirm-no_icon',
autofocus: false,
callback: (val: string) => {
console.log(val)
},
}
)
}
const deleteHandler = (row: any) => {
ElMessageBox.confirm(t('workspace.member_feng_yibudao', { msg: row.name }), {
confirmButtonType: 'danger',
confirmButtonText: t('dashboard.delete'),
cancelButtonText: t('common.cancel'),
customClass: 'confirm-no_icon',
autofocus: false,
callback: (val: string) => {
console.log(val)
},
})
.then(() => {
/* userApi.delete(id).then(() => {
ElMessage({
type: 'success',
message: 'Delete completed',
})
search()
}) */
})
.catch(() => {
ElMessage({
type: 'info',
message: 'Delete canceled',
})
})
}
const handleSelectionChange = (val: any[]) => {
const ids = fieldList.value.map((ele: any) => ele.id)
multipleSelectionAll.value = [
...multipleSelectionAll.value.filter((ele) => !ids.includes(ele.id)),
...val,
]
isIndeterminate.value = !(val.length === 0 || val.length === fieldList.value.length)
checkAll.value = val.length === fieldList.value.length
}
const handleCheckAllChange = (val: any) => {
isIndeterminate.value = false
handleSelectionChange(val ? fieldList.value : [])
if (val) {
handleToggleRowSelection()
} else {
multipleTableRef.value.clearSelection()
}
}
const handleToggleRowSelection = (check: boolean = true) => {
let i = 0
const ids = multipleSelectionAll.value.map((ele: any) => ele.id)
for (const key in fieldList.value) {
if (ids.includes((fieldList.value[key] as any).id)) {
i += 1
multipleTableRef.value.toggleRowSelection(fieldList.value[key], check)
}
}
checkAll.value = i === fieldList.value.length
isIndeterminate.value = !(i === 0 || i === fieldList.value.length)
}
const handleAddMember = () => {
authorizedWorkspaceDialog.value.open(1)
}
const search = () => {
workspaceUserList({ oid: currentTable.value.id }, pageInfo.currentPage, pageInfo.pageSize).then(
(res) => {
fieldList.value = res.items
fieldList.value = [{ id: 0, user_source: '管理员' }]
// datasourceApi.previewData(props.info.id, buildData()).then((res) => {
// previewData.value = res
// })
}
)
}
const closeField = () => {
fieldDialog.value = false
}
const saveField = () => {
workspaceFormRef.value.validate((res: any) => {
if (res) {
console.log(res)
}
})
}
const delWorkspace = (row: any) => {
ElMessageBox.confirm(t('workspace.workspace_de_workspace', { msg: row.name }), {
confirmButtonType: 'danger',
confirmButtonText: t('dashboard.delete'),
cancelButtonText: t('common.cancel'),
customClass: 'confirm-no_icon',
autofocus: false,
callback: (val: string) => {
console.log(val)
},
})
.then(() => {
/* userApi.delete(id).then(() => {
ElMessage({
type: 'success',
message: 'Delete completed',
})
search()
}) */
})
.catch(() => {
ElMessage({
type: 'info',
message: 'Delete canceled',
})
})
}
const addWorkspace = (row: any) => {
if (row) {
workspaceForm.name = row.name
workspaceForm.id = row.id
} else {
workspaceForm.name = ''
workspaceForm.id = ''
}
fieldDialog.value = true
}
const handleSizeChange = (val: number) => {
pageInfo.pageSize = val
search()
}
const handleCurrentChange = (val: number) => {
pageInfo.currentPage = val
search()
}
</script>
<template>
<div class="workspace no-padding">
<div class="side-list">
<div class="select-table_top">
{{ $t('user.workspace') }}
<el-tooltip effect="dark" :content="$t('workspace.add_workspace')" placement="top">
<el-icon size="18" @click="addWorkspace">
<icon_add_outlined></icon_add_outlined>
</el-icon>
</el-tooltip>
</div>
<el-input
v-model="keywords"
clearable
style="width: 232px"
:placeholder="$t('datasource.search')"
>
<template #prefix>
<el-icon>
<icon_searchOutline_outlined class="svg-icon" />
</el-icon>
</template>
</el-input>
<div class="list-content">
<div
v-for="ele in tableListWithSearch"
:key="ele.name"
class="model"
:class="currentTable.name === ele.name && 'isActive'"
:title="ele.name"
@click="clickTable(ele)"
>
<el-icon size="16">
<icon_form_outlined></icon_form_outlined>
</el-icon>
<span class="name">{{ ele.name }}</span>
<el-popover
trigger="click"
:teleported="false"
popper-class="popover-card"
placement="bottom"
>
<template #reference>
<el-icon class="more" size="16" style="margin-left: auto" @click.stop>
<icon_more_outlined></icon_more_outlined>
</el-icon>
</template>
<div class="content">
<div class="item" @click.stop="addWorkspace(ele)">
<el-icon size="16">
<rename></rename>
</el-icon>
{{ $t('datasource.edit') }}
</div>
<div class="item" @click.stop="delWorkspace(ele)">
<el-icon size="16">
<delIcon></delIcon>
</el-icon>
{{ $t('dashboard.delete') }}
</div>
</div>
</el-popover>
</div>
<div v-if="!!keywords && !tableListWithSearch.length" class="no-result">
{{ $t('workspace.historical_dialogue') }}
</div>
</div>
</div>
<div v-if="currentTable.name" class="info-table">
<div class="table-name">
<div class="name">
{{ currentTable.name }}
<span class="line"></span>
<el-icon style="margin: 0 4px 0 8px" size="16">
<icon_member></icon_member>
</el-icon>
{{ pageInfo.total }}
</div>
<div class="notes">
<el-button type="primary" @click="handleAddMember">
<template #icon>
<icon_add_outlined></icon_add_outlined>
</template>
{{ $t('workspace.add_member') }}
</el-button>
<el-input
v-model="keywordsMember"
clearable
style="width: 232px"
:placeholder="$t('workspace.name_username_email')"
>
<template #prefix>
<el-icon>
<icon_searchOutline_outlined class="svg-icon" />
</el-icon>
</template>
</el-input>
</div>
</div>
<div class="table-content">
<div class="preview-or-schema">
<el-table
ref="multipleTableRef"
:data="fieldListWithSearch"
style="width: 100%"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" />
<el-table-column prop="name" :label="$t('user.name')" width="160" />
<el-table-column prop="account" :label="$t('user.account')" width="160" />
<el-table-column prop="status" :label="$t('user.user_status')">
<template #default="scope">
<div
class="user-status-container"
:class="[scope.row.status ? 'active' : 'disabled']"
>
<el-icon size="16">
<SuccessFilled v-if="scope.row.status" />
<CircleCloseFilled v-else />
</el-icon>
<span>{{ $t(`user.${scope.row.status ? 'enabled' : 'disabled'}`) }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="email" :label="$t('user.email')" width="204" />
<el-table-column
class-name="user-source"
prop="user_source"
:label="$t('user.user_source')"
width="120"
>
<template #default="scope">
<el-popover popper-class="system-workspace_user" placement="bottom">
<template #reference>
<div style="display: flex; align-items: center; justify-content: space-between">
<span>{{ scope.row.user_source }}</span>
<el-icon size="16">
<arrow_down></arrow_down>
</el-icon>
</div>
</template>
<div class="popover">
<div class="popover-content">
<div
v-for="ele in userTypeList"
:key="ele.name"
class="popover-item"
:class="ele.name === scope.row.user_source && 'isActive'"
@click="handleUserTypeChange(ele.value, scope.row)"
>
<div class="model-name">{{ ele.name }}</div>
<el-icon size="16" class="done">
<icon_done_outlined></icon_done_outlined>
</el-icon>
</div>
</div>
</div>
</el-popover>
</template>
</el-table-column>
<el-table-column fixed="right" width="80" :label="t('ds.actions')">
<template #default="scope">
<div class="field-comment">
<el-tooltip
:offset="14"
effect="dark"
:content="$t('workspace.remove')"
placement="top"
>
<el-icon class="action-btn" size="16" @click="deleteHandler(scope.row)">
<assigned></assigned>
</el-icon>
</el-tooltip>
</div>
</template>
</el-table-column>
<template #empty>
<EmptyBackground
v-if="!keywordsMember && !fieldListWithSearch.length"
:description="$t('workspace.no_user')"
img-type="noneWhite"
/>
<EmptyBackground
v-if="!!keywordsMember && !fieldListWithSearch.length"
:description="$t('datasource.relevant_content_found')"
img-type="tree"
/>
</template>
</el-table>
</div>
<div class="pagination-container">
<el-pagination
v-model:current-page="pageInfo.currentPage"
v-model:page-size="pageInfo.pageSize"
:page-sizes="[10, 20, 30]"
:background="true"
layout="total, sizes, prev, pager, next, jumper"
:total="pageInfo.total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</div>
<div v-if="multipleSelectionAll.length" class="bottom-select">
<el-checkbox
v-model="checkAll"
:indeterminate="isIndeterminate"
@change="handleCheckAllChange"
>
{{ $t('datasource.select_all') }}
</el-checkbox>
<button class="danger-button" @click="deleteBatchUser">{{ $t('workspace.remove') }}</button>
<span class="selected">{{
$t('user.selected_2_items', { msg: multipleSelectionAll.length })
}}</span>
<el-button text @click="cancelDelete">
{{ $t('common.cancel') }}
</el-button>
</div>
</div>
</div>
<AuthorizedWorkspaceDialog ref="authorizedWorkspaceDialog"></AuthorizedWorkspaceDialog>
<el-dialog
v-model="fieldDialog"
:title="$t(workspaceForm.id ? 'workspace.rename_a_workspace' : 'workspace.add_workspace')"
width="420"
destroy-on-close
:close-on-click-modal="false"
@before-closed="closeField"
>
<el-form
ref="workspaceFormRef"
:model="workspaceForm"
label-width="180px"
label-position="top"
:rules="rules"
class="form-content_error"
>
<el-form-item prop="name" :label="t('workspace.workspace_name')">
<el-input
v-model="workspaceForm.name"
:placeholder="
$t('datasource.please_enter') + $t('common.empty') + $t('workspace.workspace_name')
"
/>
</el-form-item>
</el-form>
<div style="display: flex; justify-content: flex-end; margin-top: 20px">
<el-button @click="closeField">{{ t('common.cancel') }}</el-button>
<el-button type="primary" @click="saveField">{{
t(workspaceForm.id ? 'common.save' : 'model.add')
}}</el-button>
</div>
</el-dialog>
</template>
<style lang="less" scoped>
.workspace {
height: 100%;
position: relative;
.side-list {
width: 280px;
padding: 8px 16px;
height: 100%;
border-right: 1px solid #1f232926;
.select-table_top {
height: 40px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px;
.ed-icon {
cursor: pointer;
color: var(--ed-color-primary);
position: relative;
&:hover {
&::after {
display: block;
}
}
&::after {
content: '';
display: none;
position: absolute;
width: 26px;
height: 26px;
background: #1cba901a;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
border-radius: 6px;
}
}
}
.ed-input {
margin: 8px;
}
.list-content {
height: calc(100% - 100px);
.more {
display: none;
&.ed-icon {
position: relative;
cursor: pointer;
margin-top: 4px;
&::after {
content: '';
background-color: #1f23291a;
position: absolute;
border-radius: 6px;
width: 24px;
height: 24px;
transform: translate(-50%, -50%);
top: 50%;
left: 50%;
display: none;
}
&:hover {
&::after {
display: block;
}
}
}
}
.no-result {
margin-top: 72px;
font-weight: 400;
font-size: 14px;
line-height: 22px;
text-align: center;
color: #646a73;
}
.model {
width: 100%;
height: 40px;
display: flex;
align-items: center;
padding-left: 8px;
border-radius: 4px;
cursor: pointer;
padding-right: 8px;
.name {
margin-left: 8px;
font-weight: 500;
font-size: 14px;
line-height: 22px;
max-width: 80%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
&:hover {
background: #1f23291a;
.more {
display: block;
}
}
&.isActive {
background: #1cba901a;
color: var(--ed-color-primary);
}
}
}
}
.info-table {
position: absolute;
right: 0;
top: 0;
width: calc(100% - 280px);
height: 100%;
.table-name {
padding: 16px 24px 0 24px;
.name {
font-weight: 500;
font-size: 16px;
line-height: 24px;
display: flex;
align-items: center;
.line {
height: 16px;
width: 1px;
background: #bbbfc4;
margin-left: 8px;
}
}
.notes {
display: flex;
align-items: center;
justify-content: space-between;
margin: 16px 0;
}
}
.table-content {
padding: 0 24px 16px 24px;
height: calc(100% - 104px);
.pagination-container {
display: flex;
justify-content: end;
align-items: center;
margin-top: 16px;
}
.preview-or-schema {
:deep(.user-source) {
cursor: pointer;
&:hover {
.ed-icon {
transform: rotate(180deg);
}
}
}
.user-status-container {
display: flex;
align-items: center;
font-weight: 400;
font-size: 14px;
line-height: 22px;
.ed-icon {
margin-right: 8px;
}
}
.field-comment {
height: 24px;
.ed-icon {
position: relative;
cursor: pointer;
margin-top: 4px;
&::after {
content: '';
background-color: #1f23291a;
position: absolute;
border-radius: 6px;
width: 24px;
height: 24px;
transform: translate(-50%, -50%);
top: 50%;
left: 50%;
display: none;
}
&:hover {
&::after {
display: block;
}
}
}
}
.preview-num {
margin: 12px 0;
font-weight: 400;
font-size: 14px;
line-height: 22px;
color: #646a73;
}
}
}
}
}
</style>
<style lang="less">
.popover-card.popover-card.popover-card {
box-shadow: 0px 4px 8px 0px #1f23291a;
border-radius: 4px;
border: 1px solid #dee0e3;
width: 120px !important;
min-width: 120px !important;
padding: 0;
.content {
position: relative;
&::after {
position: absolute;
content: '';
top: 40px;
left: 0;
width: 100%;
height: 1px;
background: #dee0e3;
}
.item {
position: relative;
padding-left: 12px;
height: 40px;
display: flex;
align-items: center;
cursor: pointer;
.ed-icon {
margin-right: 8px;
color: #646a73;
}
&:hover {
&::after {
display: block;
}
}
&::after {
content: '';
width: 112px;
height: 32px;
border-radius: 4px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: #1f23291a;
display: none;
}
}
}
}
.system-workspace_user.system-workspace_user {
padding: 0;
width: 120px !important;
min-width: 120px !important;
box-shadow: 0px 4px 8px 0px #1f23291a;
border: 1px solid #dee0e3;
.popover {
.popover-content {
padding: 4px;
position: relative;
&::after {
position: absolute;
content: '';
left: 0;
top: 40px;
width: 100%;
height: 1px;
background: #1f232926;
}
}
.popover-item {
height: 32px;
display: flex;
align-items: center;
padding-left: 12px;
padding-right: 8px;
position: relative;
border-radius: 4px;
cursor: pointer;
&:hover {
background: #1f23291a;
}
&:nth-child(2) {
margin: 9px 0 0 0;
}
.model-name {
margin-left: 8px;
font-weight: 400;
font-size: 14px;
line-height: 22px;
}
.done {
margin-left: auto;
display: none;
}
&.isActive {
color: var(--ed-color-primary);
.done {
display: block;
}
}
}
}
}
</style>