npmapps 1.0.20 → 1.0.21

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.
@@ -0,0 +1,108 @@
1
+
2
+ import { defineComponent, ref, h } from 'vue'
3
+ import styles from './index.module.scss'
4
+ import { Expand, DeleteFilled } from '@element-plus/icons-vue'
5
+ import draggable from 'vuedraggable'
6
+ import _ from 'lodash'
7
+ export default defineComponent({
8
+ // 折叠展开组件 - 列表展示
9
+ name: 'CollapseExpand',
10
+ props: {
11
+ collapseList: {
12
+ type: Array,
13
+ default: () => [],
14
+ },
15
+ draggableDisabled: {
16
+ type: Boolean,
17
+ default: false,
18
+ },
19
+ },
20
+ emits: ['update:collapseList'],
21
+ setup(props, { emit, attrs, slots }) {
22
+
23
+ const newCollapseList = ref(_.cloneDeep(props.collapseList))
24
+ const dragEnd = (e) => {
25
+ console.log(e)
26
+ attrs.dragEnd && attrs.dragEnd(e, newCollapseList.value)
27
+ emit('update:collapseList', newCollapseList.value)
28
+ }
29
+ const handleDelete = (item) => {
30
+ newCollapseList.value = newCollapseList.value.filter((i) => i.id !== item.id)
31
+ emit('update:collapseList', newCollapseList.value)
32
+ }
33
+ return () => (
34
+ <div class={styles.collapseExpand}>
35
+ <el-collapse
36
+ {...attrs}
37
+ >
38
+ <draggable
39
+ v-model={newCollapseList.value}
40
+ item-key="id"
41
+ class="list"
42
+ ghost-class="ghost"
43
+ disabled={props.draggableDisabled}
44
+ onEnd={dragEnd}
45
+ animation={attrs.animation || 100}
46
+ handle=".drag-icon"
47
+ >
48
+ {{
49
+ item: (({ element: item, index }) => (
50
+ <el-collapse-item class={[styles.item,
51
+ index === 0 ?
52
+ styles.firstItem : '',
53
+ index === props.collapseList.length - 1 ?
54
+ styles.lastItem : '']}
55
+ key={item.id}
56
+ name={item.id}>
57
+ {{
58
+ // 自定义标题栏
59
+ title: () => (
60
+ attrs.title ? attrs.title(item) :
61
+ <div class={[styles.header]}>
62
+ <div class={[styles.headerLeft]}>
63
+ <div>
64
+ <el-icon style={{ cursor: 'grab' }} class={'drag-icon'} size={20}><Expand /></el-icon>
65
+ </div>
66
+ <div class={[styles.headerContent]}>
67
+ <div class={[styles.title]}>{item.title}</div>
68
+ {/* 目标描述 */}
69
+ <div class={[styles.goal]}>
70
+ {item.goal}
71
+ </div>
72
+ </div>
73
+ </div>
74
+ {/* 阻止冒泡避免触发折叠 */}
75
+ <el-popconfirm
76
+ title="确认删除吗?"
77
+ confirm-button-text="确定"
78
+ cancel-button-text="取消"
79
+ onConfirm={(e) => {
80
+ handleDelete(item)
81
+ e.stopPropagation()
82
+ }}
83
+ >
84
+ {{
85
+ reference: () => (
86
+ <el-icon onClick={(e) => e.stopPropagation()} style={{ marginRight: '8px' }}><DeleteFilled /></el-icon>
87
+ )
88
+ }}
89
+
90
+ </el-popconfirm>
91
+ </div>
92
+ ),
93
+ // 展开的内容区域
94
+ default: () => (
95
+ <div>
96
+ {item.item ? item.item(item) : null}
97
+ </div>
98
+ )
99
+ }}
100
+ </el-collapse-item>
101
+ ))
102
+ }}
103
+ </draggable>
104
+ </el-collapse>
105
+ </div>
106
+ )
107
+ },
108
+ })
@@ -0,0 +1,97 @@
1
+ .collapseExpand {
2
+ :global(.el-collapse .el-collapse-item__header) {
3
+ padding-left: 10px;
4
+ }
5
+ .header {
6
+ display: flex;
7
+ justify-content: space-between;
8
+ width: 100%;
9
+ align-items: center;
10
+
11
+
12
+ .headerLeft {
13
+ display: flex;
14
+ align-items: center;
15
+
16
+ .headerContent {
17
+ margin-left: 10px;
18
+ display: flex;
19
+ flex-direction: column;
20
+ align-items: flex-start;
21
+
22
+ .title,
23
+ .goal {
24
+ display: flex;
25
+ height: 16px;
26
+ font-size: 14px;
27
+ font-weight: 500;
28
+ line-height: 16px;
29
+ }
30
+
31
+ .goal {
32
+ margin-top: 5px;
33
+ color: #999;
34
+ font-size: 12px;
35
+ }
36
+ }
37
+ }
38
+ }
39
+
40
+ .item {
41
+ position: relative;
42
+ padding-left: 12px;
43
+ }
44
+
45
+
46
+ /* 画竖线 */
47
+ .item::before {
48
+ content: '';
49
+ position: absolute;
50
+ left: 0px;
51
+ top: 0;
52
+ bottom: 0;
53
+ width: 2px;
54
+ background-color: #FC6506;
55
+ }
56
+
57
+ .firstItem::before {
58
+ content: '';
59
+ position: absolute;
60
+ left: 0px;
61
+ top: 24px;
62
+ bottom: 0;
63
+ width: 2px;
64
+ background-color: #FC6506;
65
+ }
66
+
67
+ .lastItem::before {
68
+ content: '';
69
+ position: absolute;
70
+ left: 0px;
71
+ top: 0;
72
+ bottom: 24px;
73
+ width: 2px;
74
+ height: 24px;
75
+ background-color: #FC6506;
76
+
77
+ }
78
+
79
+ /* 每个 item 的标题前加圆点 */
80
+ :global(.el-collapse-item__header) {
81
+ position: relative;
82
+ padding-left: 24px;
83
+ }
84
+
85
+ :global(.el-collapse-item__header)::before {
86
+ content: '';
87
+ position: absolute;
88
+ left: -15px;
89
+ top: 50%;
90
+ transform: translateY(-50%);
91
+ width: 8px;
92
+ height: 8px;
93
+ background-color: #FC6506;
94
+ border-radius: 50%;
95
+ z-index: 1;
96
+ }
97
+ }
@@ -0,0 +1,158 @@
1
+
2
+ import { defineComponent, ref, watch } from 'vue'
3
+ import styles from './index.module.scss'
4
+ import CollapseExpand from '../collapseExpand/index.jsx'
5
+ import ScriptTable from '../scriptTable/index.jsx'
6
+
7
+ export default defineComponent({
8
+ // 对话环节
9
+ name: 'DialogueSegment',
10
+
11
+ setup() {
12
+ const collapseList = ref([
13
+ {
14
+ id: '1',
15
+ title: '环节1:需求表达',
16
+ goal: '目标:顾客直接向导购表明自己想购买一台高品质跑步机',
17
+ scripts: {
18
+ qaScripts: [
19
+ {
20
+ order: 1,
21
+ problemDesc: '1',
22
+ standardScript: '',
23
+ keyword: '',
24
+ id: '1',
25
+ },
26
+ {
27
+ order: 2,
28
+ problemDesc: '2',
29
+ standardScript: '',
30
+ keyword: '',
31
+ id: '2',
32
+ }
33
+ ],
34
+ proactiveScript: [
35
+ {
36
+ order: 1,
37
+ problemDesc: '',
38
+ standardScript: '',
39
+ keyword: '',
40
+ id: '1',
41
+ }
42
+ ]
43
+ }
44
+
45
+ },
46
+ {
47
+ id: '2',
48
+ title: '环节2:产品介绍',
49
+ goal: '目标:导购根据顾客需求,详细介绍不同款式跑步机的特点、性能和价格',
50
+ scripts: {
51
+ qaScripts: [
52
+ {
53
+ order: 1,
54
+ problemDesc: '',
55
+ standardScript: '',
56
+ keyword: '',
57
+ id: '1',
58
+ }
59
+ ],
60
+ proactiveScript: [
61
+ {
62
+ order: 1,
63
+ problemDesc: '',
64
+ standardScript: '',
65
+ keyword: '',
66
+ id: '1',
67
+ }
68
+ ]
69
+ }
70
+ },
71
+ {
72
+ id: '3',
73
+ title: '环节3:价格确认',
74
+ goal: '目标:顾客确认购买价格,确认无误后,引导顾客完成支付',
75
+ scripts: {
76
+ qaScripts: [
77
+ {
78
+ order: 1,
79
+ problemDesc: '',
80
+ standardScript: '',
81
+ keyword: '',
82
+ id: '1',
83
+ }
84
+ ],
85
+ proactiveScript: [
86
+ {
87
+ order: 1,
88
+ problemDesc: '',
89
+ standardScript: '',
90
+ keyword: '',
91
+ id: '1',
92
+ }
93
+ ]
94
+ }
95
+ },
96
+ ])
97
+ const activeNames = ref(['1'])
98
+ const addScript = (item,props) => {
99
+ console.log(item,props,'添加话术');
100
+ item.scripts[props.activeName].push({
101
+ order: item.scripts[props.activeName].length + 1,
102
+ problemDesc: '',
103
+ standardScript: '',
104
+ keyword: '',
105
+ id: Date.now().toString(),
106
+ })
107
+ }
108
+ collapseList.value.forEach((item) => {
109
+ item.item = () => (
110
+ <div class={[styles.scriptTableContainer]}>
111
+ <ScriptTable
112
+ isDraggable={true}
113
+ scripts={item.scripts}
114
+ footer={(props) => (
115
+ <div>
116
+ <el-button type="text" size="small" onClick={() => {
117
+ addScript(item,props)
118
+ }}>
119
+ 十添加话术
120
+ </el-button>
121
+ <el-button type="text" size="small">
122
+ Ai生成话术
123
+ </el-button>
124
+ </div>
125
+ )}
126
+ ></ScriptTable>
127
+
128
+ </div>
129
+ )
130
+ })
131
+
132
+ watch(() => collapseList.value, (newVal, oldVal) => {
133
+ console.log(newVal, oldVal)
134
+ })
135
+ return () => {
136
+ return <div class={[styles.dialogueSegment]}>
137
+ <div class={[styles.card]}>
138
+ <div class={[styles.cardHeader]}>
139
+ <div class={[styles.title]}>
140
+ 对话环节
141
+ </div>
142
+ <div class={[styles.operation]}>
143
+ <el-button type="text" size="small">
144
+ 添加环节
145
+ </el-button>
146
+ <el-button type="text" size="small">
147
+ 重新生成环节
148
+ </el-button>
149
+ </div>
150
+ </div>
151
+ <div class={[styles.cardBody]}>
152
+ <CollapseExpand v-model:collapseList={collapseList.value} v-model={activeNames.value} />
153
+ </div>
154
+ </div>
155
+ </div>
156
+ }
157
+ },
158
+ })
@@ -0,0 +1,28 @@
1
+ .dialogueSegment {
2
+ width: 800px;
3
+ .card {
4
+ border-radius: 8px;
5
+ border: 1px solid #E5E5E5;
6
+ .cardHeader {
7
+ display: flex;
8
+ justify-content: space-between;
9
+ align-items: center;
10
+ background-color: #F8F8F8;
11
+ padding: 12px 20px;
12
+ }
13
+ .title {
14
+ font-size: 16px;
15
+ font-weight: 500;
16
+ }
17
+ .operation {
18
+ font-size: 14px;
19
+ font-weight: 400;
20
+ }
21
+ .cardBody {
22
+ padding: 20px;
23
+ }
24
+ .scriptTableContainer {
25
+ padding: 10px 10px 0;
26
+ }
27
+ }
28
+ }
@@ -0,0 +1,20 @@
1
+ import { defineComponent, ref } from 'vue'
2
+ import DialogueSegment from './dialogueSegment/index.jsx'
3
+
4
+ export default defineComponent({
5
+ // 智能教练
6
+ name: 'AiCoach',
7
+
8
+ setup() {
9
+ const activeTab = ref('dialogue-segment')
10
+ return () => {
11
+ return <div style={{padding: '30px'}}>
12
+ <el-tabs v-model={activeTab.value} type="border-card">
13
+ <el-tab-pane label="对话环节" name="dialogue-segment">
14
+ <DialogueSegment />
15
+ </el-tab-pane>
16
+ </el-tabs>
17
+ </div>
18
+ }
19
+ },
20
+ })
@@ -0,0 +1,196 @@
1
+ import { defineComponent, ref, computed, watch, onUnmounted, nextTick } from 'vue'
2
+ import styles from './index.module.scss'
3
+ import { Expand, DeleteFilled } from '@element-plus/icons-vue'
4
+ import InputColumn from './inputColumn/index.jsx'
5
+ import Sortable from 'sortablejs'
6
+ export default defineComponent({
7
+ // 脚本表格组件
8
+ name: 'ScriptTable',
9
+ props: {
10
+ scripts: {
11
+ type: Object,
12
+ default: () => { },
13
+ },
14
+ },
15
+ setup(props, { emit, attrs, slots }) {
16
+ console.log(props.scripts);
17
+ const labelEmu = {
18
+ qaScripts: '问答脚本',
19
+ proactiveScript: '主动脚本',
20
+ }
21
+ const type = ref('all')
22
+ const activeName = ref('qaScripts')
23
+
24
+ const tabs = computed(() => {
25
+ const lsit = []
26
+ for (const key in props.scripts) {
27
+ lsit.push({
28
+ label: labelEmu[key],
29
+ value: props.scripts[key],
30
+ key: key,
31
+ })
32
+ }
33
+ return lsit
34
+ })
35
+
36
+ watch(() => props.scripts, (newVal, oldVal) => {
37
+ console.log(newVal, oldVal, 'scripts变化');
38
+
39
+ }, {
40
+ deep: true,
41
+ })
42
+
43
+ const tableRef = ref(null)
44
+ let sortable = null
45
+ watch(() => tableRef.value, async (newVal, oldVal) => {
46
+ if (newVal) {
47
+ await nextTick()
48
+ const tbody = tableRef.value?.$el.querySelector('.el-table__body-wrapper tbody')
49
+ if (!tbody) return
50
+ sortable?.destroy()
51
+ sortable = Sortable.create(tbody, {
52
+ animation: 150,
53
+ handle: '.drag-cell', // 只有手柄能拖
54
+ onEnd: evt => {
55
+ const { oldIndex, newIndex } = evt
56
+ if (oldIndex === undefined || newIndex === undefined) return
57
+ const [removed] = props.scripts[activeName.value].splice(oldIndex, 1)
58
+ props.scripts[activeName.value].splice(newIndex, 0, removed)
59
+ },
60
+ })
61
+ }
62
+ })
63
+ onUnmounted(() => sortable?.destroy())
64
+ return () => (
65
+ <div class={styles.scriptTable}>
66
+ {/* <el-button type="primary" size="small" onClick={() => {
67
+ console.log(props.scripts[activeName.value]);
68
+ }}>测试 </el-button> */}
69
+ <div class={[styles.header]}>
70
+ <div class={[styles.tabs]}>
71
+ {
72
+ tabs.value.map((item) => (
73
+ <div
74
+ class={[styles.tab, { [styles.active]: item.key === activeName.value }]}
75
+ key={item.key}
76
+ onClick={() => activeName.value = item.key}
77
+ >
78
+ {item.label} {item.value.length}
79
+ </div>
80
+ ))
81
+ }
82
+ </div>
83
+ <div class={[styles.right]}>
84
+ <span>学员需练习</span>
85
+ <el-select size="small" v-model={type.value} style={{ width: '60px', marginLeft: '10px', marginRight: '10px' }}>
86
+ {
87
+ {
88
+ default: () => (
89
+ <>
90
+ <el-option label="全部" value="all" />
91
+ <el-option label="部分" value="partial" />
92
+ </>
93
+ ),
94
+ }
95
+ }
96
+ </el-select>
97
+ <span>话术</span>
98
+ </div>
99
+ </div>
100
+ <div>
101
+ <el-table
102
+ ref={tableRef}
103
+ data={props.scripts[activeName.value]}
104
+ style={{ width: '100%' }}
105
+ border
106
+ size="small"
107
+ row-key="id"
108
+ headerCellStyle={{
109
+ background: '#F8F8F8',
110
+ color: '#303133',
111
+ fontWeight: 'bold',
112
+ }}
113
+ >
114
+ {{
115
+ default: (scope) => (
116
+ <>
117
+ {
118
+ attrs.isDraggable &&
119
+
120
+ <el-table-column prop="order" label="排序" width="50px">
121
+ {{
122
+ default: (scope) => (
123
+ <div class="drag-cell" style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
124
+ <el-icon style={{ cursor: 'grab' }} class={'drag-icon'} size={20}><Expand /></el-icon>
125
+ </div>
126
+ )
127
+ }}
128
+ </el-table-column>
129
+ }
130
+ <el-table-column prop="problemDesc" label="问题描述">
131
+ {{
132
+ default: (scope) => (
133
+ <InputColumn scope={scope} tableColumn={{
134
+ prop: 'problemDesc',
135
+ label: '问题描述',
136
+ }} />
137
+ )
138
+ }}
139
+ </el-table-column>
140
+ <el-table-column prop="standardScript" label="标准话术">
141
+ {{
142
+ default: (scope) => (
143
+ <InputColumn scope={scope} tableColumn={{
144
+ prop: 'standardScript',
145
+ label: '标准话术',
146
+ }} />
147
+ )
148
+ }}
149
+ </el-table-column>
150
+ <el-table-column prop="keyword" label="关键词">
151
+ {{
152
+ default: (scope) => (
153
+ <InputColumn scope={scope} tableColumn={{
154
+ prop: 'keyword',
155
+ label: '关键词',
156
+ placeholder: '请输入请输入请输入关键词,多个关键词用逗号分隔',
157
+ tag: true,
158
+ split: ',',
159
+ noAiGenerate: true,
160
+ }} />
161
+ )
162
+ }}
163
+ </el-table-column>
164
+ <el-table-column prop="aiScript" label="操作" width="50px">
165
+ {{
166
+ default: (scope) => (
167
+ <el-icon style={{ cursor: 'pointer' }} ><DeleteFilled /></el-icon>
168
+ )
169
+ }}
170
+ </el-table-column>
171
+ {/* <InputColumn tableColumn={{
172
+ prop: 'standardScript',
173
+ label: '标准话术',
174
+ }} />
175
+ <InputColumn tableColumn={{
176
+ prop: 'keyword',
177
+ label: '关键词',
178
+ }} /> */}
179
+ </>
180
+ ),
181
+ }}
182
+ </el-table>
183
+ </div>
184
+ <div>
185
+ {
186
+ attrs.footer && attrs.footer({
187
+ labelEmu,
188
+ activeName: activeName.value,
189
+ type: type.value,
190
+ })
191
+ }
192
+ </div>
193
+ </div>
194
+ )
195
+ }
196
+ })
@@ -0,0 +1,41 @@
1
+ .scriptTable{
2
+ .header{
3
+ display: flex;
4
+ justify-content: space-between;
5
+ align-items: center;
6
+ background-color: #F8F8F8;
7
+ border: 1px solid #EBEEF5;
8
+ border-bottom: none;
9
+ padding: 10px;
10
+ .tabs{
11
+ display: flex;
12
+ justify-content: flex-start;
13
+ align-items: center;
14
+ }
15
+ .tab{
16
+ padding: 5px 10px;
17
+ cursor: pointer;
18
+ }
19
+ .tab.active{
20
+ color: #FC6506;
21
+ font-weight: bold;
22
+ position: relative;
23
+ &::after{
24
+ content: '';
25
+ position: absolute;
26
+ bottom: -10px;
27
+ left: 0;
28
+ display: block;
29
+ transform: translateX(50%);
30
+ width: 50%;
31
+ height: 2px;
32
+ background-color: #FC6506;
33
+ }
34
+ }
35
+ .right{
36
+ display: flex;
37
+ justify-content: flex-end;
38
+ align-items: center;
39
+ }
40
+ }
41
+ }
@@ -0,0 +1,183 @@
1
+ import { defineComponent, ref, nextTick, watch, Teleport } from 'vue'
2
+ import styles from './index.module.scss'
3
+ import { Close } from '@element-plus/icons-vue'
4
+ export default defineComponent({
5
+ // 输入列组件
6
+ name: 'InputColumn',
7
+ props: {
8
+
9
+ },
10
+
11
+
12
+ setup(props, { attrs }) {
13
+ const showInput = ref(false)
14
+
15
+ const inputColumnnRef = ref(null)
16
+ const textareaRef = ref(null)
17
+ const aiGenerateRef = ref(null)
18
+ const showAiGenerate = ref(false)
19
+ const aiGenerateValue = ref('你好,我想咨询一下关于高品质跑步机的选购问题。 你好,我想咨询一下关于高品质跑步机的选购问题。')
20
+ const onClickOutside = (element, handler) => {
21
+ const listener = (e) => {
22
+ if (!element || element.contains(e.target)) return;
23
+ handler(e);
24
+ };
25
+ document.addEventListener('click', listener, false);
26
+ // 返回取消函数,想停就调它
27
+ return () => document.removeEventListener('click', listener, false);
28
+ }
29
+ watch(() => showInput.value, (newVal) => {
30
+ console.log(attrs, 'attrs');
31
+
32
+ if (newVal) {
33
+ const cell = inputColumnnRef.value.parentNode
34
+ if (!cell) return
35
+ const column = cell.parentNode
36
+ console.log(column, 'column');
37
+ if (!column) return
38
+ const elTable__cell = column.parentNode
39
+ if (!elTable__cell) return
40
+ const width = elTable__cell.offsetWidth
41
+ const rect = elTable__cell.getBoundingClientRect() // 相对于视口
42
+ const x = rect.left + window.scrollX
43
+ const y = rect.top + window.scrollY
44
+ nextTick(() => {
45
+ const textarea = textareaRef.value
46
+ if (!textarea) return
47
+ textarea.style.width = `${width}px`
48
+ document.body.style.position = 'relative'
49
+ textarea.style.top = `${y}px`
50
+ textarea.style.left = `${x}px`
51
+
52
+ })
53
+
54
+
55
+ }
56
+ })
57
+
58
+ watch(() => textareaRef.value, (newVal, _, onCleanup) => {
59
+ if (newVal) {
60
+ setTimeout(() => {
61
+ const removeClickOutside = onClickOutside(newVal, () => {
62
+ showInput.value = false
63
+ showAiGenerate.value = false
64
+ })
65
+ onCleanup(() => {
66
+ removeClickOutside()
67
+ })
68
+ const elTextareaInner = textareaRef.value.querySelector('.el-textarea__inner')
69
+ if (elTextareaInner) {
70
+ elTextareaInner.focus()
71
+ }
72
+ }, 0)
73
+
74
+ }
75
+ })
76
+
77
+
78
+ watch(() => showAiGenerate.value, async (newVal) => {
79
+ if (newVal) {
80
+ await nextTick()
81
+ const width = textareaRef.value.offsetWidth
82
+ const rect = textareaRef.value.getBoundingClientRect() // 相对于视口
83
+ const left = rect.left
84
+ if (left > width) {
85
+ aiGenerateRef.value.style.left = `${-width}px`
86
+ } else {
87
+ aiGenerateRef.value.style.left = `${width}px`
88
+ }
89
+
90
+ }
91
+ })
92
+
93
+ return () => {
94
+ return <div class={[styles.inputColumn]}>
95
+ <div ref={inputColumnnRef} >
96
+ {
97
+ showInput.value && (
98
+ <Teleport to="body">
99
+ <div class={[styles.inputColumnTextarea]} ref={textareaRef}>
100
+ <div>
101
+ <el-input maxlength={500} type="textarea" rows={4} v-model={attrs.scope.row[attrs.tableColumn.prop]} />
102
+ <div class={[styles.aiGenerateBtnContainer]}>
103
+ {
104
+ !attrs.tableColumn.noAiGenerate ?(
105
+ <span class={[styles.aiGenerateBtn]} onClick={() => {
106
+ showAiGenerate.value = true
107
+ }}>
108
+ AI润色
109
+ </span>
110
+ )
111
+ :
112
+ <span> </span>
113
+ }
114
+ <span>
115
+ {attrs.scope.row[attrs.tableColumn.prop].length}/<span style={{ color: '#999' }}>500</span>
116
+ </span>
117
+ </div>
118
+ </div>
119
+ {
120
+ showAiGenerate.value &&
121
+
122
+ <div class={[styles.aiGenerate]} ref={aiGenerateRef}>
123
+ <div class={[styles.header]}>
124
+ <div class={[styles.title]}>
125
+ AI生成
126
+ </div>
127
+ <el-icon class={[styles.close]} onClick={(e) => {
128
+ showAiGenerate.value = false
129
+ e.stopPropagation()
130
+ }}>
131
+ <Close />
132
+ </el-icon>
133
+ </div>
134
+ <el-scrollbar>
135
+ <div class={[styles.content]}>
136
+ {aiGenerateValue.value || 'AI润色后的脚本'}
137
+ </div>
138
+ </el-scrollbar>
139
+ <div class={[styles.footer]} >
140
+ <span onClick={(e) => {
141
+ attrs.scope.row[attrs.tableColumn.prop] = aiGenerateValue.value
142
+ showAiGenerate.value = false
143
+ e.stopPropagation()
144
+ }}>
145
+ 一键使用
146
+ </span>
147
+ <span onClick={(e) => {
148
+ e.stopPropagation()
149
+ }}>
150
+ 重新生成
151
+ </span>
152
+ </div>
153
+ </div>
154
+ }
155
+ </div>
156
+ </Teleport >
157
+ )}
158
+ <div onClick={(e) => {
159
+ console.log(e.target, attrs.scope.row[attrs.tableColumn.prop], '点击了输入框');
160
+ showInput.value = true
161
+ // e.stopPropagation()
162
+ }}>
163
+ {
164
+ attrs.scope.row[attrs.tableColumn.prop] ?
165
+ (attrs.tableColumn.tag ?
166
+ <div class={[styles.tagContainer]} title={attrs.scope.row[attrs.tableColumn.prop]}>
167
+
168
+ {attrs.scope.row[attrs.tableColumn.prop].split(attrs.tableColumn.split).map((item, index) => (
169
+ item.trim() ? <div class={[styles.tag]} key={index}>{item}</div> : null
170
+ ))}
171
+ </div>
172
+ :
173
+ <div class={[styles.inputColumnTextareaValue]} title={attrs.scope.row[attrs.tableColumn.prop]}>{attrs.scope.row[attrs.tableColumn.prop]}111</div>
174
+ )
175
+ :
176
+ <div class={[styles.inputColumnTextareaValue]}>{attrs.tableColumn.placeholder || '请输入'}</div>
177
+ }
178
+ </div>
179
+ </div>
180
+ </div>
181
+ }
182
+ }
183
+ })
@@ -0,0 +1,115 @@
1
+ .inputColumn {
2
+ .inputColumnTextareaValue {
3
+ overflow: hidden;
4
+ display: -webkit-box;
5
+ -webkit-box-orient: vertical;
6
+ -webkit-line-clamp: 2;
7
+ /* 想几行就写几 */
8
+ text-overflow: ellipsis;
9
+ word-break: break-all;
10
+ /* 可选,英文长单词截断 */
11
+ line-height: 18px;
12
+ }
13
+
14
+ .tagContainer {
15
+ display: flex;
16
+ flex-wrap: wrap;
17
+ gap: 4px;
18
+ .tag {
19
+ padding: 2px 8px;
20
+ background-color: #F5F5F5;
21
+ border-radius: 4px;
22
+ font-size: 12px;
23
+ }
24
+ }
25
+
26
+ }
27
+
28
+ .inputColumnTextarea {
29
+ border: 1px solid #D9D9D9;
30
+ background-color: #fff;
31
+ border-radius: 8px;
32
+
33
+ .aiGenerateBtnContainer {
34
+ background-color: #fff;
35
+ padding: 0 10px 6px;
36
+ display: flex;
37
+ justify-content: space-between;
38
+ border-radius: 8px;
39
+
40
+ .aiGenerateBtn {
41
+ color: #4F8BFF;
42
+ cursor: pointer;
43
+ }
44
+ }
45
+
46
+ :global {
47
+ .el-textarea__inner {
48
+ box-shadow: none;
49
+ border-radius: 8px;
50
+ }
51
+
52
+ .el-textarea {
53
+ box-shadow: none;
54
+ border-radius: 8px;
55
+ }
56
+ }
57
+
58
+
59
+ position: absolute;
60
+ top: 0;
61
+ left: 0;
62
+ z-index: 999999;
63
+
64
+ .aiGenerate {
65
+ box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.1);
66
+ border-radius: 8px;
67
+ position: absolute;
68
+ top: 0;
69
+ left: -100%;
70
+ z-index: 999999;
71
+ background-color: #F2F4FA;
72
+ width: 100%;
73
+ height: 100%;
74
+ padding: 10px;
75
+ box-sizing: border-box;
76
+ display: flex;
77
+ flex-direction: column;
78
+ justify-content: space-between;
79
+
80
+ .header {
81
+ display: flex;
82
+ justify-content: space-between;
83
+ align-items: center;
84
+ font-size: 12px;
85
+ color: #222222;
86
+ font-weight: bold;
87
+
88
+ .close {
89
+ cursor: pointer;
90
+ }
91
+ }
92
+
93
+ .content {
94
+ font-size: 12px;
95
+ color: #222222;
96
+ flex: 1;
97
+ }
98
+
99
+ .footer {
100
+ display: flex;
101
+ height: 12px;
102
+ gap: 10px;
103
+
104
+ span {
105
+ cursor: pointer;
106
+ font-size: 12px;
107
+ color: #4F8BFF;
108
+
109
+ &:hover {
110
+ color: #3A73E8;
111
+ }
112
+ }
113
+ }
114
+ }
115
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "npmapps",
3
- "version": "1.0.20",
3
+ "version": "1.0.21",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -1,169 +0,0 @@
1
- <script lang="jsx">
2
- import { defineComponent, ref, computed, watch } from "vue";
3
- import { ArrowDown } from "@element-plus/icons-vue";
4
- export default defineComponent({
5
- name: "BtnSelect",
6
- props: {
7
- options: { type: Array, default: () => [] },
8
- modelValue: { type: [String, Number, Array, Object, Boolean], default: null },
9
- multiple: { type: Boolean, default: false },
10
- searchable: { type: Boolean, default: false },
11
- label: { type: String, default: "" },
12
- optionKeys: { type: Object, default: () => ({ label: "label", value: "value" }) },
13
- maxLabelCount: { type: Number, default: 3 },
14
- disabled: { type: Boolean, default: false },
15
- collapseTags: { type: Boolean, default: false },
16
- itemRender: { type: Function, default: null },
17
- },
18
- emits: ["update:modelValue", "change"],
19
- setup(props, { emit }) {
20
- const keyword = ref("");
21
- const internal = ref(props.multiple ? ([]).concat(props.modelValue || []) : props.modelValue);
22
- watch(
23
- () => props.modelValue,
24
- (v) => {
25
- internal.value = props.multiple ? ([]).concat(v || []) : v;
26
- }
27
- );
28
-
29
- const isChecked = (val) => {
30
- const cur = internal.value;
31
- return Array.isArray(cur) ? cur.includes(val) : cur === val;
32
- };
33
-
34
- const toggle = (val) => {
35
- const list = props.options || [];
36
- const { label: labelKey, value: valueKey } = props.optionKeys || { label: "label", value: "value" };
37
- const found = list.find((i) => i[valueKey] === val);
38
- if (found && found.disabled) return;
39
- if (props.multiple) {
40
- const set = new Set(internal.value || []);
41
- if (set.has(val)) set.delete(val);
42
- else set.add(val);
43
- const next = Array.from(set);
44
- internal.value = next;
45
- emit("update:modelValue", next);
46
- emit("change", next);
47
- } else {
48
- internal.value = val;
49
- emit("update:modelValue", val);
50
- emit("change", val);
51
- }
52
- };
53
-
54
- const filtered = computed(() => {
55
- const list = props.options || [];
56
- const { label: labelKey, value: valueKey } = props.optionKeys || { label: "label", value: "value" };
57
- if (!props.searchable || !keyword.value) return list;
58
- const k = String(keyword.value).toLowerCase();
59
- return list.filter((o) => String(o[labelKey]).toLowerCase().includes(k));
60
- });
61
-
62
- const displayMeta = computed(() => {
63
- const list = props.options || [];
64
- const { label: labelKey, value: valueKey } = props.optionKeys || { label: "label", value: "value" };
65
- if (props.multiple) {
66
- const vals = internal.value || [];
67
- if (!vals.length) return { text: props.label || "请选择", rest: 0 };
68
- const labels = vals.map((v) => {
69
- const o = list.find((i) => i[valueKey] === v);
70
- return o ? o[labelKey] : v;
71
- });
72
- if (props.collapseTags) {
73
- const first = labels[0];
74
- const rest = labels.length - 1;
75
- return { text: `${props.label ? props.label + ":" : ""}${first}`, rest };
76
- }
77
- if (labels.length > props.maxLabelCount) return { text: (props.label ? props.label + ":" : "") + labels.slice(0, props.maxLabelCount).join("、") + ` 等${labels.length}项`, rest: 0 };
78
- return { text: (props.label ? props.label + ":" : "") + labels.join("、"), rest: 0 };
79
- } else {
80
- const v = internal.value;
81
- if (v === undefined || v === null || v === "") return { text: props.label || "请选择", rest: 0 };
82
- const o = list.find((i) => i[valueKey] === v);
83
- const text = o ? o[labelKey] : String(v);
84
- return { text: props.label ? `${props.label}:${text}` : text, rest: 0 };
85
- }
86
- });
87
-
88
- const clear = () => {
89
- if (props.disabled) return;
90
- internal.value = props.multiple ? [] : null;
91
- emit("update:modelValue", internal.value);
92
- emit("change", internal.value);
93
- };
94
-
95
- const hasValue = computed(() => (Array.isArray(internal.value) ? internal.value.length > 0 : internal.value !== null && internal.value !== undefined && internal.value !== "") );
96
-
97
- return () => (
98
- <el-dropdown hideOnClick={props.multiple ? false : true} disabled={props.disabled}>
99
- {{
100
- default: () => (
101
- <el-button>
102
- <span style="display:inline-flex;align-items:center;gap:6px;">
103
- <span>{displayMeta.value.text}</span>
104
- {props.collapseTags && displayMeta.value.rest > 0 ? (
105
- <span style="display:inline-flex;align-items:center;justify-content:center;min-width:18px;height:18px;padding:0 4px;border-radius:9px;background:rgba(64,158,255,0.15);color:#409EFF;font-size:12px;line-height:18px;">{`+${displayMeta.value.rest}`}</span>
106
- ) : null}
107
- {hasValue.value ? (
108
- <span
109
- title="清除"
110
- style="display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;border-radius:50%;background:rgba(0,0,0,0.06);cursor:pointer;margin-left:4px;"
111
- onClick={(e) => {
112
- e.stopPropagation();
113
- clear();
114
- }}
115
- >
116
- ×
117
- </span>
118
- ) : (
119
- <span style="margin-left:4px;opacity:0.7;">
120
- <el-icon><ArrowDown /></el-icon>
121
- </span>
122
- )}
123
- </span>
124
- </el-button>
125
- ),
126
- dropdown: () => (
127
- <el-dropdown-menu>
128
- {props.searchable ? (
129
- <div style="padding:8px 12px;">
130
- <el-input
131
- modelValue={keyword.value}
132
- onUpdate:modelValue={(v) => {
133
- keyword.value = v;
134
- }}
135
- clearable={true}
136
- placeholder="搜索"
137
- size="small"
138
- />
139
- </div>
140
- ) : null}
141
- {filtered.value.map((opt) => (
142
- <el-dropdown-item
143
- key={opt[(props.optionKeys || {}).value || "value"]}
144
- command={opt[(props.optionKeys || {}).value || "value"]}
145
- disabled={Boolean(opt.disabled)}
146
- onClick={() => toggle(opt[(props.optionKeys || {}).value || "value"])}
147
- style={isChecked(opt[(props.optionKeys || {}).value || "value"]) ? "background: rgba(64,158,255,0.08);" : ""}
148
- >
149
- {props.itemRender ? (
150
- props.itemRender(opt, {
151
- checked: isChecked(opt[(props.optionKeys || {}).value || "value"]),
152
- })
153
- ) : (
154
- <div style="display:flex;align-items:center;justify-content:space-between;min-width:180px;">
155
- <span>{opt[(props.optionKeys || {}).label || "label"]}</span>
156
- {isChecked(opt[(props.optionKeys || {}).value || "value"]) ? <span style="color:#67C23A;">✓ 已选</span> : null}
157
- </div>
158
- )}
159
- </el-dropdown-item>
160
- ))}
161
- {!filtered.value.length ? <el-dropdown-item disabled={true}>无数据</el-dropdown-item> : null}
162
- </el-dropdown-menu>
163
- ),
164
- }}
165
- </el-dropdown>
166
- );
167
- },
168
- });
169
- </script>
@@ -1,69 +0,0 @@
1
- <script lang="jsx">
2
- import { defineComponent, ref } from "vue";
3
- import BtnSelect from "./btnSelect.vue";
4
-
5
- export default defineComponent({
6
- name: "BtnSelectView",
7
- setup() {
8
- const options = ref([
9
- { label: "选项一", value: "a" },
10
- { label: "选项二", value: "b" },
11
- { label: "选项三", value: "c" },
12
- { label: "禁用项", value: "d", disabled: true },
13
- ]);
14
- const singleValue = ref(null);
15
- const multiValue = ref([]);
16
- const searchValue = ref(null);
17
- return () => (
18
- <>
19
- <div style="display:flex; gap:16px; align-items:center; flex-wrap:wrap;">
20
- <span>按钮下拉框(普通)</span>
21
- <BtnSelect
22
- label="名称"
23
- options={options.value}
24
- modelValue={singleValue.value}
25
- onUpdate:modelValue={(v) => {
26
- singleValue.value = v;
27
- }}
28
- />
29
- <span>按钮下拉框(多选)</span>
30
- <BtnSelect
31
- label="名称"
32
- options={options.value}
33
- multiple={true}
34
- modelValue={multiValue.value}
35
- onUpdate:modelValue={(v) => {
36
- multiValue.value = v;
37
- }}
38
- />
39
- <span>按钮下拉框(带搜索)</span>
40
- <BtnSelect
41
- label="名称"
42
- options={options.value}
43
- searchable={true}
44
- multiple={true}
45
- collapseTags={true}
46
- itemRender={(opt, { checked }) => (
47
- <div style="display:flex;align-items:center;justify-content:space-between;min-width:200px;">
48
- <span>
49
- {opt.label}
50
- {opt.disabled ? <span style="margin-left:8px;color:#F56C6C;">禁用</span> : null}
51
- </span>
52
- {checked ? (
53
- <span style="color:#67C23A;">✓ 已选</span>
54
- ) : (
55
- <span style="color:#909399;">点击选择</span>
56
- )}
57
- </div>
58
- )}
59
- modelValue={searchValue.value}
60
- onUpdate:modelValue={(v) => {
61
- searchValue.value = v;
62
- }}
63
- />
64
- </div>
65
- </>
66
- );
67
- },
68
- });
69
- </script>
package/app/btnSelect.zip DELETED
Binary file