widget.qw 0.0.2 → 1.0.1

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "widget.qw",
3
3
  "private": false,
4
- "version": "0.0.2",
4
+ "version": "1.0.1",
5
5
  "description": "marqstree Vue3组件库",
6
6
  "main": "build/widget.qw.es.js",
7
7
  "keywords": [
package/src/api/index.js CHANGED
@@ -123,4 +123,20 @@ export const auth_get_user_info = (data) => {
123
123
  method: "POST",
124
124
  data: data,
125
125
  })
126
+ }
127
+
128
+ export const part_children = (data) => {
129
+ return request_json({
130
+ url: vm.urlCallback() + "/ac-api/part/children",
131
+ method: "POST",
132
+ data: data,
133
+ })
134
+ }
135
+
136
+ export const part_detail = (data) => {
137
+ return request_json({
138
+ url: vm.urlCallback() + "/ac-api/part/detail",
139
+ method: "POST",
140
+ data: data,
141
+ })
126
142
  }
@@ -0,0 +1,107 @@
1
+ <template>
2
+ <van-field v-if="!isGone" :label="props.label" :required="isRequired" :rules="props.rules" :disabled="isDisabled"
3
+ :placeholder="props.placeholder" :is-link="isReadonly ? false : true" @click="data.isShowPop = !isReadonly"
4
+ label-class="label">
5
+ <template #input>
6
+ <van-tag class="selected-node" v-if="modelValue" :closeable="!isDisabled" type="primary" size="medium"
7
+ @close.stop="onDelete">
8
+ {{ data.selectedNode.formatLabel }}
9
+ </van-tag>
10
+ </template>
11
+ </van-field>
12
+
13
+ <CascaderPop v-model:show="data.isShowPop"
14
+ v-model="modelValue"
15
+ :placeholder="props.placeholder"
16
+ @select="onSelect" :queryNodes="props.queryNodes" :queryNode="props.queryNode" />
17
+
18
+ </template>
19
+ <script setup>
20
+ import { reactive, onMounted, watch } from "vue";
21
+ import util from "@/util"
22
+ import { useVModel } from '@vueuse/core'
23
+ import CascaderPop from './CascaderPop.vue'
24
+
25
+ const props = defineProps({
26
+ modelValue: {
27
+ type: String,
28
+ default: '',
29
+ },
30
+ label: {
31
+ type: String,
32
+ default: "",
33
+ },
34
+ placeholder: {
35
+ type: String,
36
+ default: "",
37
+ },
38
+ required: {
39
+ type: Boolean,
40
+ default: false,
41
+ },
42
+ readonly: {
43
+ type: Boolean,
44
+ default: false,
45
+ },
46
+ rules: {
47
+ type: Array,
48
+ default: [],
49
+ },
50
+ auth: {
51
+ type: String,
52
+ default: "",
53
+ },
54
+ queryNodes:{
55
+ type:Function,
56
+ default:()=>{}
57
+ },
58
+ queryNode:{
59
+ type:Function,
60
+ default:()=>{}
61
+ }
62
+ });
63
+
64
+ // 弹出层
65
+ const data = reactive({
66
+ isShowPop: false,
67
+ selectedNode: {}
68
+ })
69
+
70
+ // 定义emit
71
+ const emit = defineEmits(["update:modelValue", "select"]);
72
+ const modelValue = useVModel(props, 'modelValue', emit)
73
+ const { isRequired, isReadonly, isGone, isDisabled } = util.props2auth(props);
74
+
75
+ //首页加载
76
+ onMounted(() => {
77
+ })
78
+
79
+ const onSelect = (node) => {
80
+ data.selectedNode = node
81
+ }
82
+
83
+ const onDelete=()=>{
84
+ data.selectedNode={}
85
+ modelValue.value = null
86
+ }
87
+
88
+ watch(() => modelValue.value, async (n, o) => {
89
+ if (!n) {
90
+ data.selectedNode = {}
91
+ return
92
+ }
93
+
94
+ props.queryNode((node)=>{
95
+ data.selectedNode = node
96
+ emit('select', data.selectedNode)
97
+ })
98
+ }, {
99
+ immediate: true
100
+ })
101
+ </script>
102
+
103
+ <style scoped>
104
+ .user{
105
+ padding:4px 8px;
106
+ }
107
+ </style>
@@ -0,0 +1,313 @@
1
+ <template>
2
+ <van-popup v-model:show="show" round position="bottom" class="cascader-pop">
3
+ <div class="cascader-selector">
4
+ <div class="cascader-header">
5
+ <div class="header-left">
6
+ <van-button @click="onBack" icon="arrow-left" size="small" />
7
+ </div>
8
+
9
+ <!-- 移除header-center包装,直接使用current-path -->
10
+ <div class="current-path">{{ pathsLabel }}</div>
11
+
12
+ <div class="header-right">
13
+ <van-button size="small" @click="onCancel" class="cancel-btn">取消</van-button>
14
+
15
+ <van-button type="primary" size="small" @click="onSubmit" class="confirm-btn">确定</van-button>
16
+ </div>
17
+ </div>
18
+
19
+ <!-- 搜索框 -->
20
+ <van-search v-model="data.keyword" :placeholder="props.placeholder" @clear="onClear" @search="onSearch"
21
+ @cancel="onClear" show-action>
22
+ <template #action>
23
+ <div @click="onSearch">搜索</div>
24
+ </template>
25
+ </van-search>
26
+
27
+ <div class="scroll-container">
28
+ <!-- 遍历子节点列表 -->
29
+ <van-cell-group>
30
+ <van-cell v-for="(item, i) in optionNodes" :key="i" >
31
+ <template #title>
32
+ <div class="cell-content">
33
+ <div class="circle-selector" @click.stop="onToggle(item)">
34
+ <van-icon v-if="data.selectedNodeId === item.id" name="success" size="20" color="#409eff"/>
35
+ <van-icon v-if="data.selectedNodeId != item.id" name="circle" size="20" color="#e0e0e0"/>
36
+ </div>
37
+ <span class="name">{{ item.label }}</span>
38
+ </div>
39
+ </template>
40
+ <template #right-icon v-if="item.hasChildren" >
41
+ <van-icon name="arrow" @click.stop="onNextLevel(item)"/>
42
+ </template>
43
+ </van-cell>
44
+ </van-cell-group>
45
+ </div>
46
+ </div>
47
+ </van-popup>
48
+ </template>
49
+
50
+ <script setup>
51
+ import { ref, onMounted, watch, reactive, computed } from "vue";
52
+ import util from "@/util"
53
+ import { useVModel } from '@vueuse/core'
54
+
55
+ const props = defineProps({
56
+ modelValue: {
57
+ type: String,
58
+ default: ''
59
+ },
60
+ show: {
61
+ type: Boolean,
62
+ default: false,
63
+ },
64
+ placeholder: {
65
+ type: String,
66
+ default: ''
67
+ },
68
+ queryNodes: {
69
+ type: Function,
70
+ default: () => { }
71
+ },
72
+ queryNode: {
73
+ type: Function,
74
+ default: () => { }
75
+ }
76
+ });
77
+ // 定义emit
78
+ const emit = defineEmits(["update:show", "select"]);
79
+ // 弹出?
80
+ const show = useVModel(props, 'show', emit)
81
+ const modelValue = useVModel(props, 'modelValue', emit)
82
+ const data = reactive({
83
+ // 所有节点
84
+ totalNodes: [],
85
+ // 搜索关键字
86
+ keyword: "",
87
+ //关键字搜索到的节点列表
88
+ searchNodes: [],
89
+ //当前节点
90
+ currentNode: null,
91
+ //勾选的节点id
92
+ selectedNodeId: ''
93
+ })
94
+
95
+ // 待选节点列表
96
+ const optionNodes = computed(() => {
97
+ if (data.keyword)
98
+ return data.searchNodes
99
+ else if (data.currentNode)
100
+ return data.currentNode?.children ? data.currentNode.children : []
101
+ else
102
+ return data.totalNodes ? data.totalNodes : []
103
+ })
104
+
105
+ //选中节点的路径:父/子/孙
106
+ const pathsLabel = computed(() => {
107
+ if (!data.currentNode || !data.currentNode.id)
108
+ return ''
109
+
110
+ let paths = util.findPaths(data.currentNode.id, data.totalNodes)
111
+ console.log("paths", paths);
112
+ let res = paths.map(item => item.label).join('/')
113
+ return res
114
+ })
115
+
116
+ // 查询树状节点列表
117
+ const queryTotalNodes = async () => {
118
+ await props.queryNodes({keyword:data.keyword,parentId:''}, (nodes) => {
119
+ data.totalNodes = nodes
120
+ })
121
+ }
122
+
123
+ // 关键字查询
124
+ const onSearch = async () => {
125
+ await util.queryNodes({keyword:data.keyword,parentId:''}, (nodes) => {
126
+ data.searchNodes = nodes
127
+ })
128
+ }
129
+
130
+ // 返回上一级
131
+ const onBack = () => {
132
+ if (!data.currentNode || !data.currentNode.id)
133
+ return
134
+
135
+ data.keyword = ""
136
+ data.currentNode = util.findParent(data.currentNode.id, data.totalNodes)
137
+ }
138
+
139
+ const onClear=()=>{
140
+ data.keyword = ''
141
+ data.searchNodes = []
142
+ }
143
+
144
+ // 清除掉选中节点
145
+ const reset = () => {
146
+ // 当前节点
147
+ data.currentNode = null
148
+ // 搜索关键字
149
+ data.keyword = ""
150
+ }
151
+
152
+ // 确认按钮
153
+ const onSubmit = () => {
154
+ if (!data.selectedNodeId) {
155
+ util.warnToast("请选择")
156
+ return
157
+ }
158
+
159
+ let selectedNode = util.findNode(data.selectedNodeId, data.totalNodes)
160
+ modelValue.value = selectedNode.id
161
+ emit('select', selectedNode)
162
+ show.value = false
163
+
164
+ reset()
165
+ }
166
+
167
+ // 取消按钮
168
+ const onCancel = () => {
169
+ show.value = false
170
+ reset()
171
+ }
172
+
173
+ const onToggle = (node) => {
174
+ if(data.selectedNodeId == node.id)
175
+ data.selectedNodeId = ''
176
+ else
177
+ data.selectedNodeId = node.id
178
+ }
179
+
180
+ const onNextLevel = (node) => {
181
+ props.queryNodes({ keyword: data.keyword, parentId: node.id }, (nodes) => {
182
+ node.children = nodes
183
+ data.currentNode = node
184
+ })
185
+ }
186
+
187
+ //首页加载
188
+ onMounted(async () => {
189
+ await queryTotalNodes()
190
+ })
191
+
192
+ watch(() => props.modelValue, () => {
193
+ // 初始化当前节点
194
+ data.currentNode = util.findParent(props.modelValue, data.totalNodes)
195
+ }, { immediate: true })
196
+ </script>
197
+
198
+ <style scoped>
199
+ .cascader-pop {
200
+ width: 100%;
201
+ overflow: hidden;
202
+ }
203
+
204
+ .cascader-selector {
205
+ padding: 10px;
206
+ }
207
+
208
+ .search-buttons {
209
+ display: flex;
210
+ gap: 10px;
211
+ }
212
+
213
+ .clear-btn {
214
+ background-color: #f2f3f5;
215
+ border: none;
216
+ border-radius: 4px;
217
+ padding: 4px 8px;
218
+ font-size: 14px;
219
+ }
220
+
221
+ .confirm-btn {
222
+ background-color: #1989fa;
223
+ color: white;
224
+ border: none;
225
+ border-radius: 4px;
226
+ padding: 4px 8px;
227
+ font-size: 14px;
228
+ }
229
+
230
+ .no-results {
231
+ text-align: center;
232
+ padding: 20px;
233
+ color: #969799;
234
+ }
235
+
236
+ .cascader-header {
237
+ padding: 10px;
238
+ border-bottom: 1px solid #ebedf0;
239
+ display: flex;
240
+ justify-content: space-between;
241
+ align-items: center;
242
+ }
243
+
244
+ .current-path {
245
+ flex: 1;
246
+ font-size: 14px;
247
+ color: #323233;
248
+ white-space: nowrap;
249
+ overflow: hidden;
250
+ text-overflow: ellipsis;
251
+ }
252
+
253
+ .header-right {
254
+ display: flex;
255
+ gap: 8px;
256
+ margin-left: auto;
257
+ }
258
+
259
+ .cancel-btn {
260
+ color: #969799;
261
+ margin-right: 5px;
262
+ }
263
+
264
+ .scroll-container {
265
+ height: calc(80vh - 150px);
266
+ overflow-y: auto;
267
+ }
268
+
269
+
270
+ .cell-content {
271
+ display: flex;
272
+ align-items: center;
273
+ gap: 12px;
274
+ }
275
+
276
+ .circle-selector {
277
+ width: 20px;
278
+ height: 20px;
279
+ border-radius: 50%;
280
+ display: flex;
281
+ align-items: center;
282
+ justify-content: center;
283
+ transition: all 0.2s;
284
+ flex-shrink: 0;
285
+ }
286
+
287
+ .circle-selector.selected {
288
+ background-color: #1989fa;
289
+ border-color: #1989fa;
290
+ }
291
+
292
+ .circle-selector.selected .van-icon {
293
+ color: white;
294
+ font-size: 14px;
295
+ }
296
+
297
+ .van-cell__right-icon {
298
+ color: #969799;
299
+ font-size: 16px;
300
+ margin-left: 8px;
301
+ }
302
+
303
+ .name {
304
+ flex: 1;
305
+ overflow: hidden;
306
+ text-overflow: ellipsis;
307
+ white-space: nowrap;
308
+ }
309
+
310
+ .cancel-btn {
311
+ color: #969799;
312
+ }
313
+ </style>
@@ -27,6 +27,8 @@ import ProjectDropdown from "./ProjectDropdown.vue";
27
27
  import Switch from './Switch.vue'
