vue_zhongyou 1.0.7 → 1.0.8
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 +1 -1
- package//345/212/237/350/203/275/344/273/243/347/240/201/AI/345/257/271/350/257/235/ChatForm.vue +378 -0
- package//345/212/237/350/203/275/344/273/243/347/240/201/AI/345/257/271/350/257/235/aiChatPage.vue +757 -0
- package//345/212/237/350/203/275/344/273/243/347/240/201/AI/345/257/271/350/257/235/newChatCardList.vue +202 -0
- package//346/226/207/346/241/243/ai/346/250/241/345/235/227.md +33 -0
package/package.json
CHANGED
package//345/212/237/350/203/275/344/273/243/347/240/201/AI/345/257/271/350/257/235/ChatForm.vue
ADDED
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="chat-form">
|
|
3
|
+
<van-form :model="formData" ref="formRef">
|
|
4
|
+
<template v-for="field in schema" :key="field.field">
|
|
5
|
+
<!-- 输入框 -->
|
|
6
|
+
<van-field
|
|
7
|
+
v-if="field.type === 'input'"
|
|
8
|
+
v-model="formData[field.field]"
|
|
9
|
+
:label="field.label"
|
|
10
|
+
:placeholder="field.placeholder"
|
|
11
|
+
:rules="field.rules"
|
|
12
|
+
:required="field.rules?.some(rule => rule.required)"
|
|
13
|
+
/>
|
|
14
|
+
|
|
15
|
+
<!-- 文本域 -->
|
|
16
|
+
<van-field
|
|
17
|
+
v-else-if="field.type === 'textarea'"
|
|
18
|
+
v-model="formData[field.field]"
|
|
19
|
+
:label="field.label"
|
|
20
|
+
type="textarea"
|
|
21
|
+
:placeholder="field.placeholder"
|
|
22
|
+
:rows="field.rows || 2"
|
|
23
|
+
:autosize="field.autosize || false"
|
|
24
|
+
/>
|
|
25
|
+
|
|
26
|
+
<!-- 选择器 -->
|
|
27
|
+
<van-field
|
|
28
|
+
v-else-if="field.type === 'select'"
|
|
29
|
+
v-model="formData[field.field]"
|
|
30
|
+
:label="field.label"
|
|
31
|
+
is-link
|
|
32
|
+
readonly
|
|
33
|
+
:placeholder="field.placeholder"
|
|
34
|
+
@click="() => showPicker(field)"
|
|
35
|
+
>
|
|
36
|
+
<template #input>
|
|
37
|
+
<span v-if="formData[field.field]">{{ getOptionLabel(field, formData[field.field]) }}</span>
|
|
38
|
+
</template>
|
|
39
|
+
</van-field>
|
|
40
|
+
|
|
41
|
+
<!-- 单选框 -->
|
|
42
|
+
<van-cell-group v-else-if="field.type === 'radio'" :title="field.label">
|
|
43
|
+
<van-radio-group v-model="formData[field.field]" direction="horizontal">
|
|
44
|
+
<van-radio
|
|
45
|
+
v-for="option in field.options"
|
|
46
|
+
:key="option.value"
|
|
47
|
+
:name="option.value"
|
|
48
|
+
icon-size="16px"
|
|
49
|
+
checked-color="#1989fa"
|
|
50
|
+
>
|
|
51
|
+
{{ option.label }}
|
|
52
|
+
</van-radio>
|
|
53
|
+
</van-radio-group>
|
|
54
|
+
</van-cell-group>
|
|
55
|
+
|
|
56
|
+
<!-- 复选框 -->
|
|
57
|
+
<van-cell-group v-else-if="field.type === 'checkbox'" :title="field.label">
|
|
58
|
+
<van-checkbox-group v-model="formData[field.field]" direction="horizontal">
|
|
59
|
+
<van-checkbox
|
|
60
|
+
v-for="option in field.options"
|
|
61
|
+
:key="option.value"
|
|
62
|
+
:name="option.value"
|
|
63
|
+
icon-size="16px"
|
|
64
|
+
checked-color="#1989fa"
|
|
65
|
+
>
|
|
66
|
+
{{ option.label }}
|
|
67
|
+
</van-checkbox>
|
|
68
|
+
</van-checkbox-group>
|
|
69
|
+
</van-cell-group>
|
|
70
|
+
|
|
71
|
+
<!-- 日期选择 -->
|
|
72
|
+
<van-field
|
|
73
|
+
v-else-if="field.type === 'date'"
|
|
74
|
+
v-model="formData[field.field]"
|
|
75
|
+
:label="field.label"
|
|
76
|
+
is-link
|
|
77
|
+
readonly
|
|
78
|
+
:placeholder="field.placeholder"
|
|
79
|
+
@click="() => showDatePicker(field)"
|
|
80
|
+
/>
|
|
81
|
+
|
|
82
|
+
<!-- 日期范围 -->
|
|
83
|
+
<van-cell-group v-else-if="field.type === 'dateRange'" :title="field.label">
|
|
84
|
+
<van-cell
|
|
85
|
+
:title="`${field.label}开始`"
|
|
86
|
+
:value="formData[field.field]?.[0] || '请选择'"
|
|
87
|
+
is-link
|
|
88
|
+
@click="() => showDateRangePicker(field, 0)"
|
|
89
|
+
/>
|
|
90
|
+
<van-cell
|
|
91
|
+
:title="`${field.label}结束`"
|
|
92
|
+
:value="formData[field.field]?.[1] || '请选择'"
|
|
93
|
+
is-link
|
|
94
|
+
@click="() => showDateRangePicker(field, 1)"
|
|
95
|
+
/>
|
|
96
|
+
</van-cell-group>
|
|
97
|
+
</template>
|
|
98
|
+
|
|
99
|
+
<!-- 操作按钮 -->
|
|
100
|
+
<div class="form-actions" v-if="showActions">
|
|
101
|
+
<van-button
|
|
102
|
+
v-if="submitButtonLabel"
|
|
103
|
+
type="primary"
|
|
104
|
+
block
|
|
105
|
+
:loading="isSubmitting"
|
|
106
|
+
@click="onSubmit"
|
|
107
|
+
>
|
|
108
|
+
{{ submitButtonLabel }}
|
|
109
|
+
</van-button>
|
|
110
|
+
<van-button
|
|
111
|
+
v-if="resetButtonLabel"
|
|
112
|
+
type="default"
|
|
113
|
+
block
|
|
114
|
+
@click="onReset"
|
|
115
|
+
>
|
|
116
|
+
{{ resetButtonLabel }}
|
|
117
|
+
</van-button>
|
|
118
|
+
</div>
|
|
119
|
+
</van-form>
|
|
120
|
+
|
|
121
|
+
<!-- 选择器弹窗 -->
|
|
122
|
+
<van-popup v-model:show="showPickerDialog" position="bottom">
|
|
123
|
+
<van-picker
|
|
124
|
+
:columns="currentPickerColumns"
|
|
125
|
+
@confirm="onPickerConfirm"
|
|
126
|
+
@cancel="showPickerDialog = false"
|
|
127
|
+
/>
|
|
128
|
+
</van-popup>
|
|
129
|
+
|
|
130
|
+
<!-- 日期选择器弹窗 -->
|
|
131
|
+
<van-popup v-model:show="showDatePickerDialog" position="bottom">
|
|
132
|
+
<van-date-picker
|
|
133
|
+
v-model="currentDateValue"
|
|
134
|
+
@confirm="onDatePickerConfirm"
|
|
135
|
+
@cancel="showDatePickerDialog = false"
|
|
136
|
+
/>
|
|
137
|
+
</van-popup>
|
|
138
|
+
|
|
139
|
+
<!-- 日期范围选择器弹窗 -->
|
|
140
|
+
<van-popup v-model:show="showDateRangePickerDialog" position="bottom">
|
|
141
|
+
<van-date-picker
|
|
142
|
+
v-model="currentDateRangeValue"
|
|
143
|
+
@confirm="onDateRangePickerConfirm"
|
|
144
|
+
@cancel="showDateRangePickerDialog = false"
|
|
145
|
+
/>
|
|
146
|
+
</van-popup>
|
|
147
|
+
</div>
|
|
148
|
+
</template>
|
|
149
|
+
|
|
150
|
+
<script setup>
|
|
151
|
+
import { ref, reactive, watch, onMounted } from 'vue'
|
|
152
|
+
import { showToast } from 'vant'
|
|
153
|
+
|
|
154
|
+
// 定义组件属性
|
|
155
|
+
const props = defineProps({
|
|
156
|
+
modelValue: {
|
|
157
|
+
type: Object,
|
|
158
|
+
default: () => ({})
|
|
159
|
+
},
|
|
160
|
+
schema: {
|
|
161
|
+
type: Array,
|
|
162
|
+
required: true
|
|
163
|
+
},
|
|
164
|
+
submitButtonLabel: {
|
|
165
|
+
type: String,
|
|
166
|
+
default: '提交'
|
|
167
|
+
},
|
|
168
|
+
resetButtonLabel: {
|
|
169
|
+
type: String,
|
|
170
|
+
default: '重置'
|
|
171
|
+
},
|
|
172
|
+
showActions: {
|
|
173
|
+
type: Boolean,
|
|
174
|
+
default: true
|
|
175
|
+
}
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
// 定义事件
|
|
179
|
+
const emit = defineEmits(['update:modelValue', 'submit', 'change'])
|
|
180
|
+
|
|
181
|
+
// 表单数据
|
|
182
|
+
const formData = ref({})
|
|
183
|
+
|
|
184
|
+
// 表单引用
|
|
185
|
+
const formRef = ref(null)
|
|
186
|
+
|
|
187
|
+
// 提交状态
|
|
188
|
+
const isSubmitting = ref(false)
|
|
189
|
+
|
|
190
|
+
// 弹窗相关
|
|
191
|
+
const showPickerDialog = ref(false)
|
|
192
|
+
const showDatePickerDialog = ref(false)
|
|
193
|
+
const showDateRangePickerDialog = ref(false)
|
|
194
|
+
const currentField = ref(null)
|
|
195
|
+
const currentPickerColumns = ref([])
|
|
196
|
+
const currentDateValue = ref(new Date())
|
|
197
|
+
const currentDateRangeValue = ref([new Date(), new Date()])
|
|
198
|
+
|
|
199
|
+
// 初始化表单数据
|
|
200
|
+
const initializeFormData = () => {
|
|
201
|
+
const data = { ...props.modelValue }
|
|
202
|
+
|
|
203
|
+
props.schema.forEach(field => {
|
|
204
|
+
// 如果没有初始值但有默认值,则使用默认值
|
|
205
|
+
if (!(field.field in data) && 'default' in field) {
|
|
206
|
+
data[field.field] = field.default
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// 确保所有字段都在数据中
|
|
210
|
+
if (!(field.field in data)) {
|
|
211
|
+
if (field.type === 'checkbox') {
|
|
212
|
+
data[field.field] = []
|
|
213
|
+
} else if (field.type === 'dateRange') {
|
|
214
|
+
data[field.field] = []
|
|
215
|
+
} else {
|
|
216
|
+
data[field.field] = ''
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
formData.value = data
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// 获取选项标签
|
|
225
|
+
const getOptionLabel = (field, value) => {
|
|
226
|
+
const option = field.options.find(opt => opt.value === value)
|
|
227
|
+
return option ? option.label : value
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// 显示选择器
|
|
231
|
+
const showPicker = (field) => {
|
|
232
|
+
currentField.value = field
|
|
233
|
+
currentPickerColumns.value = field.options.map(option => ({
|
|
234
|
+
text: option.label,
|
|
235
|
+
value: option.value
|
|
236
|
+
}))
|
|
237
|
+
showPickerDialog.value = true
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// 选择器确认
|
|
241
|
+
const onPickerConfirm = ({ selectedValues }) => {
|
|
242
|
+
const value = selectedValues[0]
|
|
243
|
+
formData.value[currentField.value.field] = value
|
|
244
|
+
emit('update:modelValue', formData.value)
|
|
245
|
+
emit('change', { field: currentField.value.field, value })
|
|
246
|
+
showPickerDialog.value = false
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// 显示日期选择器
|
|
250
|
+
const showDatePicker = (field) => {
|
|
251
|
+
currentField.value = field
|
|
252
|
+
currentDateValue.value = formData.value[field.field] ? new Date(formData.value[field.field]) : new Date()
|
|
253
|
+
showDatePickerDialog.value = true
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// 日期选择器确认
|
|
257
|
+
const onDatePickerConfirm = ({ selectedValues }) => {
|
|
258
|
+
const date = new Date(selectedValues[0], selectedValues[1] - 1, selectedValues[2])
|
|
259
|
+
const dateString = date.toISOString().split('T')[0]
|
|
260
|
+
formData.value[currentField.value.field] = dateString
|
|
261
|
+
emit('update:modelValue', formData.value)
|
|
262
|
+
emit('change', { field: currentField.value.field, value: dateString })
|
|
263
|
+
showDatePickerDialog.value = false
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// 显示日期范围选择器
|
|
267
|
+
const showDateRangePicker = (field, index) => {
|
|
268
|
+
currentField.value = field
|
|
269
|
+
const range = formData.value[field.field] || []
|
|
270
|
+
if (range[index]) {
|
|
271
|
+
currentDateRangeValue.value[index] = new Date(range[index])
|
|
272
|
+
}
|
|
273
|
+
showDateRangePickerDialog.value = true
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// 日期范围选择器确认
|
|
277
|
+
const onDateRangePickerConfirm = ({ selectedValues }) => {
|
|
278
|
+
const startDate = new Date(selectedValues[0][0], selectedValues[0][1] - 1, selectedValues[0][2])
|
|
279
|
+
const endDate = new Date(selectedValues[1][0], selectedValues[1][1] - 1, selectedValues[1][2])
|
|
280
|
+
|
|
281
|
+
const range = [...(formData.value[currentField.value.field] || [])]
|
|
282
|
+
range[0] = startDate.toISOString().split('T')[0]
|
|
283
|
+
range[1] = endDate.toISOString().split('T')[0]
|
|
284
|
+
|
|
285
|
+
formData.value[currentField.value.field] = range
|
|
286
|
+
emit('update:modelValue', formData.value)
|
|
287
|
+
emit('change', { field: currentField.value.field, value: range })
|
|
288
|
+
showDateRangePickerDialog.value = false
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// 提交表单
|
|
292
|
+
const onSubmit = async () => {
|
|
293
|
+
if (formRef.value) {
|
|
294
|
+
try {
|
|
295
|
+
await formRef.value.validate()
|
|
296
|
+
isSubmitting.value = true
|
|
297
|
+
emit('submit', formData.value)
|
|
298
|
+
} catch (error) {
|
|
299
|
+
showToast(error.message || '请检查表单填写是否正确')
|
|
300
|
+
} finally {
|
|
301
|
+
isSubmitting.value = false
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// 重置表单
|
|
307
|
+
const onReset = () => {
|
|
308
|
+
initializeFormData()
|
|
309
|
+
emit('update:modelValue', formData.value)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// 监听表单数据变化
|
|
313
|
+
watch(formData, (newVal) => {
|
|
314
|
+
emit('update:modelValue', newVal)
|
|
315
|
+
}, { deep: true })
|
|
316
|
+
|
|
317
|
+
// 监听外部传入的值变化
|
|
318
|
+
watch(() => props.modelValue, (newVal) => {
|
|
319
|
+
formData.value = { ...newVal }
|
|
320
|
+
}, { deep: true })
|
|
321
|
+
|
|
322
|
+
// 组件挂载时初始化数据
|
|
323
|
+
onMounted(() => {
|
|
324
|
+
initializeFormData()
|
|
325
|
+
})
|
|
326
|
+
</script>
|
|
327
|
+
|
|
328
|
+
<style lang="scss" scoped>
|
|
329
|
+
.chat-form {
|
|
330
|
+
background: #fff;
|
|
331
|
+
border-radius: 12px;
|
|
332
|
+
padding: 16px;
|
|
333
|
+
margin: 8px 0;
|
|
334
|
+
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
|
|
335
|
+
|
|
336
|
+
// 当在消息气泡中显示时,调整样式
|
|
337
|
+
:deep(.message-bubble) & {
|
|
338
|
+
background: transparent;
|
|
339
|
+
box-shadow: none;
|
|
340
|
+
margin: 0;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
.form-actions {
|
|
344
|
+
margin-top: 20px;
|
|
345
|
+
display: flex;
|
|
346
|
+
flex-direction: column;
|
|
347
|
+
gap: 12px;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
:deep(.van-cell-group) {
|
|
351
|
+
margin-bottom: 16px;
|
|
352
|
+
|
|
353
|
+
.van-cell {
|
|
354
|
+
padding: 12px 0;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
:deep(.van-radio-group--horizontal) {
|
|
359
|
+
flex-wrap: wrap;
|
|
360
|
+
gap: 8px;
|
|
361
|
+
padding: 8px 0;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
:deep(.van-checkbox-group--horizontal) {
|
|
365
|
+
flex-wrap: wrap;
|
|
366
|
+
gap: 8px;
|
|
367
|
+
padding: 8px 0;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
:deep(.van-radio) {
|
|
371
|
+
margin-bottom: 4px;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
:deep(.van-checkbox) {
|
|
375
|
+
margin-bottom: 4px;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
</style>
|
package//345/212/237/350/203/275/344/273/243/347/240/201/AI/345/257/271/350/257/235/aiChatPage.vue
ADDED
|
@@ -0,0 +1,757 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="ai-chat-page">
|
|
3
|
+
<!-- 顶部导航栏 -->
|
|
4
|
+
<van-nav-bar
|
|
5
|
+
class="nav-bar"
|
|
6
|
+
title="智能工作台"
|
|
7
|
+
left-arrow
|
|
8
|
+
@click-left="$router.back()"
|
|
9
|
+
fixed
|
|
10
|
+
placeholder
|
|
11
|
+
/>
|
|
12
|
+
|
|
13
|
+
<!-- 消息列表容器 -->
|
|
14
|
+
<div class="chat-container" ref="chatContainerRef">
|
|
15
|
+
<div class="messages-wrapper">
|
|
16
|
+
<!-- 消息列表 -->
|
|
17
|
+
<div
|
|
18
|
+
v-for="(message, index) in messages"
|
|
19
|
+
:key="index"
|
|
20
|
+
class="message-item"
|
|
21
|
+
:class="{ 'user-message': message.role === 'user', 'ai-message': message.role === 'assistant' }"
|
|
22
|
+
>
|
|
23
|
+
<div class="message-content">
|
|
24
|
+
<div class="message-bubble">
|
|
25
|
+
<!-- 文本消息 -->
|
|
26
|
+
<div v-if="isTextMessage(message)" class="message-text" v-html="formatMessage(message.content)"></div>
|
|
27
|
+
|
|
28
|
+
<!-- 表单消息 -->
|
|
29
|
+
<div v-else-if="isFormMessage(message)">
|
|
30
|
+
<!-- 文字介绍 -->
|
|
31
|
+
<div v-if="message.output" class="message-output" v-html="formatMessage(message.output)"></div>
|
|
32
|
+
<!-- 表单内容 -->
|
|
33
|
+
<ChatForm
|
|
34
|
+
:schema="message.formSchema"
|
|
35
|
+
:model-value="message.formData || {}"
|
|
36
|
+
@submit="handleFormSubmit(index, $event)"
|
|
37
|
+
/>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<!-- 列表卡片消息 -->
|
|
41
|
+
<div v-else-if="isCardListMessage(message)">
|
|
42
|
+
<!-- 文字介绍 -->
|
|
43
|
+
<div v-if="message.output" class="message-output" v-html="formatMessage(message.output)"></div>
|
|
44
|
+
<!-- 卡片列表内容 -->
|
|
45
|
+
<NewChatCardList
|
|
46
|
+
:items="message.items"
|
|
47
|
+
:showLoadMore="message.showLoadMore"
|
|
48
|
+
:page-size="message.pageSize || 10"
|
|
49
|
+
:current-page="message.currentPage || 1"
|
|
50
|
+
:loading="message.loading"
|
|
51
|
+
@item-click="handleCardItemClick"
|
|
52
|
+
@action-click="handleCardActionClick(index, $event)"
|
|
53
|
+
@load-more="handleLoadMore(message)"
|
|
54
|
+
/>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<!-- 推荐信息 -->
|
|
58
|
+
<div v-if="message.suggestions && message.suggestions.length > 0" class="suggestions-container">
|
|
59
|
+
<div class="suggestions-title">推荐:</div>
|
|
60
|
+
<van-tag
|
|
61
|
+
v-for="(suggestion, index) in message.suggestions"
|
|
62
|
+
:key="index"
|
|
63
|
+
class="suggestion-tag"
|
|
64
|
+
@click="() => handleSuggestionClick(suggestion)"
|
|
65
|
+
>
|
|
66
|
+
{{ suggestion }}
|
|
67
|
+
</van-tag>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<!-- 加载状态 -->
|
|
71
|
+
<div v-if="message.role === 'assistant' && message.loading" class="loading-dots">
|
|
72
|
+
<span></span>
|
|
73
|
+
<span></span>
|
|
74
|
+
<span></span>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
<div class="message-time">{{ formatTime(message.timestamp) }}</div>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<!-- 输入区域 -->
|
|
84
|
+
<div class="input-area">
|
|
85
|
+
<div class="input-wrapper">
|
|
86
|
+
<van-field
|
|
87
|
+
v-model="inputText"
|
|
88
|
+
type="textarea"
|
|
89
|
+
rows="1"
|
|
90
|
+
autosize
|
|
91
|
+
placeholder="输入您的问题..."
|
|
92
|
+
:disabled="isLoading"
|
|
93
|
+
@keydown.enter.exact.prevent="handleEnter"
|
|
94
|
+
@keydown.shift.enter.exact="handleShiftEnter"
|
|
95
|
+
@focus="handleInputFocus"
|
|
96
|
+
class="chat-input"
|
|
97
|
+
/>
|
|
98
|
+
<van-button
|
|
99
|
+
type="primary"
|
|
100
|
+
size="small"
|
|
101
|
+
:disabled="!inputText.trim() || isLoading"
|
|
102
|
+
:loading="isLoading"
|
|
103
|
+
@click="sendMessage"
|
|
104
|
+
class="send-btn"
|
|
105
|
+
>
|
|
106
|
+
发送
|
|
107
|
+
</van-button>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
</template>
|
|
112
|
+
|
|
113
|
+
<script setup>
|
|
114
|
+
import { ref, nextTick, onMounted, watch } from 'vue'
|
|
115
|
+
import { showToast } from 'vant'
|
|
116
|
+
import ChatForm from '@/components/ChatForm.vue'
|
|
117
|
+
import NewChatCardList from '@/components/newChatCardList.vue'
|
|
118
|
+
|
|
119
|
+
// 消息列表
|
|
120
|
+
const messages = ref([])
|
|
121
|
+
// 输入文本
|
|
122
|
+
const inputText = ref('')
|
|
123
|
+
// 加载状态
|
|
124
|
+
const isLoading = ref(false)
|
|
125
|
+
// 聊天容器引用
|
|
126
|
+
const chatContainerRef = ref(null)
|
|
127
|
+
// 会话ID
|
|
128
|
+
const sessionId = ref('')
|
|
129
|
+
|
|
130
|
+
// 判断消息类型
|
|
131
|
+
const isTextMessage = (message) => {
|
|
132
|
+
return typeof message.content === 'string'
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const isFormMessage = (message) => {
|
|
136
|
+
return message.type === 'form' && message.formSchema
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const isCardListMessage = (message) => {
|
|
140
|
+
return message.type === 'cardList' && Array.isArray(message.items)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// 初始化欢迎消息
|
|
144
|
+
onMounted(() => {
|
|
145
|
+
addMessage('assistant', '您好!有什么可以帮助您的吗?', false)
|
|
146
|
+
scrollToBottom()
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
// 添加消息
|
|
150
|
+
const addMessage = (role, content, loading = false) => {
|
|
151
|
+
let message = {
|
|
152
|
+
role,
|
|
153
|
+
timestamp: new Date(),
|
|
154
|
+
loading
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// 根据内容类型设置消息属性
|
|
158
|
+
if (typeof content === 'string') {
|
|
159
|
+
// 文本消息
|
|
160
|
+
message.content = content
|
|
161
|
+
} else if (content.type === 'form') {
|
|
162
|
+
// 表单消息
|
|
163
|
+
message.type = 'form'
|
|
164
|
+
message.formSchema = content.schema
|
|
165
|
+
message.formData = content.data || {}
|
|
166
|
+
// 添加介绍文字(如果存在)
|
|
167
|
+
if (content.output) {
|
|
168
|
+
message.output = content.output
|
|
169
|
+
}
|
|
170
|
+
} else if (content.type === 'cardList') {
|
|
171
|
+
// 列表卡片消息
|
|
172
|
+
for(let key in content){
|
|
173
|
+
message[key] = content[key]
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// 为AI助手消息添加推荐信息
|
|
178
|
+
if (role === 'assistant' && !loading) {
|
|
179
|
+
message.suggestions = generateSuggestions(content)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
messages.value.push(message)
|
|
183
|
+
nextTick(() => {
|
|
184
|
+
scrollToBottom()
|
|
185
|
+
})
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// 生成推荐信息
|
|
189
|
+
const generateSuggestions = (content) => {
|
|
190
|
+
// 这里可以根据实际内容生成相关的推荐信息
|
|
191
|
+
// 目前先使用一些示例推荐信息
|
|
192
|
+
const sampleSuggestions = [
|
|
193
|
+
'您还可以询问相关问题',
|
|
194
|
+
'了解更多详细信息',
|
|
195
|
+
'查看相关文档',
|
|
196
|
+
'获取更多帮助'
|
|
197
|
+
]
|
|
198
|
+
|
|
199
|
+
// 根据内容类型生成不同的推荐信息
|
|
200
|
+
if (typeof content === 'string') {
|
|
201
|
+
if (content.includes('时间')) {
|
|
202
|
+
return ['查看时区信息', '设置提醒', '查看日程安排']
|
|
203
|
+
} else if (content.includes('表单')) {
|
|
204
|
+
return ['查看表单填写指南', '了解数据隐私政策', '联系客服']
|
|
205
|
+
} else if (content.includes('产品')) {
|
|
206
|
+
return ['比较不同产品', '查看用户评价', '获取优惠信息']
|
|
207
|
+
}
|
|
208
|
+
} else if (content && content.type === 'form') {
|
|
209
|
+
return ['查看表单填写示例', '了解字段含义', '下载填写模板']
|
|
210
|
+
} else if (content && content.type === 'cardList') {
|
|
211
|
+
return ['筛选产品类别', '按价格排序', '查看更多产品']
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// 默认推荐信息
|
|
215
|
+
return sampleSuggestions.slice(0, 3)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// 发送消息
|
|
219
|
+
const sendMessage = async () => {
|
|
220
|
+
const text = inputText.value.trim()
|
|
221
|
+
if (!text || isLoading.value) return
|
|
222
|
+
|
|
223
|
+
// 添加用户消息
|
|
224
|
+
addMessage('user', text)
|
|
225
|
+
inputText.value = ''
|
|
226
|
+
|
|
227
|
+
// 添加AI加载消息
|
|
228
|
+
addMessage('assistant', '', true)
|
|
229
|
+
isLoading.value = true
|
|
230
|
+
|
|
231
|
+
// 拼装一下请求体
|
|
232
|
+
let requestBody = {
|
|
233
|
+
sessionId: sessionId.value,
|
|
234
|
+
input: text,
|
|
235
|
+
payload: {
|
|
236
|
+
sceneType : "QUERY",
|
|
237
|
+
queryType : "CREATOR",
|
|
238
|
+
processType : "BUSINESS_TRIP",
|
|
239
|
+
queryBackLogReq: {
|
|
240
|
+
userId: '123456',
|
|
241
|
+
pageNum:1,
|
|
242
|
+
pageSize: 10
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
// 模拟AI回复(实际应该调用API)
|
|
249
|
+
await simulateAIResponse(text)
|
|
250
|
+
} catch (error) {
|
|
251
|
+
console.error('发送消息失败:', error)
|
|
252
|
+
showToast('发送失败,请重试')
|
|
253
|
+
// 移除加载中的消息
|
|
254
|
+
messages.value.pop()
|
|
255
|
+
} finally {
|
|
256
|
+
isLoading.value = false
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// 模拟AI回复(实际应该替换为真实的API调用)
|
|
261
|
+
const simulateAIResponse = async (userMessage) => {
|
|
262
|
+
// 模拟网络延迟
|
|
263
|
+
await new Promise(resolve => setTimeout(resolve, 1000 + Math.random() * 1000))
|
|
264
|
+
|
|
265
|
+
// 移除加载中的消息
|
|
266
|
+
const loadingMessage = messages.value.pop()
|
|
267
|
+
|
|
268
|
+
// 生成模拟回复
|
|
269
|
+
let response = ''
|
|
270
|
+
if (userMessage.includes('你好') || userMessage.includes('您好')) {
|
|
271
|
+
response = '您好!很高兴为您服务。'
|
|
272
|
+
} else if (userMessage.includes('帮助') || userMessage.includes('功能')) {
|
|
273
|
+
response = '我可以帮您解答问题、提供建议、分析数据等。请告诉我您需要什么帮助?'
|
|
274
|
+
} else if (userMessage.includes('时间')) {
|
|
275
|
+
response = `当前时间是:${new Date().toLocaleString('zh-CN')}`
|
|
276
|
+
} else if (userMessage.includes('表单')) {
|
|
277
|
+
// 返回表单类型消息,带介绍文字
|
|
278
|
+
response = {
|
|
279
|
+
type: 'form',
|
|
280
|
+
output: '请您填写以下信息,以便我们为您提供更好的服务。所有信息都将严格保密,请放心填写。',
|
|
281
|
+
schema: [
|
|
282
|
+
{
|
|
283
|
+
field: 'name',
|
|
284
|
+
label: '姓名',
|
|
285
|
+
type: 'input',
|
|
286
|
+
placeholder: '请输入您的姓名',
|
|
287
|
+
rules: [{ required: true, message: '请输入姓名' }]
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
field: 'email',
|
|
291
|
+
label: '邮箱',
|
|
292
|
+
type: 'input',
|
|
293
|
+
placeholder: '请输入您的邮箱',
|
|
294
|
+
rules: [{ required: true, message: '请输入邮箱' }, { pattern: /\w+@\w+\.\w+/, message: '邮箱格式不正确' }]
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
field: 'age',
|
|
298
|
+
label: '年龄',
|
|
299
|
+
type: 'input',
|
|
300
|
+
placeholder: '请输入您的年龄'
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
field: 'gender',
|
|
304
|
+
label: '性别',
|
|
305
|
+
type: 'radio',
|
|
306
|
+
options: [
|
|
307
|
+
{ label: '男', value: 'male' },
|
|
308
|
+
{ label: '女', value: 'female' }
|
|
309
|
+
]
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
field: 'hobbies',
|
|
313
|
+
label: '爱好',
|
|
314
|
+
type: 'checkbox',
|
|
315
|
+
options: [
|
|
316
|
+
{ label: '阅读', value: 'reading' },
|
|
317
|
+
{ label: '运动', value: 'sports' },
|
|
318
|
+
{ label: '音乐', value: 'music' },
|
|
319
|
+
{ label: '旅行', value: 'travel' }
|
|
320
|
+
]
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
field: 'bio',
|
|
324
|
+
label: '个人简介',
|
|
325
|
+
type: 'textarea',
|
|
326
|
+
placeholder: '请简单介绍一下自己',
|
|
327
|
+
rows: 3
|
|
328
|
+
}
|
|
329
|
+
]
|
|
330
|
+
}
|
|
331
|
+
} else if (userMessage.includes('列表') || userMessage.includes('卡片')) {
|
|
332
|
+
// 返回列表卡片类型消息,带介绍文字
|
|
333
|
+
// TODO 生成这种卡片列表的时候,要初始化一个这张卡片的独有的page,用来做分页
|
|
334
|
+
const items = [
|
|
335
|
+
{
|
|
336
|
+
title: '产品A',
|
|
337
|
+
subtitle: '高性能计算服务',
|
|
338
|
+
statusName : '待审',
|
|
339
|
+
creator : '刘松煜',
|
|
340
|
+
createTime : '2023-10-01 10:00:00',
|
|
341
|
+
currentNode : '节点1',
|
|
342
|
+
dpName : '信息科技部',
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
title: '产品B',
|
|
346
|
+
subtitle: '云存储解决方案',
|
|
347
|
+
statusName : '驳回',
|
|
348
|
+
creator : '刘松煜',
|
|
349
|
+
createTime : '2023-10-01 10:00:00',
|
|
350
|
+
currentNode : '节点1',
|
|
351
|
+
dpName : '固定收益投资部',
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
title: '信息科技部xxxaaaa啊啊啊你还是低哦是大家扫IDiOS滴哦啊啊大家送啊点喝酒哦洒几滴撒娇都i爱睡觉',
|
|
355
|
+
subtitle: '人工智能平台',
|
|
356
|
+
statusName : '待审',
|
|
357
|
+
creator : '刘松煜',
|
|
358
|
+
createTime : '2023-10-01',
|
|
359
|
+
currentNode : '节点1',
|
|
360
|
+
dpName : '信息科技部',
|
|
361
|
+
}
|
|
362
|
+
];
|
|
363
|
+
const total = 10
|
|
364
|
+
|
|
365
|
+
response = {
|
|
366
|
+
type: 'cardList',
|
|
367
|
+
output: '根据您的需求,我们为您推荐以下几款热门产品,您可以点击查看详细信息或进行相关操作:',
|
|
368
|
+
showLoadMore: items.length < total, // 根据当前items长度与总数的比较来确定是否显示"查看更多"
|
|
369
|
+
currentPage: 1,
|
|
370
|
+
pageSize: 10,
|
|
371
|
+
total: total,
|
|
372
|
+
items: items,
|
|
373
|
+
loading: false // 初始化加载状态
|
|
374
|
+
}
|
|
375
|
+
} else {
|
|
376
|
+
response = `我理解您说的是:"${userMessage}"。这是一个很好的问题,让我为您详细解答...\n\n(这是模拟回复,实际使用时需要接入真实的AI API)`
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// 添加AI回复
|
|
380
|
+
addMessage('assistant', response, false)
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// 格式化消息内容(支持换行)
|
|
384
|
+
const formatMessage = (content) => {
|
|
385
|
+
if (!content) return ''
|
|
386
|
+
return content.replace(/\n/g, '<br>')
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// 格式化时间
|
|
390
|
+
const formatTime = (timestamp) => {
|
|
391
|
+
if (!timestamp) return ''
|
|
392
|
+
const date = new Date(timestamp)
|
|
393
|
+
const now = new Date()
|
|
394
|
+
const diff = now - date
|
|
395
|
+
|
|
396
|
+
// 今天
|
|
397
|
+
if (diff < 24 * 60 * 60 * 1000 && date.getDate() === now.getDate()) {
|
|
398
|
+
return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
|
|
399
|
+
}
|
|
400
|
+
// 昨天
|
|
401
|
+
if (diff < 48 * 60 * 60 * 1000) {
|
|
402
|
+
return `昨天 ${date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })}`
|
|
403
|
+
}
|
|
404
|
+
// 更早
|
|
405
|
+
return date.toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// 滚动到底部
|
|
409
|
+
const scrollToBottom = () => {
|
|
410
|
+
nextTick(() => {
|
|
411
|
+
if (chatContainerRef.value) {
|
|
412
|
+
chatContainerRef.value.scrollTop = chatContainerRef.value.scrollHeight
|
|
413
|
+
}
|
|
414
|
+
})
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// 监听消息变化,自动滚动
|
|
418
|
+
watch(() => messages.value.length, () => {
|
|
419
|
+
scrollToBottom()
|
|
420
|
+
})
|
|
421
|
+
|
|
422
|
+
// 处理输入框聚焦事件
|
|
423
|
+
const handleInputFocus = (e) => {
|
|
424
|
+
// scrollToBottom()
|
|
425
|
+
setTimeout(() => {
|
|
426
|
+
e.target.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
427
|
+
}, 300);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// 处理Enter键(发送)
|
|
431
|
+
const handleEnter = () => {
|
|
432
|
+
if (!isLoading.value && inputText.value.trim()) {
|
|
433
|
+
sendMessage()
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// 处理Shift+Enter(换行)
|
|
438
|
+
const handleShiftEnter = () => {
|
|
439
|
+
// 允许换行,不做处理
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// 处理表单提交
|
|
443
|
+
const handleFormSubmit = (index, formData) => {
|
|
444
|
+
// 在实际应用中,这里应该将表单数据发送到服务器
|
|
445
|
+
console.log('表单提交数据:', formData)
|
|
446
|
+
|
|
447
|
+
// 添加确认消息
|
|
448
|
+
addMessage('assistant', `感谢您提交的信息!我们已经收到以下数据:\n${JSON.stringify(formData, null, 2)}`)
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// 处理卡片项点击,跳转至详情页
|
|
452
|
+
const handleCardItemClick = (item) => {
|
|
453
|
+
console.log('卡片项被点击:', item)
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// 处理卡片操作点击
|
|
457
|
+
const handleCardActionClick = (index, { action, item, index: itemIndex }) => {
|
|
458
|
+
console.log('卡片操作被点击:', action, item, itemIndex)
|
|
459
|
+
addMessage('assistant', `您对"${item.title}"执行了"${action.text}"操作`)
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// 处理推荐信息点击
|
|
463
|
+
const handleSuggestionClick = (suggestion) => {
|
|
464
|
+
console.log('Suggestion clicked:', suggestion)
|
|
465
|
+
// 将推荐信息作为用户消息发送
|
|
466
|
+
inputText.value = suggestion
|
|
467
|
+
sendMessage()
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// 处理加载更多卡片
|
|
471
|
+
const handleLoadMore = (message) => {
|
|
472
|
+
console.log('加载更多卡片:', message)
|
|
473
|
+
// 设置加载状态
|
|
474
|
+
message.loading = true;
|
|
475
|
+
|
|
476
|
+
// 在实际应用中,这里应该从服务器获取更多数据
|
|
477
|
+
// 示例实现:
|
|
478
|
+
// 1. 更新当前页码
|
|
479
|
+
// 2. 发起API请求获取下一页数据
|
|
480
|
+
// 3. 将新数据追加到现有items中
|
|
481
|
+
|
|
482
|
+
// 模拟异步加载
|
|
483
|
+
setTimeout(() => {
|
|
484
|
+
// 模拟新增的数据
|
|
485
|
+
const newItems = [
|
|
486
|
+
{
|
|
487
|
+
title: '新产品C',
|
|
488
|
+
subtitle: '大数据分析平台',
|
|
489
|
+
statusName : '待审',
|
|
490
|
+
creator : '刘松煜',
|
|
491
|
+
createTime : '2023-10-02 09:30:00',
|
|
492
|
+
currentNode : '节点2',
|
|
493
|
+
dpName : '信息科技部',
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
title: '新产品D',
|
|
497
|
+
subtitle: '区块链解决方案',
|
|
498
|
+
statusName : '草稿',
|
|
499
|
+
creator : '刘松煜',
|
|
500
|
+
createTime : '2023-10-02 11:45:00',
|
|
501
|
+
currentNode : '节点1',
|
|
502
|
+
dpName : '固定收益投资部',
|
|
503
|
+
}
|
|
504
|
+
];
|
|
505
|
+
|
|
506
|
+
// 将新数据添加到消息的items中
|
|
507
|
+
message.items = [...message.items, ...newItems];
|
|
508
|
+
|
|
509
|
+
// 如果有分页信息,也需要更新
|
|
510
|
+
if (message.currentPage !== undefined) {
|
|
511
|
+
message.currentPage += 1;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// 更新showLoadMore状态,根据当前items长度与总数的比较
|
|
515
|
+
if (message.total !== undefined) {
|
|
516
|
+
message.showLoadMore = message.items.length < message.total;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// 取消加载状态
|
|
520
|
+
message.loading = false;
|
|
521
|
+
}, 1000);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
</script>
|
|
525
|
+
|
|
526
|
+
<style lang="scss" scoped>
|
|
527
|
+
.ai-chat-page {
|
|
528
|
+
display: flex;
|
|
529
|
+
flex-direction: column;
|
|
530
|
+
height: 100%;
|
|
531
|
+
background-color: #f5f5f5;
|
|
532
|
+
position: relative;
|
|
533
|
+
}
|
|
534
|
+
.nav-bar{
|
|
535
|
+
position: fixed;
|
|
536
|
+
top: 0;
|
|
537
|
+
left: 0;
|
|
538
|
+
right: 0;
|
|
539
|
+
z-index: 1000;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
.chat-container {
|
|
543
|
+
// flex: 1;
|
|
544
|
+
overflow-y: auto;
|
|
545
|
+
padding: 16px;
|
|
546
|
+
padding-bottom: 80px;
|
|
547
|
+
-webkit-overflow-scrolling: touch;
|
|
548
|
+
position: absolute;
|
|
549
|
+
top: 46px;
|
|
550
|
+
left: 0;
|
|
551
|
+
right: 0;
|
|
552
|
+
bottom: 63px;
|
|
553
|
+
|
|
554
|
+
.messages-wrapper {
|
|
555
|
+
display: flex;
|
|
556
|
+
flex-direction: column;
|
|
557
|
+
gap: 16px;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
.message-item {
|
|
562
|
+
display: flex;
|
|
563
|
+
gap: 12px;
|
|
564
|
+
animation: fadeIn 0.3s ease-in;
|
|
565
|
+
|
|
566
|
+
&.user-message {
|
|
567
|
+
flex-direction: row-reverse;
|
|
568
|
+
|
|
569
|
+
.message-content {
|
|
570
|
+
align-items: flex-end;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
.message-bubble {
|
|
574
|
+
background: linear-gradient(135deg, #1989fa 0%, #0d7ce8 100%);
|
|
575
|
+
color: #fff;
|
|
576
|
+
border-radius: 18px 18px 4px 18px;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
.message-avatar {
|
|
580
|
+
background: linear-gradient(135deg, #1989fa 0%, #0d7ce8 100%);
|
|
581
|
+
color: #fff;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
&.ai-message {
|
|
586
|
+
.message-bubble {
|
|
587
|
+
background: #fff;
|
|
588
|
+
color: #333;
|
|
589
|
+
border-radius: 18px 18px 18px 4px;
|
|
590
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
.message-avatar {
|
|
594
|
+
background: #fff;
|
|
595
|
+
color: #1989fa;
|
|
596
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
.message-avatar {
|
|
602
|
+
width: 36px;
|
|
603
|
+
height: 36px;
|
|
604
|
+
border-radius: 50%;
|
|
605
|
+
display: flex;
|
|
606
|
+
align-items: center;
|
|
607
|
+
justify-content: center;
|
|
608
|
+
flex-shrink: 0;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
.message-content {
|
|
612
|
+
display: flex;
|
|
613
|
+
flex-direction: column;
|
|
614
|
+
// max-width: 75%;
|
|
615
|
+
gap: 6px;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
.message-bubble {
|
|
619
|
+
padding: 12px 16px;
|
|
620
|
+
word-wrap: break-word;
|
|
621
|
+
word-break: break-word;
|
|
622
|
+
line-height: 1.5;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
.message-text {
|
|
626
|
+
font-size: 15px;
|
|
627
|
+
white-space: pre-wrap;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
.message-output {
|
|
631
|
+
font-size: 14px;
|
|
632
|
+
color: #666;
|
|
633
|
+
line-height: 1.5;
|
|
634
|
+
margin-bottom: 12px;
|
|
635
|
+
padding: 8px 12px;
|
|
636
|
+
background-color: #f8f9fa;
|
|
637
|
+
border-radius: 8px;
|
|
638
|
+
// border-left: 3px solid #1989fa;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
.message-time {
|
|
642
|
+
font-size: 11px;
|
|
643
|
+
color: #999;
|
|
644
|
+
padding: 0 4px;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
.loading-dots {
|
|
648
|
+
display: flex;
|
|
649
|
+
gap: 4px;
|
|
650
|
+
padding: 8px 0 4px;
|
|
651
|
+
|
|
652
|
+
span {
|
|
653
|
+
width: 6px;
|
|
654
|
+
height: 6px;
|
|
655
|
+
border-radius: 50%;
|
|
656
|
+
background-color: #1989fa;
|
|
657
|
+
animation: bounce 1.4s infinite ease-in-out both;
|
|
658
|
+
|
|
659
|
+
&:nth-child(1) {
|
|
660
|
+
animation-delay: -0.32s;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
&:nth-child(2) {
|
|
664
|
+
animation-delay: -0.16s;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
@keyframes bounce {
|
|
670
|
+
0%, 80%, 100% {
|
|
671
|
+
transform: scale(0);
|
|
672
|
+
opacity: 0.5;
|
|
673
|
+
}
|
|
674
|
+
40% {
|
|
675
|
+
transform: scale(1);
|
|
676
|
+
opacity: 1;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
@keyframes fadeIn {
|
|
681
|
+
from {
|
|
682
|
+
opacity: 0;
|
|
683
|
+
transform: translateY(10px);
|
|
684
|
+
}
|
|
685
|
+
to {
|
|
686
|
+
opacity: 1;
|
|
687
|
+
transform: translateY(0);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
.input-area {
|
|
692
|
+
position: fixed;
|
|
693
|
+
bottom: 0;
|
|
694
|
+
left: 0;
|
|
695
|
+
right: 0;
|
|
696
|
+
background: #fff;
|
|
697
|
+
border-top: 1px solid #ebedf0;
|
|
698
|
+
padding: 8px 12px;
|
|
699
|
+
padding-bottom: calc(8px + env(safe-area-inset-bottom));
|
|
700
|
+
z-index: 100;
|
|
701
|
+
|
|
702
|
+
.input-wrapper {
|
|
703
|
+
display: flex;
|
|
704
|
+
align-items: flex-end;
|
|
705
|
+
gap: 8px;
|
|
706
|
+
max-width: 100%;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
.chat-input {
|
|
710
|
+
flex: 1;
|
|
711
|
+
background: #f7f8fa;
|
|
712
|
+
border-radius: 20px;
|
|
713
|
+
padding: 8px 12px;
|
|
714
|
+
min-height: 40px;
|
|
715
|
+
max-height: 120px;
|
|
716
|
+
|
|
717
|
+
:deep(.van-field__control) {
|
|
718
|
+
font-size: 15px;
|
|
719
|
+
line-height: 1.5;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
.send-btn {
|
|
724
|
+
flex-shrink: 0;
|
|
725
|
+
height: 40px;
|
|
726
|
+
padding: 0 20px;
|
|
727
|
+
border-radius: 20px;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
|
|
732
|
+
/* 推荐信息样式 */
|
|
733
|
+
.suggestions-container {
|
|
734
|
+
margin-top: 12px;
|
|
735
|
+
padding-top: 12px;
|
|
736
|
+
border-top: 1px dashed #e0e0e0;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
.suggestions-title {
|
|
740
|
+
font-size: 12px;
|
|
741
|
+
color: #666;
|
|
742
|
+
margin-bottom: 8px;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
.suggestion-tag {
|
|
746
|
+
margin-right: 8px;
|
|
747
|
+
margin-bottom: 8px;
|
|
748
|
+
background-color: #f0f0f0;
|
|
749
|
+
border-color: #ddd;
|
|
750
|
+
color: #666;
|
|
751
|
+
cursor: pointer;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
.suggestion-tag:hover {
|
|
755
|
+
background-color: #e0e0e0;
|
|
756
|
+
}
|
|
757
|
+
</style>
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="chat-card-list">
|
|
3
|
+
<div
|
|
4
|
+
v-for="item in items"
|
|
5
|
+
:key="item.title"
|
|
6
|
+
class="chat-card"
|
|
7
|
+
@click="handleClick(item)"
|
|
8
|
+
>
|
|
9
|
+
<div class="title" v-if="item.title">{{ item.title }}</div>
|
|
10
|
+
<div class="card-header">
|
|
11
|
+
<div class="left-content">
|
|
12
|
+
<div class="subtitle" v-if="item.subtitle">{{ item.subtitle }}</div>
|
|
13
|
+
</div>
|
|
14
|
+
<div class="right-content">
|
|
15
|
+
<van-tag :type="statusType(item.statusName)">{{ item.statusName }}</van-tag>
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
<div class="card-info" v-if="item.creator || item.createTime">
|
|
19
|
+
<span class="creator" v-if="item.creator">创建人:{{ item.creator }}</span>
|
|
20
|
+
<span class="time" v-if="item.dpName">部门:{{ item.dpName }}</span>
|
|
21
|
+
</div>
|
|
22
|
+
<div class="depart" v-if="item.createTime">
|
|
23
|
+
创建日期:{{ item.createTime }}
|
|
24
|
+
</div>
|
|
25
|
+
<div class="process-node" v-if="item.currentNode">
|
|
26
|
+
当前流程:{{ item.currentNode }}
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
<div class="load-more" v-if="showLoadMore && !loading" @click="loadMore">
|
|
30
|
+
查看更多
|
|
31
|
+
</div>
|
|
32
|
+
<div class="loading" v-if="loading">
|
|
33
|
+
加载中...
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
</template>
|
|
38
|
+
|
|
39
|
+
<script setup>
|
|
40
|
+
import { defineProps, defineEmits, computed } from 'vue'
|
|
41
|
+
const props = defineProps({
|
|
42
|
+
// 聊天卡片列表
|
|
43
|
+
items: {
|
|
44
|
+
type: Array,
|
|
45
|
+
required: true,
|
|
46
|
+
default: () => []
|
|
47
|
+
},
|
|
48
|
+
// 是否显示"查看更多"按钮
|
|
49
|
+
showLoadMore: {
|
|
50
|
+
type: Boolean,
|
|
51
|
+
default: false
|
|
52
|
+
},
|
|
53
|
+
// 每页显示的卡片数量
|
|
54
|
+
pageSize: {
|
|
55
|
+
type: Number,
|
|
56
|
+
default: 10
|
|
57
|
+
},
|
|
58
|
+
// 当前页码
|
|
59
|
+
currentPage: {
|
|
60
|
+
type: Number,
|
|
61
|
+
default: 1
|
|
62
|
+
},
|
|
63
|
+
// 是否正在加载
|
|
64
|
+
loading: {
|
|
65
|
+
type: Boolean,
|
|
66
|
+
default: false
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
console.log(props);
|
|
70
|
+
|
|
71
|
+
const emit = defineEmits(['item-click', 'load-more'])
|
|
72
|
+
|
|
73
|
+
const statusType = (statusName) => {
|
|
74
|
+
if (['待办','待审'].includes(statusName)) {
|
|
75
|
+
return 'primary'
|
|
76
|
+
} else if (['草稿'].includes(statusName)) {
|
|
77
|
+
return 'info'
|
|
78
|
+
} else if (['已完成'].includes(statusName)) {
|
|
79
|
+
return 'success'
|
|
80
|
+
} else if (['驳回'].includes(statusName)) {
|
|
81
|
+
return 'danger'
|
|
82
|
+
} else {
|
|
83
|
+
return 'default'
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const handleClick = (item) => {
|
|
88
|
+
emit('item-click', item)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const loadMore = () => {
|
|
92
|
+
emit('load-more')
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
</script>
|
|
96
|
+
|
|
97
|
+
<style lang='scss' scoped>
|
|
98
|
+
|
|
99
|
+
.chat-card{
|
|
100
|
+
background: #f7f7f9;
|
|
101
|
+
border-radius: 12px;
|
|
102
|
+
overflow: hidden;
|
|
103
|
+
padding: 12px;
|
|
104
|
+
box-shadow: 2px 4px 12px rgba(0, 0, 0, 0.1);
|
|
105
|
+
transition: all 0.3s ease;
|
|
106
|
+
margin-bottom: 16px;
|
|
107
|
+
&:active {
|
|
108
|
+
transform: scale(0.98);
|
|
109
|
+
}
|
|
110
|
+
.title {
|
|
111
|
+
font-size: 16px;
|
|
112
|
+
font-weight: 500;
|
|
113
|
+
color: #2c3e50;
|
|
114
|
+
margin-bottom: 4px;
|
|
115
|
+
line-height: 1.4;
|
|
116
|
+
display: -webkit-box;
|
|
117
|
+
-webkit-box-orient: vertical;
|
|
118
|
+
-webkit-line-clamp: 2;
|
|
119
|
+
overflow: hidden;
|
|
120
|
+
text-overflow: ellipsis;
|
|
121
|
+
|
|
122
|
+
}
|
|
123
|
+
.card-header{
|
|
124
|
+
display: flex;
|
|
125
|
+
justify-content: space-between;
|
|
126
|
+
align-items: flex-end;
|
|
127
|
+
.left-content{
|
|
128
|
+
flex:1;
|
|
129
|
+
.subtitle {
|
|
130
|
+
font-size: 13px;
|
|
131
|
+
color: #7f8c8d;
|
|
132
|
+
line-height: 1.4;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
.right-content{
|
|
136
|
+
width: 50px;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.card-info {
|
|
141
|
+
display: flex;
|
|
142
|
+
justify-content: space-between;
|
|
143
|
+
border-top: 1px solid #e1e8ed;
|
|
144
|
+
font-size: 12px;
|
|
145
|
+
color: #555;
|
|
146
|
+
margin-top: 8px;
|
|
147
|
+
padding: 4px 0;
|
|
148
|
+
|
|
149
|
+
.creator, .time {
|
|
150
|
+
flex: 1;
|
|
151
|
+
overflow: hidden;
|
|
152
|
+
white-space: nowrap;
|
|
153
|
+
text-overflow: ellipsis;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.creator {
|
|
157
|
+
margin-right: 10px;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.process-node {
|
|
162
|
+
font-size: 12px;
|
|
163
|
+
border-radius: 4px;
|
|
164
|
+
padding: 4px 0px;
|
|
165
|
+
max-width: calc(100% - 24px);
|
|
166
|
+
overflow: hidden;
|
|
167
|
+
white-space: nowrap;
|
|
168
|
+
text-overflow: ellipsis;
|
|
169
|
+
}
|
|
170
|
+
.depart {
|
|
171
|
+
font-size: 12px;
|
|
172
|
+
border-radius: 4px;
|
|
173
|
+
padding: 4px 0px;
|
|
174
|
+
max-width: calc(100% - 24px);
|
|
175
|
+
overflow: hidden;
|
|
176
|
+
white-space: nowrap;
|
|
177
|
+
text-overflow: ellipsis;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
.load-more {
|
|
181
|
+
text-align: center;
|
|
182
|
+
padding: 12px 0;
|
|
183
|
+
color: #1989fa;
|
|
184
|
+
font-size: 14px;
|
|
185
|
+
border-radius: 8px;
|
|
186
|
+
margin: 16px 0;
|
|
187
|
+
cursor: pointer;
|
|
188
|
+
user-select: none;
|
|
189
|
+
|
|
190
|
+
&:active {
|
|
191
|
+
opacity: 0.8;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.loading {
|
|
196
|
+
text-align: center;
|
|
197
|
+
padding: 12px 0;
|
|
198
|
+
color: #999;
|
|
199
|
+
font-size: 14px;
|
|
200
|
+
margin: 16px 0;
|
|
201
|
+
}
|
|
202
|
+
</style>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
### 前端模块
|
|
2
|
+
1、表单模块
|
|
3
|
+
前端封装一个json表单,根据json内容生产对应表单项
|
|
4
|
+
例如:
|
|
5
|
+
```js
|
|
6
|
+
[
|
|
7
|
+
{
|
|
8
|
+
type: 'input',
|
|
9
|
+
field: 'title',
|
|
10
|
+
value: ''
|
|
11
|
+
label: '公文标题',
|
|
12
|
+
placeholder: '请输入标题',
|
|
13
|
+
rules: [
|
|
14
|
+
{ required: true, message: '请输入标题' }
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
type: 'select',
|
|
19
|
+
field: 'category',
|
|
20
|
+
value: 'notice'
|
|
21
|
+
label: '公文类型',
|
|
22
|
+
placeholder: '请选择类型',
|
|
23
|
+
options: [
|
|
24
|
+
{ label: '通知', value: 'notice' },
|
|
25
|
+
{ label: '通报', value: 'circular' },
|
|
26
|
+
{ label: '请示', value: 'request' }
|
|
27
|
+
],
|
|
28
|
+
rules: [{ required: true, message: '请选择类型' }]
|
|
29
|
+
},
|
|
30
|
+
]
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
2、卡片模块
|