会议纪要改为 列表页+独立编辑页 结构,移除语音录入
- index.vue 重写为标准列表页:搜索(关键字/项目/类型/日期)+ 表格 + 分页, 显示待办同步进度(已同步/总数),双击行或点编辑进入编辑页 - 新增 edit.vue 独立编辑页:新增 /hint/meeting/add、编辑 /hint/meeting/edit/:id, 新建保存后自动切换为编辑路由,防止重复新增;路由复用时通过 $route watch 重置/加载 - router/index.js 增加 /hint 静态隐藏路由(与 /people、/claim 同惯例,activeMenu 高亮列表菜单) - 按要求删除语音录入功能(SpeechRecognition 相关全部移除) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -160,6 +160,25 @@ export const constantRoutes = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/hint",
|
||||||
|
component: Layout,
|
||||||
|
hidden: true,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "meeting/add",
|
||||||
|
component: () => import("@/views/oa/meeting/edit"),
|
||||||
|
name: "addMeetingMinutes",
|
||||||
|
meta: { title: "新增会议纪要", activeMenu: "/hint/meeting" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "meeting/edit/:id(\\d+)",
|
||||||
|
component: () => import("@/views/oa/meeting/edit"),
|
||||||
|
name: "editMeetingMinutes",
|
||||||
|
meta: { title: "编辑会议纪要", activeMenu: "/hint/meeting" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/claim",
|
path: "/claim",
|
||||||
component: Layout,
|
component: Layout,
|
||||||
|
|||||||
468
ruoyi-ui/src/views/oa/meeting/edit.vue
Normal file
468
ruoyi-ui/src/views/oa/meeting/edit.vue
Normal file
@@ -0,0 +1,468 @@
|
|||||||
|
<template>
|
||||||
|
<div class="app-container meeting-edit">
|
||||||
|
<!-- 顶部操作栏 -->
|
||||||
|
<el-card shadow="never" class="topbar">
|
||||||
|
<div class="topbar-inner">
|
||||||
|
<div class="brand">
|
||||||
|
<el-button size="small" icon="el-icon-back" @click="goBack">返回列表</el-button>
|
||||||
|
<span class="brand-title">{{ isEdit ? '编辑会议纪要' : '新增会议纪要' }}</span>
|
||||||
|
<span v-if="form.meetingCode" class="hd-code">{{ form.meetingCode }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="actions">
|
||||||
|
<el-checkbox v-model="form.syncTask" :true-label="1" :false-label="0" class="sync-chk">
|
||||||
|
<span style="font-size:12px">保存时同步生成 OA 任务</span>
|
||||||
|
</el-checkbox>
|
||||||
|
<el-button size="small" type="primary" icon="el-icon-document-checked"
|
||||||
|
:loading="saving" @click="cmdSave">保存</el-button>
|
||||||
|
<el-button size="small" icon="el-icon-download" @click="cmdExport">导出</el-button>
|
||||||
|
<el-button size="small" icon="el-icon-printer" @click="cmdPrint">打印</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-card shadow="never" class="editor-card" v-loading="loadingDetail">
|
||||||
|
<!-- 元数据 -->
|
||||||
|
<el-form :model="form" label-width="80px" size="small" class="meta-form">
|
||||||
|
<el-row :gutter="10">
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-form-item label="会议日期" required>
|
||||||
|
<el-date-picker v-model="form.meetingDate" type="date" value-format="yyyy-MM-dd"
|
||||||
|
style="width:100%" placeholder="选择日期" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-form-item label="项目">
|
||||||
|
<project-select v-model="form.projectId" placeholder="不选则为非项目会议" clearable style="width:100%" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-form-item label="会议类型">
|
||||||
|
<el-select v-model="form.meetingType" style="width:100%">
|
||||||
|
<el-option v-for="t in dict.type.oa_meeting_type" :key="t.value" :value="t.value" :label="t.label" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="16">
|
||||||
|
<el-form-item label="会议主题" required>
|
||||||
|
<el-input v-model="form.subject" maxlength="200" placeholder="输入会议主题" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-form-item label="会议地点">
|
||||||
|
<el-input v-model="form.location" maxlength="100" placeholder="会议室 / 线上" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-form-item label="主持人">
|
||||||
|
<el-tag v-if="form.hostUserId" closable @close="clearHost">
|
||||||
|
{{ form.hostUserName || ('#' + form.hostUserId) }}
|
||||||
|
</el-tag>
|
||||||
|
<el-button type="text" @click="pickHost">{{ form.hostUserId ? '更换' : '点击选择' }}</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="16">
|
||||||
|
<el-form-item label="参会人员">
|
||||||
|
<user-select v-model="form.attendeeUserIds" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<!-- 4 结构化区块 -->
|
||||||
|
<div class="sec-block">
|
||||||
|
<div class="sec-hd"><span class="sec-num">一</span> 会议议题</div>
|
||||||
|
<el-input type="textarea" :rows="4" v-model="form.topic" placeholder="1. 2. 3." />
|
||||||
|
</div>
|
||||||
|
<div class="sec-block">
|
||||||
|
<div class="sec-hd"><span class="sec-num">二</span> 讨论内容</div>
|
||||||
|
<el-input type="textarea" :rows="5" v-model="form.discussion" placeholder="记录讨论要点和各方意见..." />
|
||||||
|
</div>
|
||||||
|
<div class="sec-block">
|
||||||
|
<div class="sec-hd"><span class="sec-num">三</span> 决议事项</div>
|
||||||
|
<el-input type="textarea" :rows="4" v-model="form.decision" placeholder="记录会议达成的决议和结论..." />
|
||||||
|
</div>
|
||||||
|
<div class="sec-block">
|
||||||
|
<div class="sec-hd">
|
||||||
|
<span class="sec-num">四</span> 待办事项
|
||||||
|
<span class="sec-tip">填了负责人和内容的待办,保存时按上方开关同步为 OA 任务并通知负责人</span>
|
||||||
|
<el-button type="text" icon="el-icon-plus" class="add-task" @click="addTask">添加待办</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="form.tasks.length === 0" class="task-empty">尚未添加待办</div>
|
||||||
|
<div v-for="(t, i) in form.tasks" :key="i" class="task-row">
|
||||||
|
<div class="task-line">
|
||||||
|
<div class="tf tf-assignee">
|
||||||
|
<label>负责人</label>
|
||||||
|
<div>
|
||||||
|
<el-tag v-if="t.assigneeUserId" size="small" closable @close="clearAssignee(t)">
|
||||||
|
{{ t.assigneeName || ('#' + t.assigneeUserId) }}
|
||||||
|
</el-tag>
|
||||||
|
<el-button type="text" size="mini" @click="pickAssignee(i)">
|
||||||
|
{{ t.assigneeUserId ? '更换' : '选择' }}
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tf tf-content">
|
||||||
|
<label>任务内容</label>
|
||||||
|
<el-input v-model="t.content" size="mini" maxlength="200" placeholder="任务描述..." />
|
||||||
|
</div>
|
||||||
|
<div class="tf tf-deadline">
|
||||||
|
<label>截止日期</label>
|
||||||
|
<el-date-picker v-model="t.deadline" type="date" size="mini"
|
||||||
|
value-format="yyyy-MM-dd" style="width:100%" placeholder="日期" />
|
||||||
|
</div>
|
||||||
|
<div class="tf tf-status">
|
||||||
|
<label>状态</label>
|
||||||
|
<el-select v-model="t.status" size="mini" style="width:100%">
|
||||||
|
<el-option v-for="o in dict.type.oa_meeting_task_status" :key="o.value"
|
||||||
|
:value="o.value" :label="o.label" />
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
<div class="tf tf-act">
|
||||||
|
<el-tag v-if="t.taskId" size="mini" type="success">已同步</el-tag>
|
||||||
|
<el-button type="text" icon="el-icon-delete" style="color:#f56c6c"
|
||||||
|
@click="removeTask(i)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 人员单选弹窗(主持人 / 待办负责人共用) -->
|
||||||
|
<user-single-select v-model="userPickerVisible" @onSelected="onUserPicked" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
getMeetingMinutes, addMeetingMinutes, updateMeetingMinutes
|
||||||
|
} from '@/api/oa/meetingMinutes'
|
||||||
|
import UserSelect from '@/components/UserSelect'
|
||||||
|
import UserSingleSelect from '@/components/UserSelect/single'
|
||||||
|
import ProjectSelect from '@/components/fad-service/ProjectSelect'
|
||||||
|
|
||||||
|
function localToday () {
|
||||||
|
const d = new Date()
|
||||||
|
const p = n => String(n).padStart(2, '0')
|
||||||
|
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function emptyForm () {
|
||||||
|
return {
|
||||||
|
id: null,
|
||||||
|
meetingCode: '',
|
||||||
|
meetingDate: localToday(),
|
||||||
|
projectId: null,
|
||||||
|
meetingType: 'other',
|
||||||
|
subject: '',
|
||||||
|
location: '',
|
||||||
|
hostUserId: null,
|
||||||
|
hostUserName: '',
|
||||||
|
attendeeUserIds: '',
|
||||||
|
attendeeUserNames: '',
|
||||||
|
topic: '',
|
||||||
|
discussion: '',
|
||||||
|
decision: '',
|
||||||
|
tasks: [],
|
||||||
|
syncTask: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'OaMeetingEdit',
|
||||||
|
components: { UserSelect, UserSingleSelect, ProjectSelect },
|
||||||
|
dicts: ['oa_meeting_type', 'oa_meeting_task_status'],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
saving: false,
|
||||||
|
loadingDetail: false,
|
||||||
|
form: emptyForm(),
|
||||||
|
|
||||||
|
// 人员单选弹窗当前服务对象:'host' 或待办行下标
|
||||||
|
userPickerVisible: false,
|
||||||
|
userPickerTarget: 'host'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isEdit () { return !!this.form.id }
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
const id = this.$route.params.id
|
||||||
|
if (id) this.loadMinutes(id)
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
// 新增/编辑两个路由共用本组件,路由切换时组件实例可能被复用,created 不会重新触发
|
||||||
|
'$route' (to) {
|
||||||
|
if (to.name === 'addMeetingMinutes') {
|
||||||
|
this.form = emptyForm()
|
||||||
|
} else if (to.name === 'editMeetingMinutes' && to.params.id
|
||||||
|
&& String(this.form.id) !== String(to.params.id)) {
|
||||||
|
this.loadMinutes(to.params.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
goBack () {
|
||||||
|
this.$router.push('/hint/meeting')
|
||||||
|
},
|
||||||
|
|
||||||
|
// ============ 人员选择 ============
|
||||||
|
pickHost () {
|
||||||
|
this.userPickerTarget = 'host'
|
||||||
|
this.userPickerVisible = true
|
||||||
|
},
|
||||||
|
clearHost () {
|
||||||
|
this.form.hostUserId = null
|
||||||
|
this.form.hostUserName = ''
|
||||||
|
},
|
||||||
|
pickAssignee (i) {
|
||||||
|
this.userPickerTarget = i
|
||||||
|
this.userPickerVisible = true
|
||||||
|
},
|
||||||
|
clearAssignee (t) {
|
||||||
|
t.assigneeUserId = null
|
||||||
|
t.assigneeName = ''
|
||||||
|
},
|
||||||
|
onUserPicked (row) {
|
||||||
|
if (!row) return
|
||||||
|
if (this.userPickerTarget === 'host') {
|
||||||
|
this.form.hostUserId = row.userId
|
||||||
|
this.form.hostUserName = row.nickName
|
||||||
|
} else {
|
||||||
|
const t = this.form.tasks[this.userPickerTarget]
|
||||||
|
if (t) {
|
||||||
|
t.assigneeUserId = row.userId
|
||||||
|
t.assigneeName = row.nickName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// ============ 待办 ============
|
||||||
|
addTask () {
|
||||||
|
this.form.tasks.push({
|
||||||
|
assigneeUserId: null, assigneeName: '',
|
||||||
|
content: '', deadline: '', status: 'pending', taskId: null
|
||||||
|
})
|
||||||
|
},
|
||||||
|
removeTask (i) {
|
||||||
|
const t = this.form.tasks[i]
|
||||||
|
if (t.taskId) {
|
||||||
|
this.$modal.confirm('该待办已同步为 OA 任务,移除后任务本身不会删除,仅与纪要解除关联。继续?')
|
||||||
|
.then(() => this.form.tasks.splice(i, 1))
|
||||||
|
.catch(() => {})
|
||||||
|
} else {
|
||||||
|
this.form.tasks.splice(i, 1)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// ============ 保存 ============
|
||||||
|
async cmdSave () {
|
||||||
|
if (!this.form.meetingDate) return this.$modal.msgError('请选择会议日期')
|
||||||
|
if (!this.form.subject) return this.$modal.msgError('请输入会议主题')
|
||||||
|
const cleanTasks = (this.form.tasks || []).map(t => ({
|
||||||
|
assigneeUserId: t.assigneeUserId,
|
||||||
|
assigneeName: t.assigneeName,
|
||||||
|
content: t.content,
|
||||||
|
deadline: t.deadline,
|
||||||
|
status: t.status,
|
||||||
|
taskId: t.taskId || null
|
||||||
|
}))
|
||||||
|
const payload = { ...this.form, tasksJson: JSON.stringify(cleanTasks) }
|
||||||
|
delete payload.tasks
|
||||||
|
this.saving = true
|
||||||
|
try {
|
||||||
|
let id = this.form.id
|
||||||
|
if (id) {
|
||||||
|
await updateMeetingMinutes(payload)
|
||||||
|
} else {
|
||||||
|
const res = await addMeetingMinutes(payload)
|
||||||
|
id = res.data
|
||||||
|
// 切到编辑路由,刷新/再保存都是更新而不是再次新增(先置 id 避免路由 watch 重复加载)
|
||||||
|
if (id) {
|
||||||
|
this.form.id = id
|
||||||
|
this.$router.replace('/hint/meeting/edit/' + id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.$modal.msgSuccess('纪要已保存' + (this.form.syncTask ? ',待办已同步到 OA 任务' : ''))
|
||||||
|
if (id) await this.loadMinutes(id)
|
||||||
|
} finally {
|
||||||
|
this.saving = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// ============ 加载 ============
|
||||||
|
async loadMinutes (id) {
|
||||||
|
this.loadingDetail = true
|
||||||
|
try {
|
||||||
|
const res = await getMeetingMinutes(id)
|
||||||
|
const m = res.data
|
||||||
|
if (!m) return
|
||||||
|
this.form = {
|
||||||
|
id: m.id,
|
||||||
|
meetingCode: m.meetingCode,
|
||||||
|
meetingDate: m.meetingDate,
|
||||||
|
projectId: m.projectId,
|
||||||
|
meetingType: m.meetingType || 'other',
|
||||||
|
subject: m.subject || '',
|
||||||
|
location: m.location || '',
|
||||||
|
hostUserId: m.hostUserId || null,
|
||||||
|
hostUserName: m.hostUserName || '',
|
||||||
|
attendeeUserIds: m.attendeeUserIds || '',
|
||||||
|
attendeeUserNames: m.attendeeUserNames || '',
|
||||||
|
topic: m.topic || '',
|
||||||
|
discussion: m.discussion || '',
|
||||||
|
decision: m.decision || '',
|
||||||
|
tasks: this.parseTasks(m.tasksJson),
|
||||||
|
syncTask: m.syncTask == null ? 1 : m.syncTask
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.loadingDetail = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
parseTasks (s) {
|
||||||
|
if (!s) return []
|
||||||
|
try {
|
||||||
|
const a = JSON.parse(s)
|
||||||
|
if (!Array.isArray(a)) return []
|
||||||
|
return a.map(t => ({
|
||||||
|
assigneeUserId: t.assigneeUserId || null,
|
||||||
|
assigneeName: t.assigneeName || '',
|
||||||
|
content: t.content || '',
|
||||||
|
deadline: t.deadline || '',
|
||||||
|
status: t.status || 'pending',
|
||||||
|
taskId: t.taskId || null
|
||||||
|
}))
|
||||||
|
} catch (e) { return [] }
|
||||||
|
},
|
||||||
|
|
||||||
|
dictLabel (dictKey, v) {
|
||||||
|
const hit = (this.dict.type[dictKey] || []).find(t => t.value === v)
|
||||||
|
return hit ? hit.label : (v || '-')
|
||||||
|
},
|
||||||
|
|
||||||
|
// ============ 导出 / 打印 ============
|
||||||
|
cmdExport () {
|
||||||
|
if (!this.form.subject) return this.$modal.msgError('无内容可导出')
|
||||||
|
const d = this.form
|
||||||
|
const lines = []
|
||||||
|
lines.push('德睿福成套设备有限公司 · 会议纪要')
|
||||||
|
lines.push('='.repeat(50))
|
||||||
|
lines.push('编号: ' + (d.meetingCode || '-'))
|
||||||
|
lines.push('日期: ' + d.meetingDate + ' 类型: ' + this.dictLabel('oa_meeting_type', d.meetingType))
|
||||||
|
lines.push('主题: ' + d.subject)
|
||||||
|
lines.push('地点: ' + (d.location || '-'))
|
||||||
|
lines.push('主持: ' + (d.hostUserName || '-'))
|
||||||
|
lines.push('参会: ' + (d.attendeeUserNames || '-'))
|
||||||
|
lines.push('='.repeat(50))
|
||||||
|
lines.push('')
|
||||||
|
lines.push('一、会议议题'); lines.push('-'.repeat(30)); lines.push(d.topic || '(无)'); lines.push('')
|
||||||
|
lines.push('二、讨论内容'); lines.push('-'.repeat(30)); lines.push(d.discussion || '(无)'); lines.push('')
|
||||||
|
lines.push('三、决议事项'); lines.push('-'.repeat(30)); lines.push(d.decision || '(无)'); lines.push('')
|
||||||
|
lines.push('四、待办事项'); lines.push('-'.repeat(30))
|
||||||
|
if (d.tasks && d.tasks.length) {
|
||||||
|
d.tasks.forEach(t => {
|
||||||
|
lines.push(' • [' + (t.assigneeName || '未指派') + '] ' + (t.content || '') +
|
||||||
|
' | 截止:' + (t.deadline || '-') + ' | 状态:' + this.dictLabel('oa_meeting_task_status', t.status))
|
||||||
|
})
|
||||||
|
} else { lines.push('(无)') }
|
||||||
|
lines.push(''); lines.push('='.repeat(50))
|
||||||
|
lines.push('导出时间: ' + new Date().toLocaleString('zh-CN'))
|
||||||
|
const blob = new Blob(['' + lines.join('\n')], { type: 'text/plain;charset=utf-8' })
|
||||||
|
const url = URL.createObjectURL(blob)
|
||||||
|
const a = document.createElement('a')
|
||||||
|
a.href = url; a.download = '会议纪要_' + d.meetingDate + '_' + d.subject + '.txt'; a.click()
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
this.$modal.msgSuccess('已导出')
|
||||||
|
},
|
||||||
|
cmdPrint () {
|
||||||
|
if (!this.form.subject) return this.$modal.msgError('无内容')
|
||||||
|
const d = this.form
|
||||||
|
const esc = s => String(s == null ? '' : s).replace(/[&<>"]/g, c =>
|
||||||
|
({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]))
|
||||||
|
let taskHtml = '(无)'
|
||||||
|
if (d.tasks && d.tasks.length) {
|
||||||
|
const rows = d.tasks.map(t =>
|
||||||
|
`<tr><td>${esc(t.assigneeName || '未指派')}</td><td>${esc(t.content)}</td>` +
|
||||||
|
`<td>${esc(t.deadline || '-')}</td><td>${esc(this.dictLabel('oa_meeting_task_status', t.status))}</td></tr>`
|
||||||
|
).join('')
|
||||||
|
taskHtml = `<table border="1" cellpadding="6" cellspacing="0" style="border-collapse:collapse;width:100%">
|
||||||
|
<tr style="background:#eee"><th>负责人</th><th>任务内容</th><th>截止</th><th>状态</th></tr>${rows}</table>`
|
||||||
|
}
|
||||||
|
const w = window.open('', '', 'width=800,height=700')
|
||||||
|
w.document.write(
|
||||||
|
`<!DOCTYPE html><html><head><meta charset="UTF-8"><title>会议纪要</title>
|
||||||
|
<style>body{font-family:"Microsoft YaHei",sans-serif;padding:40px;max-width:760px;margin:0 auto;line-height:1.8}
|
||||||
|
h1{text-align:center;font-size:20px}.sub{text-align:center;color:#888;margin-bottom:20px}
|
||||||
|
.meta{font-size:13px;margin-bottom:18px;border-bottom:1px solid #ddd;padding-bottom:10px}
|
||||||
|
.meta span{margin-right:18px}.sect{font-size:14px;margin:14px 0 6px;font-weight:700}
|
||||||
|
.body{white-space:pre-wrap;font-size:13px;margin-bottom:16px}
|
||||||
|
@media print{body{padding:20px}}</style></head><body>
|
||||||
|
<h1>德睿福成套设备有限公司</h1><div class="sub">会 议 纪 要 ${esc(d.meetingCode || '')}</div>
|
||||||
|
<div class="meta"><span>📅 ${esc(d.meetingDate)}</span>
|
||||||
|
<span>📝 ${esc(d.subject)}</span>
|
||||||
|
<span>🏷 ${esc(this.dictLabel('oa_meeting_type', d.meetingType))}</span></div>
|
||||||
|
<div class="meta"><span>📍 ${esc(d.location || '-')}</span>
|
||||||
|
<span>🎤 主持:${esc(d.hostUserName || '-')}</span></div>
|
||||||
|
<div class="meta"><span>👥 参会:${esc(d.attendeeUserNames || '-')}</span></div>
|
||||||
|
<div class="sect">一、会议议题</div><div class="body">${esc(d.topic || '(无)')}</div>
|
||||||
|
<div class="sect">二、讨论内容</div><div class="body">${esc(d.discussion || '(无)')}</div>
|
||||||
|
<div class="sect">三、决议事项</div><div class="body">${esc(d.decision || '(无)')}</div>
|
||||||
|
<div class="sect">四、待办事项</div><div class="body">${taskHtml}</div>
|
||||||
|
</body></html>`
|
||||||
|
)
|
||||||
|
w.document.close()
|
||||||
|
setTimeout(() => w.print(), 400)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.meeting-edit { padding: 8px; }
|
||||||
|
.topbar { margin-bottom: 8px; ::v-deep .el-card__body { padding: 10px 14px; } }
|
||||||
|
.topbar-inner { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
|
||||||
|
.brand { display: flex; align-items: center; gap: 10px;
|
||||||
|
.brand-title { font-weight: 600; font-size: 15px; color: #303133; }
|
||||||
|
.hd-code { font-family: monospace; font-size: 12px; color: #909399; }
|
||||||
|
}
|
||||||
|
.actions { display: flex; align-items: center; gap: 6px; }
|
||||||
|
.sync-chk { margin-right: 6px; }
|
||||||
|
|
||||||
|
.editor-card { ::v-deep .el-card__body { padding: 12px 16px; } }
|
||||||
|
|
||||||
|
.meta-form {
|
||||||
|
::v-deep .el-form-item { margin-bottom: 8px; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.sec-block { margin-top: 10px; }
|
||||||
|
.sec-hd {
|
||||||
|
display: flex; align-items: center; gap: 6px;
|
||||||
|
padding: 6px 10px; background: #f4f7fc; border-radius: 4px 4px 0 0;
|
||||||
|
font-size: 13px; font-weight: 600; color: #409eff;
|
||||||
|
.sec-num { display: inline-block; min-width: 18px; height: 18px; line-height: 18px;
|
||||||
|
text-align: center; background: #409eff; color: #fff; border-radius: 3px; font-size: 11px; }
|
||||||
|
.sec-tip { color: #909399; font-weight: normal; font-size: 11px; margin-left: 8px; }
|
||||||
|
.add-task { margin-left: auto; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-empty {
|
||||||
|
border: 1px dashed #dcdfe6; border-top: none;
|
||||||
|
padding: 14px; text-align: center; color: #c0c4cc; font-size: 12px;
|
||||||
|
border-radius: 0 0 4px 4px;
|
||||||
|
}
|
||||||
|
.task-row {
|
||||||
|
border: 1px solid #ebeef5; border-top: none; padding: 8px 10px;
|
||||||
|
background: #fff;
|
||||||
|
&:last-child { border-radius: 0 0 4px 4px; }
|
||||||
|
}
|
||||||
|
.task-line {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 170px 1fr 140px 110px 100px;
|
||||||
|
gap: 8px; align-items: start;
|
||||||
|
.tf {
|
||||||
|
label { display: block; font-size: 11px; color: #909399; margin-bottom: 2px; }
|
||||||
|
&.tf-act { display: flex; flex-direction: column; align-items: center; gap: 4px;
|
||||||
|
padding-top: 16px; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,732 +1,157 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="meeting-page">
|
<div class="app-container">
|
||||||
<!-- 顶部操作栏 -->
|
<!-- 搜索 -->
|
||||||
<el-card shadow="never" class="topbar">
|
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
|
||||||
<div class="topbar-inner">
|
<el-form-item label="关键字" prop="keyword">
|
||||||
<div class="brand">
|
<el-input v-model="queryParams.keyword" placeholder="编号 / 主题 / 地点" clearable
|
||||||
<i class="el-icon-microphone" />
|
style="width: 200px" @keyup.enter.native="handleQuery" />
|
||||||
<span class="brand-title">智能会议纪要</span>
|
</el-form-item>
|
||||||
<el-tag size="mini" :type="statusType" effect="dark" class="brand-status">
|
<el-form-item label="项目" prop="projectId">
|
||||||
<span class="dot" :class="statusType" />{{ statusText }}
|
<project-select v-model="queryParams.projectId" placeholder="选择项目" clearable style="width: 280px" />
|
||||||
</el-tag>
|
</el-form-item>
|
||||||
</div>
|
<el-form-item label="会议类型" prop="meetingType">
|
||||||
<div class="actions">
|
<el-select v-model="queryParams.meetingType" placeholder="全部" clearable style="width: 120px">
|
||||||
<el-checkbox v-model="form.syncTask" :true-label="1" :false-label="0" class="sync-chk">
|
<el-option v-for="t in dict.type.oa_meeting_type" :key="t.value" :value="t.value" :label="t.label" />
|
||||||
<span style="font-size:12px">保存时同步生成 OA 任务</span>
|
</el-select>
|
||||||
</el-checkbox>
|
</el-form-item>
|
||||||
<el-button size="small" icon="el-icon-edit-outline" @click="cmdNew">新建</el-button>
|
<el-form-item label="会议日期">
|
||||||
<el-button size="small" :type="isRecording ? 'danger' : 'warning'"
|
<el-date-picker v-model="dateRange" type="daterange" range-separator="-"
|
||||||
:icon="isRecording ? 'el-icon-video-pause' : 'el-icon-microphone'"
|
start-placeholder="开始日期" end-placeholder="结束日期" value-format="yyyy-MM-dd" style="width: 240px" />
|
||||||
@click="cmdRecord">{{ isRecording ? '停止录音' : '语音录入' }}</el-button>
|
</el-form-item>
|
||||||
<el-button size="small" type="primary" icon="el-icon-document-checked"
|
<el-form-item>
|
||||||
:loading="saving" @click="cmdSave">保存</el-button>
|
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
|
||||||
<el-button size="small" icon="el-icon-download" @click="cmdExport">导出</el-button>
|
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
|
||||||
<el-button size="small" icon="el-icon-printer" @click="cmdPrint">打印</el-button>
|
</el-form-item>
|
||||||
</div>
|
</el-form>
|
||||||
</div>
|
|
||||||
<el-alert v-if="protoWarn" type="warning" :closable="false" show-icon class="proto-warn">
|
|
||||||
语音识别需通过 <b>https</b> 或 <b>localhost</b> 访问。当前协议不支持,录音按钮将无法工作。
|
|
||||||
</el-alert>
|
|
||||||
</el-card>
|
|
||||||
|
|
||||||
<el-row :gutter="10" class="body-row">
|
<!-- 工具栏 -->
|
||||||
<!-- 左:历史纪要列表 -->
|
<el-row :gutter="10" class="mb8">
|
||||||
<el-col :span="7" :xs="24" class="left-col">
|
<el-col :span="1.5">
|
||||||
<el-card shadow="never" class="side-card">
|
<el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd">新增</el-button>
|
||||||
<div slot="header" class="card-hd">
|
|
||||||
<span><i class="el-icon-notebook-2" /> 历史纪要 <em class="hd-count">({{ historyTotal }})</em></span>
|
|
||||||
<el-button type="text" icon="el-icon-plus" @click="cmdNew">新建</el-button>
|
|
||||||
</div>
|
|
||||||
<div class="filter-row">
|
|
||||||
<el-input v-model="historyQuery.keyword" size="mini" placeholder="搜索编号 / 主题 / 地点"
|
|
||||||
clearable prefix-icon="el-icon-search"
|
|
||||||
@keyup.enter.native="searchHistory" @clear="searchHistory" />
|
|
||||||
</div>
|
|
||||||
<div class="filter-row">
|
|
||||||
<project-select v-model="historyQuery.projectId" placeholder="项目筛选" clearable
|
|
||||||
size="mini" style="flex:1" @input="searchHistory" />
|
|
||||||
<el-select v-model="historyQuery.meetingType" size="mini" clearable placeholder="类型"
|
|
||||||
style="width:90px;margin-left:4px" @change="searchHistory">
|
|
||||||
<el-option v-for="t in dict.type.oa_meeting_type" :key="t.value" :value="t.value" :label="t.label" />
|
|
||||||
</el-select>
|
|
||||||
</div>
|
|
||||||
<div class="filter-row">
|
|
||||||
<el-date-picker v-model="historyQuery.dateRange" type="daterange" size="mini" range-separator="~"
|
|
||||||
start-placeholder="开始" end-placeholder="结束" value-format="yyyy-MM-dd"
|
|
||||||
style="width:100%" @change="searchHistory" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-loading="historyLoading" class="hist-list">
|
|
||||||
<div v-if="historyList.length === 0 && !historyLoading" class="hist-empty">
|
|
||||||
<i class="el-icon-document" /><div>暂无会议纪要</div>
|
|
||||||
</div>
|
|
||||||
<div v-for="m in historyList" :key="m.id"
|
|
||||||
class="hist-card" :class="{ sel: form.id === m.id }"
|
|
||||||
@click="loadMinutes(m.id)">
|
|
||||||
<div class="hc-top">
|
|
||||||
<span class="hc-code">{{ m.meetingCode }}</span>
|
|
||||||
<dict-tag :options="dict.type.oa_meeting_type" :value="m.meetingType" />
|
|
||||||
<el-button type="text" icon="el-icon-delete" class="hc-del"
|
|
||||||
@click.stop="removeMinutes(m)" />
|
|
||||||
</div>
|
|
||||||
<div class="hc-subject">{{ m.subject }}</div>
|
|
||||||
<div class="hc-line"><i class="el-icon-date" /> {{ m.meetingDate }}
|
|
||||||
<span v-if="m.projectName" class="hc-proj">· {{ m.projectName }}</span>
|
|
||||||
</div>
|
|
||||||
<div v-if="m.hostUserName" class="hc-line"><i class="el-icon-s-custom" /> 主持:{{ m.hostUserName }}</div>
|
|
||||||
<div v-if="m.location" class="hc-line"><i class="el-icon-location-outline" /> {{ m.location }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<pagination v-show="historyTotal > 0" :total="historyTotal"
|
|
||||||
:page.sync="historyQuery.pageNum" :limit.sync="historyQuery.pageSize"
|
|
||||||
:page-sizes="[10, 20, 50]" small
|
|
||||||
@pagination="loadHistory" />
|
|
||||||
</el-card>
|
|
||||||
</el-col>
|
|
||||||
|
|
||||||
<!-- 右:编辑器 -->
|
|
||||||
<el-col :span="17" :xs="24" class="right-col">
|
|
||||||
<el-card shadow="never" class="editor-card">
|
|
||||||
<div slot="header" class="card-hd">
|
|
||||||
<span><i class="el-icon-edit-outline" />
|
|
||||||
{{ form.id ? ('编辑:' + (form.subject || '未命名')) : '新建会议纪要' }}
|
|
||||||
</span>
|
|
||||||
<span v-if="form.meetingCode" class="hd-tail-code">{{ form.meetingCode }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 元数据 -->
|
|
||||||
<el-form :model="form" label-width="80px" size="small" class="meta-form">
|
|
||||||
<el-row :gutter="10">
|
|
||||||
<el-col :span="8">
|
|
||||||
<el-form-item label="会议日期" required>
|
|
||||||
<el-date-picker v-model="form.meetingDate" type="date" value-format="yyyy-MM-dd"
|
|
||||||
style="width:100%" placeholder="选择日期" />
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="8">
|
|
||||||
<el-form-item label="项目">
|
|
||||||
<project-select v-model="form.projectId" placeholder="不选则为非项目会议" clearable style="width:100%" />
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="8">
|
|
||||||
<el-form-item label="会议类型">
|
|
||||||
<el-select v-model="form.meetingType" style="width:100%">
|
|
||||||
<el-option v-for="t in dict.type.oa_meeting_type" :key="t.value" :value="t.value" :label="t.label" />
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="16">
|
|
||||||
<el-form-item label="会议主题" required>
|
|
||||||
<el-input v-model="form.subject" maxlength="200" placeholder="输入会议主题" />
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="8">
|
|
||||||
<el-form-item label="会议地点">
|
|
||||||
<el-input v-model="form.location" maxlength="100" placeholder="会议室 / 线上" />
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="8">
|
|
||||||
<el-form-item label="主持人">
|
|
||||||
<el-tag v-if="form.hostUserId" closable @close="clearHost">
|
|
||||||
{{ form.hostUserName || ('#' + form.hostUserId) }}
|
|
||||||
</el-tag>
|
|
||||||
<el-button type="text" @click="pickHost">{{ form.hostUserId ? '更换' : '点击选择' }}</el-button>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="16">
|
|
||||||
<el-form-item label="参会人员">
|
|
||||||
<user-select v-model="form.attendeeUserIds" />
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
</el-form>
|
|
||||||
|
|
||||||
<!-- 语音面板 -->
|
|
||||||
<el-card v-if="isRecording || voiceFinalText" class="voice-card" shadow="never">
|
|
||||||
<div class="voice-hd">
|
|
||||||
<span class="live-dot" :class="{ blink: isRecording }" />
|
|
||||||
<span class="voice-title">{{ isRecording ? '实时语音识别中 — 请对着麦克风讲话' : '语音识别结果' }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="voice-interim">{{ voiceInterim }}</div>
|
|
||||||
<div class="voice-final">{{ voiceFinalText || '(无内容)' }}</div>
|
|
||||||
<div class="voice-actions">
|
|
||||||
<el-button size="mini" icon="el-icon-plus" @click="insertVoiceTo('topic')">插入到 议题</el-button>
|
|
||||||
<el-button size="mini" icon="el-icon-plus" @click="insertVoiceTo('discussion')">插入到 讨论</el-button>
|
|
||||||
<el-button size="mini" icon="el-icon-plus" @click="insertVoiceTo('decision')">插入到 决议</el-button>
|
|
||||||
<el-button size="mini" type="danger" icon="el-icon-delete" plain @click="clearVoice">清空</el-button>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
|
|
||||||
<!-- 4 结构化区块 -->
|
|
||||||
<div class="sec-block">
|
|
||||||
<div class="sec-hd"><span class="sec-num">一</span> 会议议题</div>
|
|
||||||
<el-input type="textarea" :rows="4" v-model="form.topic" placeholder="1. 2. 3." />
|
|
||||||
</div>
|
|
||||||
<div class="sec-block">
|
|
||||||
<div class="sec-hd"><span class="sec-num">二</span> 讨论内容</div>
|
|
||||||
<el-input type="textarea" :rows="5" v-model="form.discussion" placeholder="记录讨论要点和各方意见..." />
|
|
||||||
</div>
|
|
||||||
<div class="sec-block">
|
|
||||||
<div class="sec-hd"><span class="sec-num">三</span> 决议事项</div>
|
|
||||||
<el-input type="textarea" :rows="4" v-model="form.decision" placeholder="记录会议达成的决议和结论..." />
|
|
||||||
</div>
|
|
||||||
<div class="sec-block">
|
|
||||||
<div class="sec-hd">
|
|
||||||
<span class="sec-num">四</span> 待办事项
|
|
||||||
<span class="sec-tip">填了负责人和内容的待办,保存时按上方开关同步为 OA 任务并通知负责人</span>
|
|
||||||
<el-button type="text" icon="el-icon-plus" class="add-task" @click="addTask">添加待办</el-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="form.tasks.length === 0" class="task-empty">尚未添加待办</div>
|
|
||||||
<div v-for="(t, i) in form.tasks" :key="i" class="task-row">
|
|
||||||
<div class="task-line">
|
|
||||||
<div class="tf tf-assignee">
|
|
||||||
<label>负责人</label>
|
|
||||||
<div>
|
|
||||||
<el-tag v-if="t.assigneeUserId" size="small" closable @close="clearAssignee(t)">
|
|
||||||
{{ t.assigneeName || ('#' + t.assigneeUserId) }}
|
|
||||||
</el-tag>
|
|
||||||
<el-button type="text" size="mini" @click="pickAssignee(i)">
|
|
||||||
{{ t.assigneeUserId ? '更换' : '选择' }}
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="tf tf-content">
|
|
||||||
<label>任务内容</label>
|
|
||||||
<el-input v-model="t.content" size="mini" maxlength="200" placeholder="任务描述..." />
|
|
||||||
</div>
|
|
||||||
<div class="tf tf-deadline">
|
|
||||||
<label>截止日期</label>
|
|
||||||
<el-date-picker v-model="t.deadline" type="date" size="mini"
|
|
||||||
value-format="yyyy-MM-dd" style="width:100%" placeholder="日期" />
|
|
||||||
</div>
|
|
||||||
<div class="tf tf-status">
|
|
||||||
<label>状态</label>
|
|
||||||
<el-select v-model="t.status" size="mini" style="width:100%">
|
|
||||||
<el-option v-for="o in dict.type.oa_meeting_task_status" :key="o.value"
|
|
||||||
:value="o.value" :label="o.label" />
|
|
||||||
</el-select>
|
|
||||||
</div>
|
|
||||||
<div class="tf tf-act">
|
|
||||||
<el-tag v-if="t.taskId" size="mini" type="success">已同步</el-tag>
|
|
||||||
<el-button type="text" icon="el-icon-delete" style="color:#f56c6c"
|
|
||||||
@click="removeTask(i)" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
</el-col>
|
</el-col>
|
||||||
|
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
<!-- 人员单选弹窗(主持人 / 待办负责人共用) -->
|
<!-- 列表 -->
|
||||||
<user-single-select ref="userPicker" v-model="userPickerVisible" @onSelected="onUserPicked" />
|
<el-table v-loading="loading" :data="list" @row-dblclick="handleEdit">
|
||||||
|
<el-table-column label="会议编号" align="center" prop="meetingCode" width="180" show-overflow-tooltip />
|
||||||
|
<el-table-column label="会议日期" align="center" prop="meetingDate" width="100" />
|
||||||
|
<el-table-column label="类型" align="center" width="90">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<dict-tag :options="dict.type.oa_meeting_type" :value="scope.row.meetingType" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="会议主题" align="left" prop="subject" min-width="180" show-overflow-tooltip />
|
||||||
|
<el-table-column label="项目" align="center" prop="projectName" min-width="140" show-overflow-tooltip>
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span v-if="scope.row.projectName">{{ scope.row.projectName }}</span>
|
||||||
|
<span v-else style="color:#c0c4cc">—</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="主持人" align="center" prop="hostUserName" width="90" />
|
||||||
|
<el-table-column label="地点" align="center" prop="location" width="110" show-overflow-tooltip />
|
||||||
|
<el-table-column label="待办" align="center" width="90">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span v-if="taskCount(scope.row)">
|
||||||
|
{{ syncedCount(scope.row) }}/{{ taskCount(scope.row) }} 已同步
|
||||||
|
</span>
|
||||||
|
<span v-else style="color:#c0c4cc">—</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" align="center" width="140" class-name="small-padding fixed-width">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleEdit(scope.row)">编辑</el-button>
|
||||||
|
<el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<pagination v-show="total > 0" :total="total"
|
||||||
|
:page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {
|
import { listMeetingMinutes, delMeetingMinutes } from '@/api/oa/meetingMinutes'
|
||||||
listMeetingMinutes, getMeetingMinutes, addMeetingMinutes,
|
|
||||||
updateMeetingMinutes, delMeetingMinutes
|
|
||||||
} from '@/api/oa/meetingMinutes'
|
|
||||||
import UserSelect from '@/components/UserSelect'
|
|
||||||
import UserSingleSelect from '@/components/UserSelect/single'
|
|
||||||
import ProjectSelect from '@/components/fad-service/ProjectSelect'
|
import ProjectSelect from '@/components/fad-service/ProjectSelect'
|
||||||
|
|
||||||
function localToday () {
|
|
||||||
const d = new Date()
|
|
||||||
const p = n => String(n).padStart(2, '0')
|
|
||||||
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`
|
|
||||||
}
|
|
||||||
|
|
||||||
function emptyForm () {
|
|
||||||
return {
|
|
||||||
id: null,
|
|
||||||
meetingCode: '',
|
|
||||||
meetingDate: localToday(),
|
|
||||||
projectId: null,
|
|
||||||
meetingType: 'other',
|
|
||||||
subject: '',
|
|
||||||
location: '',
|
|
||||||
hostUserId: null,
|
|
||||||
hostUserName: '',
|
|
||||||
attendeeUserIds: '',
|
|
||||||
attendeeUserNames: '',
|
|
||||||
topic: '',
|
|
||||||
discussion: '',
|
|
||||||
decision: '',
|
|
||||||
tasks: [],
|
|
||||||
syncTask: 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'OaMeeting',
|
name: 'OaMeeting',
|
||||||
components: { UserSelect, UserSingleSelect, ProjectSelect },
|
components: { ProjectSelect },
|
||||||
dicts: ['oa_meeting_type', 'oa_meeting_task_status'],
|
dicts: ['oa_meeting_type'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
statusType: 'success',
|
loading: true,
|
||||||
statusText: '就绪',
|
showSearch: true,
|
||||||
protoWarn: false,
|
total: 0,
|
||||||
saving: false,
|
list: [],
|
||||||
|
dateRange: [],
|
||||||
form: emptyForm(),
|
queryParams: {
|
||||||
|
pageNum: 1,
|
||||||
historyList: [],
|
pageSize: 10,
|
||||||
historyTotal: 0,
|
keyword: '',
|
||||||
historyLoading: false,
|
meetingType: '',
|
||||||
historyQuery: {
|
projectId: null
|
||||||
pageNum: 1, pageSize: 20,
|
}
|
||||||
keyword: '', meetingType: '', projectId: null, dateRange: []
|
|
||||||
},
|
|
||||||
|
|
||||||
recognition: null,
|
|
||||||
isRecording: false,
|
|
||||||
voiceFinalText: '',
|
|
||||||
voiceInterim: '',
|
|
||||||
|
|
||||||
// 人员单选弹窗当前服务对象:'host' 或待办行下标
|
|
||||||
userPickerVisible: false,
|
|
||||||
userPickerTarget: 'host'
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created () {
|
created () {
|
||||||
this.checkProtocol()
|
this.getList()
|
||||||
this.loadHistory()
|
},
|
||||||
|
activated () {
|
||||||
|
// 从编辑页返回时刷新
|
||||||
|
this.getList()
|
||||||
},
|
},
|
||||||
beforeDestroy () { this.stopRecording() },
|
|
||||||
methods: {
|
methods: {
|
||||||
setStatus (text, type = 'success') { this.statusText = text; this.statusType = type },
|
getList () {
|
||||||
checkProtocol () {
|
this.loading = true
|
||||||
const host = location.hostname
|
const q = { ...this.queryParams }
|
||||||
const local = host === 'localhost' || host === '127.0.0.1'
|
if (this.dateRange && this.dateRange.length === 2) {
|
||||||
this.protoWarn = !(location.protocol === 'https:' || local)
|
q.dateFrom = this.dateRange[0]
|
||||||
},
|
q.dateTo = this.dateRange[1]
|
||||||
dictLabel (dictKey, v) {
|
|
||||||
const hit = (this.dict.type[dictKey] || []).find(t => t.value === v)
|
|
||||||
return hit ? hit.label : (v || '-')
|
|
||||||
},
|
|
||||||
|
|
||||||
// ============ 人员选择 ============
|
|
||||||
pickHost () {
|
|
||||||
this.userPickerTarget = 'host'
|
|
||||||
this.userPickerVisible = true
|
|
||||||
},
|
|
||||||
clearHost () {
|
|
||||||
this.form.hostUserId = null
|
|
||||||
this.form.hostUserName = ''
|
|
||||||
},
|
|
||||||
pickAssignee (i) {
|
|
||||||
this.userPickerTarget = i
|
|
||||||
this.userPickerVisible = true
|
|
||||||
},
|
|
||||||
clearAssignee (t) {
|
|
||||||
t.assigneeUserId = null
|
|
||||||
t.assigneeName = ''
|
|
||||||
},
|
|
||||||
onUserPicked (row) {
|
|
||||||
if (!row) return
|
|
||||||
if (this.userPickerTarget === 'host') {
|
|
||||||
this.form.hostUserId = row.userId
|
|
||||||
this.form.hostUserName = row.nickName
|
|
||||||
} else {
|
|
||||||
const t = this.form.tasks[this.userPickerTarget]
|
|
||||||
if (t) {
|
|
||||||
t.assigneeUserId = row.userId
|
|
||||||
t.assigneeName = row.nickName
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
listMeetingMinutes(q).then(res => {
|
||||||
|
this.list = res.rows || []
|
||||||
|
this.total = res.total || 0
|
||||||
|
}).finally(() => { this.loading = false })
|
||||||
},
|
},
|
||||||
|
handleQuery () {
|
||||||
// ============ 待办 ============
|
this.queryParams.pageNum = 1
|
||||||
addTask () {
|
this.getList()
|
||||||
this.form.tasks.push({
|
|
||||||
assigneeUserId: null, assigneeName: '',
|
|
||||||
content: '', deadline: '', status: 'pending', taskId: null
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
removeTask (i) {
|
resetQuery () {
|
||||||
const t = this.form.tasks[i]
|
this.dateRange = []
|
||||||
if (t.taskId) {
|
this.queryParams.projectId = null
|
||||||
this.$modal.confirm('该待办已同步为 OA 任务,移除后任务本身不会删除,仅与纪要解除关联。继续?')
|
this.resetForm('queryForm')
|
||||||
.then(() => this.form.tasks.splice(i, 1))
|
this.handleQuery()
|
||||||
.catch(() => {})
|
|
||||||
} else {
|
|
||||||
this.form.tasks.splice(i, 1)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
taskCount (row) {
|
||||||
// ============ 新建 / 保存 ============
|
return this.parseTasks(row).length
|
||||||
cmdNew () {
|
|
||||||
const keepProject = this.form.projectId
|
|
||||||
this.form = emptyForm()
|
|
||||||
if (keepProject) this.form.projectId = keepProject
|
|
||||||
this.clearVoice()
|
|
||||||
},
|
},
|
||||||
async cmdSave () {
|
syncedCount (row) {
|
||||||
if (!this.form.meetingDate) return this.$modal.msgError('请选择会议日期')
|
return this.parseTasks(row).filter(t => t.taskId).length
|
||||||
if (!this.form.subject) return this.$modal.msgError('请输入会议主题')
|
},
|
||||||
const cleanTasks = (this.form.tasks || []).map(t => ({
|
parseTasks (row) {
|
||||||
assigneeUserId: t.assigneeUserId,
|
if (!row.tasksJson) return []
|
||||||
assigneeName: t.assigneeName,
|
|
||||||
content: t.content,
|
|
||||||
deadline: t.deadline,
|
|
||||||
status: t.status,
|
|
||||||
taskId: t.taskId || null
|
|
||||||
}))
|
|
||||||
const payload = { ...this.form, tasksJson: JSON.stringify(cleanTasks) }
|
|
||||||
delete payload.tasks
|
|
||||||
this.saving = true
|
|
||||||
try {
|
try {
|
||||||
let id = this.form.id
|
const a = JSON.parse(row.tasksJson)
|
||||||
if (id) {
|
return Array.isArray(a) ? a : []
|
||||||
await updateMeetingMinutes(payload)
|
|
||||||
} else {
|
|
||||||
const res = await addMeetingMinutes(payload)
|
|
||||||
id = res.data
|
|
||||||
}
|
|
||||||
this.setStatus('已保存', 'success')
|
|
||||||
setTimeout(() => this.setStatus('就绪', 'success'), 2000)
|
|
||||||
this.$modal.msgSuccess('纪要已保存' + (this.form.syncTask ? ',待办已同步到 OA 任务' : ''))
|
|
||||||
await this.loadHistory()
|
|
||||||
if (id) await this.loadMinutes(id, true)
|
|
||||||
} catch (err) {
|
|
||||||
this.setStatus('保存失败', 'warning')
|
|
||||||
} finally {
|
|
||||||
this.saving = false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// ============ 历史 ============
|
|
||||||
searchHistory () {
|
|
||||||
this.historyQuery.pageNum = 1
|
|
||||||
this.loadHistory()
|
|
||||||
},
|
|
||||||
async loadHistory () {
|
|
||||||
this.historyLoading = true
|
|
||||||
try {
|
|
||||||
const q = {
|
|
||||||
pageNum: this.historyQuery.pageNum,
|
|
||||||
pageSize: this.historyQuery.pageSize,
|
|
||||||
keyword: this.historyQuery.keyword,
|
|
||||||
meetingType: this.historyQuery.meetingType,
|
|
||||||
projectId: this.historyQuery.projectId
|
|
||||||
}
|
|
||||||
const r = this.historyQuery.dateRange
|
|
||||||
if (r && r.length === 2) { q.dateFrom = r[0]; q.dateTo = r[1] }
|
|
||||||
const res = await listMeetingMinutes(q)
|
|
||||||
this.historyList = res.rows || []
|
|
||||||
this.historyTotal = res.total || 0
|
|
||||||
} finally { this.historyLoading = false }
|
|
||||||
},
|
|
||||||
async loadMinutes (id, silent) {
|
|
||||||
const res = await getMeetingMinutes(id)
|
|
||||||
const m = res.data
|
|
||||||
if (!m) return
|
|
||||||
this.form = {
|
|
||||||
id: m.id,
|
|
||||||
meetingCode: m.meetingCode,
|
|
||||||
meetingDate: m.meetingDate,
|
|
||||||
projectId: m.projectId,
|
|
||||||
meetingType: m.meetingType || 'other',
|
|
||||||
subject: m.subject || '',
|
|
||||||
location: m.location || '',
|
|
||||||
hostUserId: m.hostUserId || null,
|
|
||||||
hostUserName: m.hostUserName || '',
|
|
||||||
attendeeUserIds: m.attendeeUserIds || '',
|
|
||||||
attendeeUserNames: m.attendeeUserNames || '',
|
|
||||||
topic: m.topic || '',
|
|
||||||
discussion: m.discussion || '',
|
|
||||||
decision: m.decision || '',
|
|
||||||
tasks: this.parseTasks(m.tasksJson),
|
|
||||||
syncTask: m.syncTask == null ? 1 : m.syncTask
|
|
||||||
}
|
|
||||||
if (!silent) this.$modal.msgSuccess('已加载:' + m.subject)
|
|
||||||
},
|
|
||||||
parseTasks (s) {
|
|
||||||
if (!s) return []
|
|
||||||
try {
|
|
||||||
const a = JSON.parse(s)
|
|
||||||
if (!Array.isArray(a)) return []
|
|
||||||
return a.map(t => ({
|
|
||||||
assigneeUserId: t.assigneeUserId || null,
|
|
||||||
assigneeName: t.assigneeName || '',
|
|
||||||
content: t.content || '',
|
|
||||||
deadline: t.deadline || '',
|
|
||||||
status: t.status || 'pending',
|
|
||||||
taskId: t.taskId || null
|
|
||||||
}))
|
|
||||||
} catch (e) { return [] }
|
} catch (e) { return [] }
|
||||||
},
|
},
|
||||||
removeMinutes (m) {
|
handleAdd () {
|
||||||
this.$modal.confirm(`确认删除「${m.subject}」?已同步的 OA 任务不受影响。`).then(async () => {
|
this.$router.push('/hint/meeting/add')
|
||||||
await delMeetingMinutes(m.id)
|
},
|
||||||
|
handleEdit (row) {
|
||||||
|
this.$router.push('/hint/meeting/edit/' + row.id)
|
||||||
|
},
|
||||||
|
handleDelete (row) {
|
||||||
|
this.$modal.confirm(`确认删除「${row.subject}」?已同步的 OA 任务不受影响。`).then(() => {
|
||||||
|
return delMeetingMinutes(row.id)
|
||||||
|
}).then(() => {
|
||||||
this.$modal.msgSuccess('已删除')
|
this.$modal.msgSuccess('已删除')
|
||||||
if (this.form.id === m.id) this.cmdNew()
|
this.getList()
|
||||||
await this.loadHistory()
|
|
||||||
}).catch(() => {})
|
}).catch(() => {})
|
||||||
},
|
|
||||||
|
|
||||||
// ============ 语音识别 ============
|
|
||||||
cmdRecord () {
|
|
||||||
if (this.isRecording) return this.stopRecording()
|
|
||||||
this.checkProtocol()
|
|
||||||
if (this.protoWarn) return this.$modal.msgError('需 https / localhost 才能录音')
|
|
||||||
const SR = window.SpeechRecognition || window.webkitSpeechRecognition
|
|
||||||
if (!SR) return this.$modal.msgError('浏览器不支持语音 API,请用 Chrome / Edge')
|
|
||||||
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
|
|
||||||
navigator.mediaDevices.getUserMedia({ audio: true })
|
|
||||||
.then(() => this.startRecording(SR))
|
|
||||||
.catch(e => this.$modal.msgError('麦克风权限被拒绝:' + e.message))
|
|
||||||
} else { this.startRecording(SR) }
|
|
||||||
},
|
|
||||||
startRecording (SR) {
|
|
||||||
const r = new SR()
|
|
||||||
r.lang = 'zh-CN'; r.interimResults = true; r.continuous = true; r.maxAlternatives = 1
|
|
||||||
r.onstart = () => {
|
|
||||||
this.isRecording = true; this.voiceFinalText = ''; this.voiceInterim = ''
|
|
||||||
this.setStatus('录音中...', 'danger')
|
|
||||||
}
|
|
||||||
r.onresult = (event) => {
|
|
||||||
let interim = ''; let final = ''
|
|
||||||
for (let i = event.resultIndex; i < event.results.length; i++) {
|
|
||||||
const rr = event.results[i]
|
|
||||||
if (rr.isFinal) final += rr[0].transcript; else interim += rr[0].transcript
|
|
||||||
}
|
|
||||||
if (final) this.voiceFinalText += final
|
|
||||||
this.voiceInterim = interim
|
|
||||||
}
|
|
||||||
r.onerror = (event) => {
|
|
||||||
const msgMap = {
|
|
||||||
'not-allowed': '麦克风权限被拒绝', 'audio-capture': '未检测到麦克风',
|
|
||||||
'network': '网络错误', 'service-not-allowed': '语音识别服务不可用',
|
|
||||||
'no-speech': '', 'aborted': ''
|
|
||||||
}
|
|
||||||
const msg = msgMap[event.error] || ('语音识别错误:' + event.error)
|
|
||||||
if (msg) this.$modal.msgError(msg)
|
|
||||||
if (event.error !== 'no-speech' && event.error !== 'aborted') this.stopRecording()
|
|
||||||
}
|
|
||||||
r.onend = () => {
|
|
||||||
if (this.isRecording) {
|
|
||||||
setTimeout(() => {
|
|
||||||
if (this.isRecording && this.recognition) {
|
|
||||||
try { this.recognition.start() } catch (e) {}
|
|
||||||
}
|
|
||||||
}, 400)
|
|
||||||
} else { this.setStatus('就绪', 'success') }
|
|
||||||
}
|
|
||||||
this.recognition = r
|
|
||||||
try { r.start() } catch (e) {
|
|
||||||
this.$modal.msgError('启动失败:' + e.message); this.stopRecording()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
stopRecording () {
|
|
||||||
this.isRecording = false
|
|
||||||
if (this.recognition) {
|
|
||||||
try { this.recognition.stop() } catch (e) {}
|
|
||||||
this.recognition = null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
insertVoiceTo (sec) {
|
|
||||||
const txt = this.voiceFinalText.trim()
|
|
||||||
if (!txt) return this.$modal.msgError('没有可插入的语音内容')
|
|
||||||
this.form[sec] = (this.form[sec] ? this.form[sec] + '\n' : '') + txt
|
|
||||||
this.clearVoice()
|
|
||||||
this.$modal.msgSuccess('已插入')
|
|
||||||
},
|
|
||||||
clearVoice () { this.voiceFinalText = ''; this.voiceInterim = '' },
|
|
||||||
|
|
||||||
// ============ 导出 / 打印 ============
|
|
||||||
cmdExport () {
|
|
||||||
if (!this.form.subject) return this.$modal.msgError('无内容可导出')
|
|
||||||
const d = this.form
|
|
||||||
const lines = []
|
|
||||||
lines.push('德睿福成套设备有限公司 · 会议纪要')
|
|
||||||
lines.push('='.repeat(50))
|
|
||||||
lines.push('编号: ' + (d.meetingCode || '-'))
|
|
||||||
lines.push('日期: ' + d.meetingDate + ' 类型: ' + this.dictLabel('oa_meeting_type', d.meetingType))
|
|
||||||
lines.push('主题: ' + d.subject)
|
|
||||||
lines.push('地点: ' + (d.location || '-'))
|
|
||||||
lines.push('主持: ' + (d.hostUserName || '-'))
|
|
||||||
lines.push('参会: ' + (d.attendeeUserNames || '-'))
|
|
||||||
lines.push('='.repeat(50))
|
|
||||||
lines.push('')
|
|
||||||
lines.push('一、会议议题'); lines.push('-'.repeat(30)); lines.push(d.topic || '(无)'); lines.push('')
|
|
||||||
lines.push('二、讨论内容'); lines.push('-'.repeat(30)); lines.push(d.discussion || '(无)'); lines.push('')
|
|
||||||
lines.push('三、决议事项'); lines.push('-'.repeat(30)); lines.push(d.decision || '(无)'); lines.push('')
|
|
||||||
lines.push('四、待办事项'); lines.push('-'.repeat(30))
|
|
||||||
if (d.tasks && d.tasks.length) {
|
|
||||||
d.tasks.forEach(t => {
|
|
||||||
lines.push(' • [' + (t.assigneeName || '未指派') + '] ' + (t.content || '') +
|
|
||||||
' | 截止:' + (t.deadline || '-') + ' | 状态:' + this.dictLabel('oa_meeting_task_status', t.status))
|
|
||||||
})
|
|
||||||
} else { lines.push('(无)') }
|
|
||||||
lines.push(''); lines.push('='.repeat(50))
|
|
||||||
lines.push('导出时间: ' + new Date().toLocaleString('zh-CN'))
|
|
||||||
const blob = new Blob(['' + lines.join('\n')], { type: 'text/plain;charset=utf-8' })
|
|
||||||
const url = URL.createObjectURL(blob)
|
|
||||||
const a = document.createElement('a')
|
|
||||||
a.href = url; a.download = '会议纪要_' + d.meetingDate + '_' + d.subject + '.txt'; a.click()
|
|
||||||
URL.revokeObjectURL(url)
|
|
||||||
this.$modal.msgSuccess('已导出')
|
|
||||||
},
|
|
||||||
cmdPrint () {
|
|
||||||
if (!this.form.subject) return this.$modal.msgError('无内容')
|
|
||||||
const d = this.form
|
|
||||||
const esc = s => String(s == null ? '' : s).replace(/[&<>"]/g, c =>
|
|
||||||
({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]))
|
|
||||||
let taskHtml = '(无)'
|
|
||||||
if (d.tasks && d.tasks.length) {
|
|
||||||
const rows = d.tasks.map(t =>
|
|
||||||
`<tr><td>${esc(t.assigneeName || '未指派')}</td><td>${esc(t.content)}</td>` +
|
|
||||||
`<td>${esc(t.deadline || '-')}</td><td>${esc(this.dictLabel('oa_meeting_task_status', t.status))}</td></tr>`
|
|
||||||
).join('')
|
|
||||||
taskHtml = `<table border="1" cellpadding="6" cellspacing="0" style="border-collapse:collapse;width:100%">
|
|
||||||
<tr style="background:#eee"><th>负责人</th><th>任务内容</th><th>截止</th><th>状态</th></tr>${rows}</table>`
|
|
||||||
}
|
|
||||||
const w = window.open('', '', 'width=800,height=700')
|
|
||||||
w.document.write(
|
|
||||||
`<!DOCTYPE html><html><head><meta charset="UTF-8"><title>会议纪要</title>
|
|
||||||
<style>body{font-family:"Microsoft YaHei",sans-serif;padding:40px;max-width:760px;margin:0 auto;line-height:1.8}
|
|
||||||
h1{text-align:center;font-size:20px}.sub{text-align:center;color:#888;margin-bottom:20px}
|
|
||||||
.meta{font-size:13px;margin-bottom:18px;border-bottom:1px solid #ddd;padding-bottom:10px}
|
|
||||||
.meta span{margin-right:18px}.sect{font-size:14px;margin:14px 0 6px;font-weight:700}
|
|
||||||
.body{white-space:pre-wrap;font-size:13px;margin-bottom:16px}
|
|
||||||
@media print{body{padding:20px}}</style></head><body>
|
|
||||||
<h1>德睿福成套设备有限公司</h1><div class="sub">会 议 纪 要 ${esc(d.meetingCode || '')}</div>
|
|
||||||
<div class="meta"><span>📅 ${esc(d.meetingDate)}</span>
|
|
||||||
<span>📝 ${esc(d.subject)}</span>
|
|
||||||
<span>🏷 ${esc(this.dictLabel('oa_meeting_type', d.meetingType))}</span></div>
|
|
||||||
<div class="meta"><span>📍 ${esc(d.location || '-')}</span>
|
|
||||||
<span>🎤 主持:${esc(d.hostUserName || '-')}</span></div>
|
|
||||||
<div class="meta"><span>👥 参会:${esc(d.attendeeUserNames || '-')}</span></div>
|
|
||||||
<div class="sect">一、会议议题</div><div class="body">${esc(d.topic || '(无)')}</div>
|
|
||||||
<div class="sect">二、讨论内容</div><div class="body">${esc(d.discussion || '(无)')}</div>
|
|
||||||
<div class="sect">三、决议事项</div><div class="body">${esc(d.decision || '(无)')}</div>
|
|
||||||
<div class="sect">四、待办事项</div><div class="body">${taskHtml}</div>
|
|
||||||
</body></html>`
|
|
||||||
)
|
|
||||||
w.document.close()
|
|
||||||
setTimeout(() => w.print(), 400)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.meeting-page { padding: 8px; }
|
|
||||||
.topbar { margin-bottom: 8px; ::v-deep .el-card__body { padding: 10px 14px; } }
|
|
||||||
.topbar-inner { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
|
|
||||||
.brand { display: flex; align-items: center; gap: 8px;
|
|
||||||
.el-icon-microphone { color: #409eff; font-size: 18px; }
|
|
||||||
.brand-title { font-weight: 600; font-size: 15px; color: #303133; }
|
|
||||||
}
|
|
||||||
.brand-status .dot {
|
|
||||||
display: inline-block; width: 6px; height: 6px; border-radius: 50%;
|
|
||||||
background: #67c23a; margin-right: 5px; vertical-align: middle;
|
|
||||||
&.danger { background: #f56c6c; animation: blink 1s infinite; }
|
|
||||||
&.warning { background: #e6a23c; }
|
|
||||||
}
|
|
||||||
.actions { display: flex; align-items: center; gap: 6px; }
|
|
||||||
.sync-chk { margin-right: 6px; }
|
|
||||||
.proto-warn { margin-top: 8px; }
|
|
||||||
|
|
||||||
.body-row { ::v-deep .el-card__body { padding: 8px 12px; } }
|
|
||||||
|
|
||||||
.side-card {
|
|
||||||
::v-deep .el-card__header { padding: 8px 12px; }
|
|
||||||
}
|
|
||||||
.card-hd {
|
|
||||||
display: flex; justify-content: space-between; align-items: center;
|
|
||||||
font-size: 13px; color: #303133; font-weight: 600;
|
|
||||||
i { margin-right: 4px; color: #409eff; }
|
|
||||||
.hd-count { font-style: normal; color: #909399; font-weight: normal; }
|
|
||||||
.hd-tail-code { font-family: monospace; font-size: 12px; color: #909399; font-weight: normal; }
|
|
||||||
}
|
|
||||||
.filter-row { margin-bottom: 6px; display: flex; align-items: center; }
|
|
||||||
|
|
||||||
.hist-list {
|
|
||||||
margin-top: 4px; max-height: calc(100vh - 360px); overflow-y: auto;
|
|
||||||
display: flex; flex-direction: column; gap: 4px;
|
|
||||||
}
|
|
||||||
.hist-empty {
|
|
||||||
text-align: center; color: #c0c4cc; padding: 30px 0; font-size: 12px;
|
|
||||||
i { font-size: 32px; opacity: 0.4; display: block; margin-bottom: 6px; }
|
|
||||||
}
|
|
||||||
.hist-card {
|
|
||||||
background: #fafbfc; border: 1px solid #ebeef5; border-radius: 4px;
|
|
||||||
padding: 6px 10px; cursor: pointer; transition: 0.15s; font-size: 12px;
|
|
||||||
&:hover { border-color: #409eff; background: #fff; }
|
|
||||||
&.sel { border-color: #409eff; background: #ecf5ff; }
|
|
||||||
.hc-top { display: flex; align-items: center; gap: 6px; margin-bottom: 2px;
|
|
||||||
.hc-code { font-family: monospace; color: #409eff; font-weight: 600; font-size: 11px; }
|
|
||||||
.hc-del { padding: 0; color: #c0c4cc; margin-left: auto;
|
|
||||||
&:hover { color: #f56c6c; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.hc-subject { font-weight: 600; font-size: 13px; color: #303133; margin: 2px 0;
|
|
||||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
||||||
.hc-line { font-size: 11px; color: #909399; line-height: 1.6;
|
|
||||||
i { margin-right: 3px; }
|
|
||||||
.hc-proj { color: #4db8c9; margin-left: 4px; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-card { ::v-deep .el-card__header { padding: 8px 14px; } }
|
|
||||||
|
|
||||||
.meta-form {
|
|
||||||
::v-deep .el-form-item { margin-bottom: 8px; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.voice-card {
|
|
||||||
border-left: 3px solid #f56c6c; margin: 6px 0 10px;
|
|
||||||
::v-deep .el-card__body { padding: 8px 12px; }
|
|
||||||
.voice-hd { display: flex; align-items: center; gap: 8px; margin-bottom: 4px;
|
|
||||||
.live-dot { width: 8px; height: 8px; border-radius: 50%; background: #f56c6c;
|
|
||||||
&.blink { animation: blink 0.7s infinite; }
|
|
||||||
}
|
|
||||||
.voice-title { font-size: 12px; font-weight: 600; color: #e6a23c; }
|
|
||||||
}
|
|
||||||
.voice-interim { font-size: 12px; color: #909399; font-style: italic; min-height: 16px; }
|
|
||||||
.voice-final { font-size: 13px; color: #303133; white-space: pre-wrap;
|
|
||||||
max-height: 100px; overflow-y: auto; padding-top: 4px; margin-top: 4px;
|
|
||||||
border-top: 1px solid #ebeef5;
|
|
||||||
}
|
|
||||||
.voice-actions { display: flex; gap: 6px; margin-top: 6px; flex-wrap: wrap; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.sec-block { margin-top: 10px; }
|
|
||||||
.sec-hd {
|
|
||||||
display: flex; align-items: center; gap: 6px;
|
|
||||||
padding: 6px 10px; background: #f4f7fc; border-radius: 4px 4px 0 0;
|
|
||||||
font-size: 13px; font-weight: 600; color: #409eff;
|
|
||||||
.sec-num { display: inline-block; min-width: 18px; height: 18px; line-height: 18px;
|
|
||||||
text-align: center; background: #409eff; color: #fff; border-radius: 3px; font-size: 11px; }
|
|
||||||
.sec-tip { color: #909399; font-weight: normal; font-size: 11px; margin-left: 8px; }
|
|
||||||
.add-task { margin-left: auto; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-empty {
|
|
||||||
border: 1px dashed #dcdfe6; border-top: none;
|
|
||||||
padding: 14px; text-align: center; color: #c0c4cc; font-size: 12px;
|
|
||||||
border-radius: 0 0 4px 4px;
|
|
||||||
}
|
|
||||||
.task-row {
|
|
||||||
border: 1px solid #ebeef5; border-top: none; padding: 8px 10px;
|
|
||||||
background: #fff;
|
|
||||||
&:last-child { border-radius: 0 0 4px 4px; }
|
|
||||||
}
|
|
||||||
.task-line {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 170px 1fr 140px 110px 100px;
|
|
||||||
gap: 8px; align-items: start;
|
|
||||||
.tf {
|
|
||||||
label { display: block; font-size: 11px; color: #909399; margin-bottom: 2px; }
|
|
||||||
&.tf-act { display: flex; flex-direction: column; align-items: center; gap: 4px;
|
|
||||||
padding-top: 16px; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
|
|
||||||
</style>
|
|
||||||
|
|||||||
Reference in New Issue
Block a user