28
28
  import Sheet from './Sheet.vue'
29
29
  import TreePicker from './TreePicker.vue'
30
+ import CascaderPop from './CascaderPop.vue'
31
+ import CascaderPicker from './CascaderPicker.vue'
30
32
 
31
33
  const components = [
32
34
  {
@@ -136,6 +138,14 @@ const components = [
136
138
  {
137
139
  name: 'WidgetQwTreePicker',
138
140
  widget: TreePicker
141
+ },
142
+ {
143
+ name: 'WidgetQwCascaderPop',
144
+ widget: CascaderPop
145
+ },
146
+ {
147
+ name: 'WidgetQwCascaderPicker',
148
+ widget: CascaderPicker
139
149
  }
140
150
  ];
141
151
  // 导出组件
@@ -149,6 +149,16 @@ const routes: Array<RouteRecordRaw> = [
149
149
  path: "/treepicker",
150
150
  name: "treepicker",
151
151
  component: () => import("../views/treepicker/index.vue"),
152
+ },
153
+ {
154
+ path: "/cascaderpop",
155
+ name: "cascaderpop",
156
+ component: () => import("../views/cascaderpop/index.vue"),
157
+ },
158
+ {
159
+ path: "/cascaderpicker",
160
+ name: "cascaderpicker",
161
+ component: () => import("../views/cascaderpicker/index.vue"),
152
162
  }
153
163
  ];
154
164
 
@@ -41,6 +41,40 @@ export const findNode = (id, nodes) => {
41
41
  return res[res.length - 1]
42
42
  }
43
43
 
44
+ // 查找节点路径列表
45
+ // 返回:[父,子,孙]
46
+ export const findPaths =(targetId, nodes, path = [])=> {
47
+ if (!nodes || nodes.length < 1 || !targetId)
48
+ return []
49
+
50
+ for (const node of nodes) {
51
+ if (node.id === targetId) {
52
+ return [...path, node];
53
+ }
54
+ if (node.children?.length) {
55
+ const founds = findPaths(targetId, node.children, [...path, node]);
56
+ if (founds && founds.length > 0) return founds;
57
+ }
58
+ }
59
+
60
+ return []
61
+ }
62
+
63
+ //查找父节点
64
+ export const findParent = (id, nodes) => {
65
+ if (!nodes || !id) {
66
+ return null
67
+ }
68
+
69
+ let paths = findPaths(id, nodes)
70
+
71
+ if (!paths || paths.length < 2) {
72
+ return null
73
+ }
74
+
75
+ return paths[paths.length-2]
76
+ }
77
+
44
78
  // id列表转为节点对象列表
45
79
  export const ids2nodes=(ids,totalNodes)=>{
46
80
  if(!totalNodes || totalNodes.length<1 || !ids || ids.length<1)
@@ -0,0 +1,86 @@
1
+ <template>
2
+ <div class="page">
3
+ <widget-qw-cascader-picker v-model="data.id" label="零件" placeholder="请选择零件"
4
+ :queryNodes="queryNodes"
5
+ :queryNode="queryNode"
6
+ :auth="data.auth" />
7
+ </div>
8
+ </template>
9
+
10
+ <script setup>
11
+ import { onMounted, reactive, watch } from "vue";
12
+ import util from '@/util'
13
+
14
+ const data = reactive({
15
+ auth: 'require',
16
+ id: '',
17
+ projectId:'GnsMl3mO'
18
+ })
19
+
20
+ const queryNodes = async (params,setNodes) => {
21
+ if (!data.projectId)
22
+ return
23
+
24
+ let p = {
25
+ projectId: data.projectId,
26
+ hasChildren: false
27
+ }
28
+
29
+ if(params.keyword)
30
+ p.keyword = params.keyword
31
+
32
+ if(params.parentId)
33
+ p.parentId = params.parentId
34
+
35
+ let res = await util.part_children(p)
36
+ if (res.code != 200)
37
+ return
38
+
39
+ let formatNodes = res.data.map(item=>{
40
+ return {
41
+ id:item.id,
42
+ label: item.formatName,
43
+ formatLabel: item.path,
44
+ hasChildren: item.hasChildren,
45
+ data: item
46
+ }
47
+ })
48
+ setNodes(formatNodes)
49
+ }
50
+
51
+ const queryNode=async(setNode)=>{
52
+ let params={
53
+ id:data.id
54
+ }
55
+ let res = await util.part_detail(params)
56
+ if(res.code!=200)
57
+ return
58
+
59
+ let formatNode = {
60
+ id:res.data.id,
61
+ label: res.data.formatName,
62
+ formatLabel: item.path,
63
+ hasChildren: res.data.hasChildren,
64
+ data:res.data
65
+ }
66
+ setNode(formatNode)
67
+ }
68
+
69
+ watch(() => data.id, (newVal, oldVal) => {
70
+ console.log(newVal, oldVal)
71
+ })
72
+ </script>
73
+
74
+ <style lang="scss" scoped>
75
+ .page {
76
+ height: 100vh;
77
+ background: #fff;
78
+ text-align: center;
79
+ background-size: cover;
80
+ display: flex;
81
+ flex-direction: column;
82
+ justify-content: flex-start;
83
+ padding: 80px 15px 0 15px;
84
+ box-sizing: border-box;
85
+ }
86
+ </style>
@@ -0,0 +1,91 @@
1
+ <template>
2
+ <div class="page">
3
+ <van-button @click="data.isShow=true">弹出</van-button>
4
+ <widget-qw-cascader-pop v-model="data.id" v-model:show="data.isShow"
5
+ label="零件" placeholder="请选择零件"
6
+ :queryNodes="queryNodes" :queryNode="queryNode" @select="onSelect" />
7
+ </div>
8
+ </template>
9
+
10
+ <script setup>
11
+ import { onMounted, reactive, watch } from "vue";
12
+ import util from '@/util'
13
+
14
+ const data = reactive({
15
+ auth: 'require',
16
+ id: '',
17
+ isShow: false,
18
+ projectId: 'GnsMl3mO'
19
+ })
20
+
21
+ const queryNodes = async (params, setNodes) => {
22
+ if (!data.projectId)
23
+ return
24
+
25
+ let p = {
26
+ projectId: data.projectId,
27
+ hasChildren: false
28
+ }
29
+
30
+ if (params.keyword)
31
+ p.keyword = params.keyword
32
+
33
+ if (params.parentId)
34
+ p.parentId = params.parentId
35
+
36
+ let res = await util.part_children(p)
37
+ if (res.code != 200)
38
+ return
39
+
40
+ let formatNodes = res.data.map(item => {
41
+ return {
42
+ id: item.id,
43
+ label: item.formatName,
44
+ formatLabel: item.path,
45
+ hasChildren: item.hasChildren,
46
+ data: item
47
+ }
48
+ })
49
+ setNodes(formatNodes)
50
+ }
51
+
52
+ const queryNode = async (setNode) => {
53
+ let params = {
54
+ id: data.id
55
+ }
56
+ let res = await util.part_detail(params)
57
+ if (res.code != 200)
58
+ return
59
+
60
+ let formatNode = {
61
+ id: res.data.id,
62
+ label: res.data.formatName,
63
+ formatLabel: item.path,
64
+ hasChildren: res.data.hasChildren,
65
+ data: res.data
66
+ }
67
+ setNode(formatNode)
68
+ }
69
+
70
+ const onSelect=(node)=>{
71
+ console.log('onSelect',node)
72
+ }
73
+
74
+ watch(() => data.id, (newVal, oldVal) => {
75
+ console.log(newVal, oldVal)
76
+ })
77
+ </script>
78
+
79
+ <style lang="scss" scoped>
80
+ .page {
81
+ height: 100vh;
82
+ background: #fff;
83
+ text-align: center;
84
+ background-size: cover;
85
+ display: flex;
86
+ flex-direction: column;
87
+ justify-content: flex-start;
88
+ padding: 80px 15px 0 15px;
89
+ box-sizing: border-box;
90
+ }
91
+ </style>