@yqg/permission 1.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.html ADDED
@@ -0,0 +1,14 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>Vite + Vue + TS</title>
8
+ <!-- csp script-src 'self' -->
9
+ </head>
10
+ <body>
11
+ <div id="app"></div>
12
+ <script type="module" src="/src/main.ts"></script>
13
+ </body>
14
+ </html>
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@yqg/permission",
3
+ "version": "1.0.0-beta.0",
4
+ "main": "dist/index.js",
5
+ "module": "dist/index.js",
6
+ "type": "module",
7
+ "publishConfig": {
8
+ "access": "public",
9
+ "registry": "https://registry.npmjs.org/"
10
+ },
11
+ "scripts": {
12
+ "build": "vue-tsc -b && vite build",
13
+ "dev": "vite",
14
+ "preview": "vite preview"
15
+ },
16
+ "dependencies": {
17
+ "@ant-design/icons-vue": "^7.0.1",
18
+ "ant-design-vue": "4.x",
19
+ "axios": "^1.7.7",
20
+ "vite-plugin-css-injected-by-js": "^3.5.2",
21
+ "vue": "^3.5.12"
22
+ },
23
+ "devDependencies": {
24
+ "@vitejs/plugin-vue": "^5.1.4",
25
+ "typescript": "~5.5.4",
26
+ "vite": "^5.4.9",
27
+ "vue-tsc": "^2.1.8"
28
+ }
29
+ }
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
package/src/App.vue ADDED
@@ -0,0 +1,79 @@
1
+ <script setup lang="ts">
2
+ import {reactive, ref} from 'vue';
3
+ import {Button} from 'ant-design-vue';
4
+
5
+ // 中英印尼菲律宾
6
+ type LocaleType = 'zh-CN' | 'en-US' | 'id-ID' | 'fil-PH';
7
+
8
+ const color = ref<string>('#1677ff');
9
+ const locale = ref<LocaleType>('zh-CN');
10
+
11
+ const permissions = reactive(['PPDL.0801-test2.XIUGAI.test5', 'PPDL.0801-test2.XIUGAI.test4', 'PPDL.0801-test2.XIUGAI.test6']);
12
+ // const permissions = reactive(['CRANE.BUSINESS.QUERY', 'RANE.BUSINESS.CREATE', 'CRANE.BUSINESS.UPDATE', 'CRANE.BUSINESS.DELETE']);
13
+ const changeColor = () => {
14
+ color.value = color.value === '#f00' ? '#1677ff' : '#f00';
15
+ }
16
+ const changeLocale = () => {
17
+ locale.value = locale.value === 'zh-CN' ? 'en-US' : 'zh-CN';
18
+ }
19
+
20
+ </script>
21
+
22
+ <template>
23
+ <Button type="primary" @click="changeColor" style="margin-right: 10px;">切换主题色</Button>
24
+ <Button danger @click="changeLocale">切换语言</Button>
25
+
26
+ <div style="display: flex; margin-top: 50px;">
27
+ <div class="case-card">
28
+ <div>1:默认组件</div>
29
+ <!-- 03541 -->
30
+ <!-- 02124 -->
31
+ <yqg-permission
32
+ :permissions="permissions"
33
+ workNumber="02124"
34
+ businessCode="PPDL"
35
+ :color="color"
36
+ :locale="locale"
37
+ @success="() => {console.log('成功')}"
38
+ >
39
+ </yqg-permission>
40
+ </div>
41
+ <!-- <data class="case-card">
42
+ <div>2:文字组件</div>
43
+ <yqg-permission
44
+ :permissions="permissions"
45
+ businessCode="CRANE"
46
+ :color="color"
47
+ workNumber="02124"
48
+ type="text"
49
+ >
50
+ </yqg-permission>
51
+ </data>
52
+ -->
53
+
54
+
55
+ <!-- <div class="case-card">
56
+ <div>3:自定义</div>
57
+ <yqg-permission
58
+ :permissions="permissions"
59
+ businessCode="CRANE"
60
+ :color="color"
61
+ workNumber="02124"
62
+ locale="zh-CN"
63
+ type="custom"
64
+ >
65
+ <div style="color: red;" slot="custom">自定义按钮</div>
66
+ </yqg-permission>
67
+ </div> -->
68
+
69
+ </div>
70
+
71
+ </template>
72
+
73
+ <style>
74
+ .case-card {
75
+ height: 380px;
76
+ width: 380px;
77
+ }
78
+ </style>
79
+
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,59 @@
1
+ import { message } from 'ant-design-vue';
2
+
3
+ import axios from 'axios';
4
+
5
+ const YQG_SUCCESS = 0;
6
+ const YQG_NOT_LOGIN = 10008;
7
+
8
+ axios.defaults.withCredentials = true;
9
+ // instance.defaults.paramsSerializer = (params) => qs.stringify(params, { arrayFormat: 'repeat' });
10
+
11
+ axios.defaults.headers.common['Accept-Language'] = localStorage.getItem('permission_locale') as string;
12
+
13
+ axios.interceptors.response.use(
14
+ (res: any) => {
15
+ const { status } = res?.data || {};
16
+ const data = res?.data || {};
17
+ const { code, detail } = status || {};
18
+ if (res?.config?.responseType === 'blob') {
19
+ if (res?.data?.type === 'application/json') {
20
+ // 没有拿到文件时,将拿到的 blob 流转为 js 对象,进而拿到错误信息(detail)并返回 reject 状态
21
+ // 不处理将默认下载一个 json 文件
22
+ const reader: any = new FileReader();
23
+ reader.readAsText(res.data, 'utf-8');
24
+ reader.onload = () => {
25
+ const parsedObj = JSON.parse(reader.result);
26
+ message.info(parsedObj?.status?.detail);
27
+ };
28
+
29
+ return Promise.reject();
30
+ } else {
31
+ return res;
32
+ }
33
+ }
34
+
35
+ if (res.headers && res.headers['content-type'] === 'image/jpeg') {
36
+ return res;
37
+ }
38
+
39
+ switch (code) {
40
+ case YQG_SUCCESS:
41
+ return Promise.resolve(data);
42
+ case YQG_NOT_LOGIN: {
43
+ return Promise.reject(data);
44
+ }
45
+ default: {
46
+ message.error(detail);
47
+ return Promise.reject(data);
48
+ }
49
+ }
50
+ },
51
+ (err: any) => {
52
+ const detail = err?.data?.status?.detail || 'Unknown error';
53
+ message.error(detail);
54
+
55
+ return Promise.reject(err);
56
+ },
57
+ );
58
+
59
+ export default axios;
@@ -0,0 +1,15 @@
1
+ import axios from './axios';
2
+
3
+ const urlPrefix = '/admin/crane';
4
+
5
+ type apiType = {
6
+ [key in string]: (params: any) => Promise<{body: any}>
7
+ }
8
+
9
+ export default {
10
+ getPermissions: (params: any) => axios.get(`${urlPrefix}/permission/apply`, {params}),
11
+
12
+ getFlowPreview: (params: any)=> axios.post(`${urlPrefix}/permission/apply/oa/flow/submit/preview`, params),
13
+
14
+ submitApply: (params: any) => axios.post(`${urlPrefix}/permission/apply/oa/flow/submit`, params),
15
+ } as apiType
@@ -0,0 +1,177 @@
1
+ <template>
2
+ <Modal
3
+ v-model:open="open"
4
+ :title="t('permissionApply')"
5
+ width="800px"
6
+ @ok="handleOk"
7
+ :okText="t('submit')"
8
+ :cancelText="t('cancel')">
9
+ <Form
10
+ ref="formRef"
11
+ :model="formState"
12
+ :labelCol="{ span: 4 }"
13
+ :wrapperCol="{ span: 19 }">
14
+
15
+ <FormItem
16
+ :label="t('applyPermission')"
17
+ name="roleIds"
18
+ :rules="[{ required: true, message: t('selectPlaceholder')}]">
19
+ <CheckboxGroup
20
+ v-model:value="formState.roleIds"
21
+ style="display: block;"
22
+ @change="onChangeHandler"
23
+ >
24
+ <CheckboxItem
25
+ v-for="item in permissionList"
26
+ :key="item.roleId"
27
+ :item="item"
28
+ :checkedIds="formState.roleIds"
29
+ :onChangeTime="onChangeHandler">
30
+ </CheckboxItem>
31
+ </CheckboxGroup>
32
+ </FormItem>
33
+
34
+ <FormItem
35
+ name="applyReason"
36
+ :label="t('applyReason')"
37
+ :rules="[{ required: true, message: t('reasonPlaceholder')}]">
38
+ <Textarea
39
+ v-model:value="formState.applyReason"
40
+ :placeholder="t('applyReasonPlaceholder')"
41
+ :auto-size="{ minRows: 2, maxRows: 5 }">
42
+ </Textarea>
43
+ <div style="margin-top: 4px;">
44
+ <Tag
45
+ :bordered="false"
46
+ style="cursor: pointer;"
47
+ v-for="item in reasons"
48
+ @click="addReasonHandler(item)"
49
+ :key="item">{{ item }}</Tag>
50
+ </div>
51
+ </FormItem>
52
+
53
+ <FormItem :label="t('approvalProcess')">
54
+ <ApprovalSteps :stepNodes="stepNodes"/>
55
+ </FormItem>
56
+ </Form>
57
+
58
+ <SuccessModal ref="successModal"/>
59
+
60
+ </Modal>
61
+ </template>
62
+ <script lang="ts" setup>
63
+ import {
64
+ defineModel,
65
+ reactive,
66
+ defineAsyncComponent,
67
+ toRef,
68
+ ref,
69
+ watch,
70
+ } from 'vue';
71
+ import Http from '../axios/index';
72
+ import {
73
+ Modal,
74
+ Form,
75
+ FormItem,
76
+ CheckboxGroup,
77
+ Textarea,
78
+ Tag,
79
+ } from 'ant-design-vue';
80
+ import SuccessModal from './success-modal.vue';
81
+ import ApprovalSteps from './approval-steps.vue';
82
+ import t, {throttle} from '../utils';
83
+ const CheckboxItem = defineAsyncComponent(() =>
84
+ import('./checkbox-item.vue')
85
+ );
86
+
87
+ const reasons = [
88
+ t('applyReason1'),
89
+ t('applyReason2'),
90
+ t('applyReason3'),
91
+ ]
92
+
93
+ const props = defineProps({
94
+ permissionList: {
95
+ type: Array as () => PermissionType[],
96
+ default: () => []
97
+ },
98
+ businessCode: {
99
+ type: String,
100
+ default: ''
101
+ },
102
+ workNumber: {
103
+ type: String,
104
+ default: ''
105
+ },
106
+
107
+ });
108
+ const emit = defineEmits(['onSuccess']);
109
+ const open = defineModel({
110
+ required: true,
111
+ default: false,
112
+ });
113
+ const permissionList = toRef(props, 'permissionList');
114
+ const businessCode = toRef(props, 'businessCode');
115
+ const submitWorkNumber = toRef(props, 'workNumber');
116
+ const successModal = ref<InstanceType<typeof SuccessModal>>();
117
+ const formRef = ref();
118
+ let stepNodes = ref([]);
119
+ const formState = reactive<formStateType>({
120
+ applyBusinessCode: businessCode.value,
121
+ roleIds: [],
122
+ roleVoList: [],
123
+ applyReason: '',
124
+ submitWorkNumber: submitWorkNumber.value,
125
+ });
126
+
127
+ const addReasonHandler = (item: string) => {
128
+ if (formState.applyReason.includes(item)) {
129
+ return;
130
+ }
131
+ if (!formState.applyReason || formState.applyReason.endsWith('、')) {
132
+ formState.applyReason += item;
133
+ } else {
134
+ formState.applyReason += `、${item}`;
135
+ }
136
+ }
137
+
138
+ const handleOk = async() => {
139
+ formRef.value.validate().then(async() => {
140
+ const params = getParams();
141
+ let res = await Http.submitApply(params);
142
+ const url = res?.body?.oaFlowUrl;
143
+ open.value = false;
144
+ emit('onSuccess');
145
+ successModal.value?.countDown(url);
146
+ })
147
+ };
148
+
149
+ const getParams = () => {
150
+ formState.roleVoList = formState.roleIds.map((roleId) => {
151
+ return {
152
+ roleId,
153
+ validTime: permissionList.value.find((item) => item.roleId === roleId)?.validTime
154
+ };
155
+ });
156
+ return formState;
157
+ }
158
+
159
+ const onChangeHandler = throttle(async () => {
160
+ if (!formState.roleIds.length) {
161
+ stepNodes.value = [];
162
+ return;
163
+ }
164
+ const params = getParams();
165
+ let res = await Http.getFlowPreview(params);
166
+
167
+ stepNodes.value = res?.body?.nodes || [];
168
+ }, 0)
169
+
170
+ watch(() => open.value, (cur) => {
171
+ if (cur) {
172
+ formRef.value?.resetFields();
173
+ stepNodes.value = [];
174
+ }
175
+ })
176
+
177
+ </script>
@@ -0,0 +1,67 @@
1
+ <template>
2
+ <div v-if="stepNodes?.length > 1" class="crane-step-wraper">
3
+ <div v-for="(item, index) in stepNodes" :key="item.auditorName" class="crane-step-node">
4
+ <span style="white-space: nowrap;">
5
+ {{ item.auditorName }} {{ getSubTip(index) }}
6
+ </span>
7
+
8
+ <Popover v-if="item.employeeNameList?.length">
9
+ <template #content>
10
+ {{item.auditorName}}:{{ item.employeeNameList.join(',')}}
11
+ </template>
12
+ <ExclamationCircleOutlined style="margin: 0 2px"/>
13
+ </Popover>
14
+
15
+ <img
16
+ v-if="index !== stepNodes.length - 1"
17
+ :src="arrowImg"
18
+ class="crane-step-icon">
19
+
20
+ </div>
21
+ </div>
22
+ <span v-else-if="stepNodes?.length === 1">
23
+ {{t('noNeed')}}
24
+ </span>
25
+ <span v-else>-</span>
26
+ </template>
27
+ <script lang="ts" setup>
28
+ import { PropType, toRef } from 'vue';
29
+ import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
30
+ import arrowImg from '@/assets/arrow.png';
31
+ import { Popover } from 'ant-design-vue';
32
+ import t from '../utils';
33
+
34
+ const props = defineProps({
35
+ stepNodes: {
36
+ type: Array as PropType<any[]>,
37
+ required: true,
38
+ default: () => []
39
+ },
40
+ });
41
+
42
+ const stepNodes = toRef(props, 'stepNodes');
43
+
44
+ const getSubTip = (index: number) => {
45
+ return index === stepNodes.value.length - 1 ? `[${t('end')}]` : index === 0 ? `[${t('start')}]` : '';
46
+ }
47
+
48
+ </script>
49
+ <style scoped>
50
+ .crane-step-wraper {
51
+ display: flex;
52
+ align-items: center;
53
+ width: 100%;
54
+ overflow-x: scroll;
55
+ line-height: 32px;
56
+ padding-bottom: 8px;
57
+ }
58
+ .crane-step-node {
59
+ display: flex;
60
+ align-items: center;
61
+ }
62
+ .crane-step-icon {
63
+ margin: 0 8px;
64
+ height: auto;
65
+ max-width: none;
66
+ }
67
+ </style>
@@ -0,0 +1,194 @@
1
+ <template>
2
+ <div class="crane-checkbox-line">
3
+ <Checkbox :value="item.roleId" :disabled="!!item.businessApplyType" @change="onCheck">
4
+ <div class="crane-flex-center crane-checkbox-label">
5
+ <Tag
6
+ v-if="item.securityLevel"
7
+ :bordered="false"
8
+ :color="levelMap[item.securityLevel].color"
9
+ class="crane-tag-position">
10
+ {{ levelMap[item.securityLevel].text }}
11
+ </Tag>
12
+
13
+ <span>{{t(`operationType.${item.operationType}`)}}|
14
+ </span>
15
+ <Popover>
16
+ <template #content>
17
+ {{ item.name }}
18
+ </template>
19
+ <span class="crane-text-overflow">{{ item.name }}</span>
20
+ </Popover>
21
+ <Tag
22
+ v-if="item.businessApplyType"
23
+ :bordered="false"
24
+ class="crane-tag-position crane-margin-left-4 crane-margin-right-0"
25
+ :class="item.businessApplyType !== StatusTypePending ? 'crane-disabled-color' : ''">
26
+ {{ statusMap[item.businessApplyType].text }}
27
+ </Tag>
28
+ </div>
29
+ </Checkbox>
30
+
31
+ <Popover v-if="item.desc">
32
+ <template #content>
33
+ {{ item.desc }}
34
+ </template>
35
+ <QuestionCircleOutlined class=" crane-week-color"/>
36
+ </Popover>
37
+
38
+ <Popover v-if="item.relatedDepartments">
39
+ <template #content>
40
+ {{t('adaptDepartment')}}:{{ item.relatedDepartments.map((item: any) => {
41
+ return item.name;
42
+ }).join(',') }}
43
+ </template>
44
+ <span class="crane-flex-center crane-margin-left-4">
45
+ <img :src="departmentImg" height="14" width="14">
46
+ <span class="crane-week-color crane-margin-left-4">{{ item.relatedDepartments.length }}</span>
47
+ </span>
48
+ </Popover>
49
+
50
+ <span v-show="checkedIds.includes(item.roleId)" class="crane-week-color crane-margin-left-12">
51
+ {{t('availableTime')}}:
52
+ <Select
53
+ v-model:value="validTime"
54
+ style="width: 100px"
55
+ :options="timeStatusOptions"
56
+ @change="onChangeTimeHandler"
57
+ size="small"
58
+ >
59
+ </Select>
60
+ </span>
61
+ </div>
62
+ </template>
63
+ <script lang="ts" setup>
64
+ import { PropType, ref} from 'vue';
65
+ import {Checkbox, Tag, Popover, Select } from 'ant-design-vue';
66
+ import { QuestionCircleOutlined } from '@ant-design/icons-vue';
67
+ import departmentImg from '@/assets/department.png';
68
+ import t, {formatOptions} from '../utils';
69
+
70
+ const levelMap:LevelMapType = {
71
+ L1: {
72
+ color: 'green',
73
+ text: t('levels.L1')
74
+ },
75
+ L2: {
76
+ color: 'orange',
77
+ text: t('levels.L2')
78
+ },
79
+ L3: {
80
+ color: 'red',
81
+ text: t('levels.L3')
82
+ },
83
+ };
84
+
85
+ const statusMap:StatusMapType = {
86
+ PENDING: {
87
+ color: 'orange',
88
+ text: t('status.PENDING')
89
+ },
90
+ OWNER: {
91
+ color: 'green',
92
+ text: t('status.OWNER')
93
+ },
94
+ NO: {
95
+ color: 'red',
96
+ text: t('status.NO')
97
+ }
98
+ }
99
+
100
+ let props = defineProps({
101
+ item: {
102
+ type: Object as PropType<PermissionType>,
103
+ default: () => {}
104
+ },
105
+ checkedIds: {
106
+ type: Array as PropType<number[]>,
107
+ default: []
108
+ },
109
+ onChangeTime: {
110
+ type: Function as PropType<() => void>,
111
+ default: () => {}
112
+ }
113
+ });
114
+ const timeStatusOptions = formatOptions('availiables');
115
+ const StatusTypePending = 'PENDING';
116
+ const validTime = ref('');
117
+
118
+ //1登录人所在部门 = 权限适用范围,都默认90天,不需要区分等级和类型,
119
+ //2登录人所在部门 ≠ 权限适用范围,根据等级来,高(L3)默认给7天,中(L2)默认给30天,低(L1)默认给60天
120
+ const setDefaultTime = (item: PermissionType) => {
121
+ const validMap = {
122
+ L1: 'SIXTY_DAYS',
123
+ L2: 'THIRTY_DAYS',
124
+ L3: 'SEVEN_DAYS',
125
+ };
126
+ const { relatedDepartmentIds = [], curDepartmentId = 0, securityLevel } = item;
127
+ if (relatedDepartmentIds.includes(curDepartmentId as number)) {
128
+ item.validTime = 'NINETY_DAYS';
129
+ } else {
130
+ item.validTime = validMap[securityLevel];
131
+ }
132
+ return item.validTime;
133
+ };
134
+
135
+ const onChangeTimeHandler = (value: any) => {
136
+ props.item.validTime = value;
137
+ props.onChangeTime();
138
+ };
139
+
140
+ const onCheck = (e: any) => {
141
+ if (e.target.checked) {
142
+ validTime.value = setDefaultTime(props.item);
143
+ }
144
+ };
145
+
146
+ </script>
147
+ <style scoped>
148
+ .crane-flex-center {
149
+ display: flex;
150
+ align-items: center;
151
+ }
152
+ .crane-checkbox-line {
153
+ margin-bottom: 12px;
154
+ display: flex;
155
+ align-items: center;
156
+ }
157
+ .crane-checkbox-line:last-child {
158
+ margin-bottom: 0;
159
+ }
160
+ .crane-checkbox-label {
161
+ max-width: 346px;
162
+ }
163
+ .crane-tag-position {
164
+ margin-right: 4px;
165
+ font-size: 10px;
166
+ padding: 2px 4px;
167
+ line-height: 12px;
168
+ font-weight: 500;
169
+ }
170
+ .crane-margin-right-0 {
171
+ margin-right: 0;
172
+ }
173
+ .crane-margin-left-4 {
174
+ margin-left: 4px;
175
+ }
176
+ .crane-margin-right-4 {
177
+ margin-right: 4px;
178
+ }
179
+ .crane-margin-left-12 {
180
+ margin-left: 12px;
181
+ }
182
+ .crane-disabled-color {
183
+ color: #C9CDD4;
184
+ }
185
+ .crane-week-color {
186
+ color: #86909C;
187
+ }
188
+ .crane-text-overflow {
189
+ flex: 1;
190
+ overflow: hidden;
191
+ text-overflow: ellipsis
192
+ }
193
+
194
+ </style>