widget.qw 0.0.2 → 1.0.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/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.0",
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 './widget/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>
@@ -27,6 +27,7 @@ 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 CascaderPicker from './CascaderPicker.vue'
30
31
 
31
32
  const components = [
32
33
  {
@@ -136,6 +137,10 @@ const components = [
136
137
  {
137
138
  name: 'WidgetQwTreePicker',
138
139
  widget: TreePicker
140
+ },
141
+ {
142
+ name: 'WidgetQwCascaderPicker',
143
+ widget: CascaderPicker
139
144
  }
140
145
  ];
141
146
  // 导出组件
@@ -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>
@@ -149,6 +149,11 @@ const routes: Array<RouteRecordRaw> = [
149
149
  path: "/treepicker",
150
150
  name: "treepicker",
151
151
  component: () => import("../views/treepicker/index.vue"),
152
+ },
153
+ {
154
+ path: "/cascaderpicker",
155
+ name: "cascaderpicker",
156
+ component: () => import("../views/cascaderpicker/index.vue"),
152
157
  }
153
158
  ];
154
159
 
@@ -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>
@@ -32,6 +32,7 @@
32
32
  <van-button class="btn" type="primary" @click="router.push('/switch')">开关</van-button>
33
33
  <van-button class="btn" type="primary" @click="router.push('/sheet')">表格</van-button>
34
34
  <van-button class="btn" type="primary" @click="router.push('/treepicker')">树选择器</van-button>
35
+ <van-button class="btn" type="primary" @click="router.push('/cascaderpicker')">级联选择器</van-button>
35
36
  </div>
36
37
  </template>
37
38
 
package/vite.config.ts CHANGED
@@ -40,8 +40,8 @@ export default {
40
40
  VUE_APP_NEED_LOGIN_CODE: 401,
41
41
  //注意:发布时 VUE_APP_IS_DEBUG必须配置为false
42
42
  // 本地开发时 VUE_APP_IS_DEBUG必须配置为true
43
- VUE_APP_IS_DEBUG: false,
44
- // VUE_APP_IS_DEBUG: true,
43
+ // VUE_APP_IS_DEBUG: false,
44
+ VUE_APP_IS_DEBUG: true,
45
45
  VUE_APP_DEBUG_TOKEN: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpblR5cGUiOiJsb2dpbiIsImxvZ2luSWQiOiIwMTQzOTEiLCJyblN0ciI6ImZ0ejhnaFpLMWR4ejhnOTFDZkJUMXQxVWNJajF3R3c1In0.tgI4iGOJ8OrjoqWIqC2pjH7J52l6H7wyuUwJATKIVns'
46
46
  },
47
47
  },