|
|
|
|
@ -40,6 +40,11 @@
|
|
|
|
|
<span class="event-content">{{ scope.row.event }}</span>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column prop="location" label="地点" width="140">
|
|
|
|
|
<template #default="scope">
|
|
|
|
|
<span>{{ scope.row.location || '-' }}</span>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column label="操作" width="90">
|
|
|
|
|
<template #default="scope">
|
|
|
|
|
<el-button size="small" type="text" @click="editSchedule(scope.$index)">编辑</el-button>
|
|
|
|
|
@ -62,6 +67,11 @@
|
|
|
|
|
<el-form-item label="时间">
|
|
|
|
|
<el-time-picker v-model="scheduleForm.time" value-format="HH:mm" placeholder="选择时间" style="width: 100%" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<el-form-item label="地点">
|
|
|
|
|
<el-select v-model="scheduleForm.location" placeholder="选择地点" style="width: 100%">
|
|
|
|
|
<el-option v-for="loc in communityLocations" :key="loc" :label="loc" :value="loc" />
|
|
|
|
|
</el-select>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<el-form-item label="活动">
|
|
|
|
|
<el-input v-model="scheduleForm.event" placeholder="活动内容" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
@ -82,35 +92,69 @@
|
|
|
|
|
<script setup>
|
|
|
|
|
import { ref, watch, computed } from 'vue'
|
|
|
|
|
import { ElMessage } from 'element-plus'
|
|
|
|
|
import { updateOC } from '../stores/ocStore.js'
|
|
|
|
|
import { updateOC, loadOCFromStorage, getOCById } from '../stores/ocStore.js'
|
|
|
|
|
import { currentUser } from '../stores/userStore.js'
|
|
|
|
|
import { useRouter } from 'vue-router'
|
|
|
|
|
const props = defineProps({
|
|
|
|
|
modelValue: Boolean,
|
|
|
|
|
oc: Object
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 社区地点白名单(与开发日志一致)
|
|
|
|
|
const communityLocations = [
|
|
|
|
|
'超市','饭店','电影院','商场','公寓','甜品店','植物园','公园','体育馆','娱乐休闲区','宠物店'
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
// 日程表相关
|
|
|
|
|
const scheduleList = ref([])
|
|
|
|
|
const scheduleDialogVisible = ref(false)
|
|
|
|
|
const scheduleForm = ref({ date: '', time: '', event: '' })
|
|
|
|
|
const scheduleForm = ref({ date: '', time: '', event: '', location: '' })
|
|
|
|
|
const editingIndex = ref(-1)
|
|
|
|
|
|
|
|
|
|
// 监听OC变化,刷新日程
|
|
|
|
|
watch(() => props.oc, (oc) => {
|
|
|
|
|
if (oc && Array.isArray(oc.schedule)) {
|
|
|
|
|
// 排序,最新在前
|
|
|
|
|
scheduleList.value = [...oc.schedule].sort((a, b) => {
|
|
|
|
|
const t1 = new Date(a.date + 'T' + (a.time || '00:00')).getTime()
|
|
|
|
|
const t2 = new Date(b.date + 'T' + (b.time || '00:00')).getTime()
|
|
|
|
|
return t2 - t1
|
|
|
|
|
})
|
|
|
|
|
// 排序,按日期+时间升序(早到晚)
|
|
|
|
|
scheduleList.value = [...oc.schedule]
|
|
|
|
|
sortSchedule(scheduleList.value)
|
|
|
|
|
} else {
|
|
|
|
|
scheduleList.value = []
|
|
|
|
|
}
|
|
|
|
|
}, { immediate: true })
|
|
|
|
|
|
|
|
|
|
// 当弹窗打开时,从 storage 刷新 oc 列表并读取最新的 oc 数据,保证日程与最近一次生成内容同步
|
|
|
|
|
watch(() => props.modelValue, (val) => {
|
|
|
|
|
if (val) {
|
|
|
|
|
// 尝试用当前登录用户的键读取
|
|
|
|
|
try {
|
|
|
|
|
const username = currentUser?.value?.username || null
|
|
|
|
|
if (username) loadOCFromStorage(username)
|
|
|
|
|
} catch (e) {
|
|
|
|
|
// ignore
|
|
|
|
|
}
|
|
|
|
|
if (props.oc && props.oc.id) {
|
|
|
|
|
const fresh = getOCById(props.oc.id)
|
|
|
|
|
if (fresh && Array.isArray(fresh.schedule)) {
|
|
|
|
|
scheduleList.value = [...fresh.schedule]
|
|
|
|
|
sortSchedule(scheduleList.value)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}, { immediate: false })
|
|
|
|
|
|
|
|
|
|
// 将日程按 date+time 升序排序(就地排序或对数组副本生效)
|
|
|
|
|
function sortSchedule(list) {
|
|
|
|
|
if (!Array.isArray(list)) return list
|
|
|
|
|
list.sort((a, b) => {
|
|
|
|
|
const t1 = new Date((a.date || '') + 'T' + (a.time || '00:00')).getTime()
|
|
|
|
|
const t2 = new Date((b.date || '') + 'T' + (b.time || '00:00')).getTime()
|
|
|
|
|
return (isNaN(t1) ? 0 : t1) - (isNaN(t2) ? 0 : t2)
|
|
|
|
|
})
|
|
|
|
|
return list
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function openScheduleDialog() {
|
|
|
|
|
scheduleForm.value = { date: '', time: '', event: '' }
|
|
|
|
|
scheduleForm.value = { date: '', time: '', event: '', location: communityLocations[0] || '' }
|
|
|
|
|
editingIndex.value = -1
|
|
|
|
|
scheduleDialogVisible.value = true
|
|
|
|
|
}
|
|
|
|
|
@ -120,47 +164,148 @@ function editSchedule(idx) {
|
|
|
|
|
scheduleDialogVisible.value = true
|
|
|
|
|
}
|
|
|
|
|
function saveSchedule() {
|
|
|
|
|
if (!scheduleForm.value.date || !scheduleForm.value.time || !scheduleForm.value.event) {
|
|
|
|
|
if (!scheduleForm.value.date || !scheduleForm.value.time || !scheduleForm.value.event || !scheduleForm.value.location) {
|
|
|
|
|
ElMessage.warning('请填写完整信息')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if (editingIndex.value === -1) {
|
|
|
|
|
scheduleList.value.unshift({ ...scheduleForm.value })
|
|
|
|
|
scheduleList.value.push({ ...scheduleForm.value })
|
|
|
|
|
} else {
|
|
|
|
|
scheduleList.value[editingIndex.value] = { ...scheduleForm.value }
|
|
|
|
|
}
|
|
|
|
|
// 保持升序
|
|
|
|
|
sortSchedule(scheduleList.value)
|
|
|
|
|
updateScheduleToOC()
|
|
|
|
|
scheduleDialogVisible.value = false
|
|
|
|
|
}
|
|
|
|
|
function deleteSchedule(idx) {
|
|
|
|
|
scheduleList.value.splice(idx, 1)
|
|
|
|
|
// 已删除,保持升序并同步
|
|
|
|
|
sortSchedule(scheduleList.value)
|
|
|
|
|
updateScheduleToOC()
|
|
|
|
|
}
|
|
|
|
|
function updateScheduleToOC() {
|
|
|
|
|
// 更新到OC对象并持久化
|
|
|
|
|
if (props.oc && props.oc.id) {
|
|
|
|
|
// 排序后同步
|
|
|
|
|
updateOC(props.oc.id, { schedule: scheduleList.value })
|
|
|
|
|
// 排序后同步并持久化(使用当前登录用户的存储键)
|
|
|
|
|
const username = currentUser?.value?.username || null
|
|
|
|
|
updateOC(props.oc.id, { schedule: scheduleList.value }, username)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
function generateScheduleAI() {
|
|
|
|
|
// 更丰富的AI生成逻辑
|
|
|
|
|
async function generateScheduleAI() {
|
|
|
|
|
if (!props.oc) return
|
|
|
|
|
const today = new Date()
|
|
|
|
|
const dateStr = today.getFullYear() + '-' + String(today.getMonth()+1).padStart(2,'0') + '-' + String(today.getDate()).padStart(2,'0')
|
|
|
|
|
const base = []
|
|
|
|
|
if (props.oc.occupation) base.push({ time: '09:00', event: `专注于${props.oc.occupation}相关工作` })
|
|
|
|
|
if (props.oc.hobbies && props.oc.hobbies.length) {
|
|
|
|
|
const hobbies = Array.isArray(props.oc.hobbies) ? props.oc.hobbies : [props.oc.hobbies]
|
|
|
|
|
hobbies.slice(0,2).forEach((h, i) => {
|
|
|
|
|
base.push({ time: i === 0 ? '14:00' : '16:00', event: `参与${h}活动` })
|
|
|
|
|
const API_KEY = import.meta.env.VITE_ZHIPU_API_KEY
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// 获取当前日期
|
|
|
|
|
const today = new Date()
|
|
|
|
|
const dateStr = today.getFullYear() + '-' +
|
|
|
|
|
String(today.getMonth()+1).padStart(2,'0') + '-' +
|
|
|
|
|
String(today.getDate()).padStart(2,'0')
|
|
|
|
|
|
|
|
|
|
// 获取星期几
|
|
|
|
|
const weekDays = ['日', '一', '二', '三', '四', '五', '六']
|
|
|
|
|
const weekDay = weekDays[today.getDay()]
|
|
|
|
|
|
|
|
|
|
// 构造更明确的prompt,包含今天的日期信息
|
|
|
|
|
let persona = `今天是${today.getFullYear()}年${today.getMonth()+1}月${today.getDate()}日,星期${weekDay}。`
|
|
|
|
|
persona += `请根据以下OC信息为其生成与现实真实时间一致的今日(${dateStr})日程安排,时间段尽量分散,包含工作/爱好/社交/休息等合理内容。`
|
|
|
|
|
persona += `返回纯JSON数组,不要任何说明或代码块。` + '\n\n'
|
|
|
|
|
|
|
|
|
|
persona += `OC信息:` + '\n'
|
|
|
|
|
persona += `- 姓名:${props.oc.name || '未知'}` + '\n'
|
|
|
|
|
if (props.oc.occupation) persona += `- 职业:${props.oc.occupation}` + '\n'
|
|
|
|
|
if (props.oc.hobbies) persona += `- 爱好:${Array.isArray(props.oc.hobbies) ? props.oc.hobbies.join('、') : props.oc.hobbies}` + '\n'
|
|
|
|
|
if (props.oc.personality) persona += `- 性格:${Array.isArray(props.oc.personality) ? props.oc.personality.join('、') : props.oc.personality}` + '\n'
|
|
|
|
|
if (props.oc.age) persona += `- 年龄:${props.oc.age}` + '\n'
|
|
|
|
|
|
|
|
|
|
persona += `\n地点白名单:${communityLocations.join('、')}` + '\n\n'
|
|
|
|
|
|
|
|
|
|
persona += `要求:` + '\n'
|
|
|
|
|
persona += `1. 日程必须基于今天的日期:${dateStr}` + '\n'
|
|
|
|
|
persona += `2. 时间安排要合理,符合现实生活规律` + '\n'
|
|
|
|
|
persona += `3. 活动内容要符合OC的职业、爱好和性格` + '\n'
|
|
|
|
|
persona += `4. 每个活动必须包含地点,且只能使用白名单中的地点` + '\n\n'
|
|
|
|
|
|
|
|
|
|
persona += `输出格式示例:[{"date":"${dateStr}","time":"09:00","event":"示例活动","location":"超市"}, {...}]` + '\n'
|
|
|
|
|
persona += `时间格式为HH:MM,日期格式为YYYY-MM-DD。返回纯JSON数组。`
|
|
|
|
|
// Call AI
|
|
|
|
|
let aiResp = null
|
|
|
|
|
if (!API_KEY) {
|
|
|
|
|
throw new Error('AI API_KEY 未配置')
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch('https://open.bigmodel.cn/api/paas/v4/chat/completions', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: {
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
'Authorization': `Bearer ${API_KEY}`
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
model: 'glm-4-flash',
|
|
|
|
|
messages: [ { role: 'user', content: persona } ],
|
|
|
|
|
max_tokens: 512,
|
|
|
|
|
temperature: 0.8
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
const data = await res.json()
|
|
|
|
|
aiResp = data.choices?.[0]?.message?.content || ''
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('AI调用失败:', e)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let parsed = false
|
|
|
|
|
let result = []
|
|
|
|
|
if (aiResp) {
|
|
|
|
|
let str = aiResp.replace(/^```json|```$/g, '').trim()
|
|
|
|
|
// 提取第一个JSON数组
|
|
|
|
|
const m = str.match(/\[[\s\S]*\]/)
|
|
|
|
|
let candidate = m ? m[0] : str
|
|
|
|
|
try {
|
|
|
|
|
const parsedJson = JSON.parse(candidate)
|
|
|
|
|
if (Array.isArray(parsedJson)) {
|
|
|
|
|
result = parsedJson
|
|
|
|
|
parsed = true
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.warn('解析AI输出失败,candidate:', candidate)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!parsed) {
|
|
|
|
|
// 降级:使用原有启发式生成
|
|
|
|
|
const base = []
|
|
|
|
|
if (props.oc.occupation) base.push({ time: '09:00', event: `专注于${props.oc.occupation}相关工作` })
|
|
|
|
|
if (props.oc.hobbies && props.oc.hobbies.length) {
|
|
|
|
|
const hobbies = Array.isArray(props.oc.hobbies) ? props.oc.hobbies : [props.oc.hobbies]
|
|
|
|
|
hobbies.slice(0,2).forEach((h, i) => {
|
|
|
|
|
base.push({ time: i === 0 ? '14:00' : '16:00', event: `参与${h}活动` })
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
base.push({ time: '18:00', event: '与朋友社交/放松' })
|
|
|
|
|
base.push({ time: '20:00', event: '休息与自我提升' })
|
|
|
|
|
result = base.map(item => ({ date: dateStr, ...item }))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 规整时间/日期格式并保存(同时校验地点白名单)
|
|
|
|
|
result = result.map(item => {
|
|
|
|
|
const loc = item.location && communityLocations.includes(item.location) ? item.location : (communityLocations[0] || '')
|
|
|
|
|
return {
|
|
|
|
|
date: item.date || dateStr,
|
|
|
|
|
time: (item.time || '18:00').slice(0,5),
|
|
|
|
|
event: item.event || '',
|
|
|
|
|
location: loc
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
// 保持升序
|
|
|
|
|
sortSchedule(result)
|
|
|
|
|
scheduleList.value = result
|
|
|
|
|
updateScheduleToOC()
|
|
|
|
|
ElMessage.success('已为该OC生成今日日程')
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error('generateScheduleAI error:', err)
|
|
|
|
|
ElMessage.error('生成日程失败:' + (err?.message || '未知错误'))
|
|
|
|
|
}
|
|
|
|
|
base.push({ time: '18:00', event: '与朋友社交/放松' })
|
|
|
|
|
base.push({ time: '20:00', event: '休息与自我提升' })
|
|
|
|
|
scheduleList.value = base.map(item => ({ date: dateStr, ...item }))
|
|
|
|
|
updateScheduleToOC()
|
|
|
|
|
ElMessage.success('已为该OC智能生成今日日程')
|
|
|
|
|
}
|
|
|
|
|
function formatDate(date) {
|
|
|
|
|
if (!date) return ''
|
|
|
|
|
|