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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vue_zhongyou",
3
- "version": "1.0.7",
3
+ "version": "1.0.8",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "keywords": [],
@@ -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>
@@ -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、卡片模块