feat(oa): render instance detail progress from API projection
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
98beade090
commit
265d790ea6
2 files changed
+747
No files matched your search
@@ -0,0 +1,279 @@
|
||||
import request from '@/utils/http'
|
||||
|
||||
const BASE = '/web/admin/oa-approval'
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 工作流角色
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function fetchOaRoleList(params?: { keyword?: string; status?: string }) {
|
||||
return request.get<{ list: any[] }>({
|
||||
url: `${BASE}/roles`,
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchEnsureOaRolePresets() {
|
||||
return request.post<{ createdCount: number }>({
|
||||
url: `${BASE}/roles/ensure-presets`,
|
||||
showSuccessMessage: true
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchCreateOaRole(data: {
|
||||
roleName: string
|
||||
roleCode: string
|
||||
status?: string
|
||||
orderNum?: number
|
||||
remark?: string
|
||||
}) {
|
||||
return request.post({
|
||||
url: `${BASE}/roles`,
|
||||
data,
|
||||
showSuccessMessage: true
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchUpdateOaRole(
|
||||
id: string,
|
||||
data: Partial<{
|
||||
roleName: string
|
||||
status: string
|
||||
orderNum: number
|
||||
remark: string
|
||||
}>
|
||||
) {
|
||||
return request.put({
|
||||
url: `${BASE}/roles/${id}`,
|
||||
data,
|
||||
showSuccessMessage: true
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchDeleteOaRole(id: string) {
|
||||
return request.del({
|
||||
url: `${BASE}/roles/${id}`,
|
||||
showSuccessMessage: true
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchOaRoleUsers(roleId: string) {
|
||||
return request.get<{ list: any[] }>({
|
||||
url: `${BASE}/roles/${roleId}/users`
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchReplaceOaRoleUsers(roleId: string, userIds: string[]) {
|
||||
return request.put<{ list: any[] }>({
|
||||
url: `${BASE}/roles/${roleId}/users`,
|
||||
data: { userIds },
|
||||
showSuccessMessage: true
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 分组 / 应用 / 草稿 / 发布
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function fetchOaGroupList() {
|
||||
return request.get<{ list: any[] }>({ url: `${BASE}/groups` })
|
||||
}
|
||||
|
||||
export function fetchCreateOaGroup(data: {
|
||||
groupName: string
|
||||
orderNum?: number
|
||||
remark?: string
|
||||
}) {
|
||||
return request.post({
|
||||
url: `${BASE}/groups`,
|
||||
data,
|
||||
showSuccessMessage: true
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchUpdateOaGroup(
|
||||
id: string,
|
||||
data: Partial<{ groupName: string; orderNum: number; remark: string }>
|
||||
) {
|
||||
return request.put({
|
||||
url: `${BASE}/groups/${id}`,
|
||||
data,
|
||||
showSuccessMessage: true
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchDeleteOaGroup(id: string) {
|
||||
return request.del({
|
||||
url: `${BASE}/groups/${id}`,
|
||||
showSuccessMessage: true
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchOaAppList(params?: {
|
||||
groupId?: string
|
||||
keyword?: string
|
||||
status?: string
|
||||
}) {
|
||||
return request.get<{ list: any[] }>({
|
||||
url: `${BASE}/apps`,
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchCreateOaApp(data: Record<string, any>) {
|
||||
return request.post({
|
||||
url: `${BASE}/apps`,
|
||||
data,
|
||||
showSuccessMessage: true
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchUpdateOaApp(id: string, data: Record<string, any>) {
|
||||
return request.put({
|
||||
url: `${BASE}/apps/${id}`,
|
||||
data,
|
||||
showSuccessMessage: true
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchDeleteOaApp(id: string) {
|
||||
return request.del({
|
||||
url: `${BASE}/apps/${id}`,
|
||||
showSuccessMessage: true
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchOaAppDraft(appId: string) {
|
||||
return request.get<any>({ url: `${BASE}/apps/${appId}/draft` })
|
||||
}
|
||||
|
||||
export function fetchSaveOaAppDraft(
|
||||
appId: string,
|
||||
data: {
|
||||
basic?: Record<string, any>
|
||||
formSchemaJson?: any
|
||||
flowTreeJson?: any
|
||||
advancedSettingsJson?: any
|
||||
}
|
||||
) {
|
||||
return request.put({
|
||||
url: `${BASE}/apps/${appId}/draft`,
|
||||
data,
|
||||
showSuccessMessage: true
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchPublishOaApp(appId: string) {
|
||||
return request.post({
|
||||
url: `${BASE}/apps/${appId}/publish`,
|
||||
showSuccessMessage: true
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 运行时
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function fetchOaTemplates() {
|
||||
return request.get<{ list: any[] }>({ url: `${BASE}/templates` })
|
||||
}
|
||||
|
||||
export function fetchOaTemplateDetail(appId: string) {
|
||||
return request.get<any>({ url: `${BASE}/templates/${appId}` })
|
||||
}
|
||||
|
||||
/** 发起前审批流预览(按当前登录人解析审批人) */
|
||||
export function fetchOaFlowPreview(appId: string, initiatorSelectUserIds?: string[]) {
|
||||
return request.get<{ steps: OaFlowPreviewStep[] }>({
|
||||
url: `${BASE}/templates/${appId}/flow-preview`,
|
||||
params: initiatorSelectUserIds?.length
|
||||
? { initiatorSelectUserIds: initiatorSelectUserIds.join(',') }
|
||||
: undefined
|
||||
})
|
||||
}
|
||||
|
||||
export type OaFlowPreviewStep = {
|
||||
id: string
|
||||
type: 'start' | 'approval' | 'cc' | 'branch' | 'end' | string
|
||||
name: string
|
||||
signMode?: string
|
||||
assigneeType?: string
|
||||
meta?: string
|
||||
people?: Array<{ id: string; nickName: string }>
|
||||
resolveError?: string
|
||||
}
|
||||
|
||||
export function fetchStartOaInstance(data: {
|
||||
appId: string
|
||||
formData?: Record<string, any>
|
||||
title?: string
|
||||
initiatorDeptId?: string
|
||||
initiatorSelectUserIds?: string[]
|
||||
relatedInstanceIds?: string[]
|
||||
}) {
|
||||
return request.post({
|
||||
url: `${BASE}/instances`,
|
||||
data,
|
||||
showSuccessMessage: true
|
||||
})
|
||||
}
|
||||
|
||||
/** 详情审批进度投影项(后端 build_progress_items) */
|
||||
export interface OaProgressItem {
|
||||
kind: 'submit' | 'task' | 'pending_node' | 'terminal' | string
|
||||
roundNo?: number
|
||||
title?: string
|
||||
status?: string
|
||||
assigneeUserId?: string
|
||||
assigneeUserName?: string
|
||||
timestamp?: string
|
||||
commentText?: string
|
||||
nodeKey?: string
|
||||
taskType?: string
|
||||
resolveSource?: string
|
||||
taskId?: string
|
||||
}
|
||||
|
||||
export function fetchOaInstanceDetail(id: string) {
|
||||
return request.get<{ progress?: OaProgressItem[]; tasks?: any[] } & Record<string, any>>({
|
||||
url: `${BASE}/instances/${id}`
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchOaTasks(kind: 'pending' | 'done' | 'mine' | 'cc') {
|
||||
return request.get<{ list: any[] }>({ url: `${BASE}/tasks/${kind}` })
|
||||
}
|
||||
|
||||
export function fetchApproveOaTask(taskId: string, comment?: string) {
|
||||
return request.post({
|
||||
url: `${BASE}/tasks/${taskId}/approve`,
|
||||
data: { comment },
|
||||
showSuccessMessage: true
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchRejectOaTask(taskId: string, comment?: string) {
|
||||
return request.post({
|
||||
url: `${BASE}/tasks/${taskId}/reject`,
|
||||
data: { comment },
|
||||
showSuccessMessage: true
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchAddSignOaTask(
|
||||
taskId: string,
|
||||
data: { userIds: string[]; comment?: string }
|
||||
) {
|
||||
return request.post({
|
||||
url: `${BASE}/tasks/${taskId}/add-sign`,
|
||||
data,
|
||||
showSuccessMessage: true
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchWithdrawOaInstance(instanceId: string) {
|
||||
return request.post({
|
||||
url: `${BASE}/instances/${instanceId}/withdraw`,
|
||||
showSuccessMessage: true
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,468 @@
|
||||
<!-- 审批详情 -->
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<section class="panel">
|
||||
<div class="panel-top oa-detail__top">
|
||||
<div class="oa-detail__title-wrap">
|
||||
<el-button class="btn-ghost" @click="goBack">
|
||||
<el-icon><ArrowLeft /></el-icon>
|
||||
返回
|
||||
</el-button>
|
||||
<h2 class="title-bar-heading">{{ detail?.title || '审批详情' }}</h2>
|
||||
<el-tag v-if="detail?.status" size="small" :type="statusType(detail.status)">
|
||||
{{ statusText(detail.status) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="oa-detail__actions">
|
||||
<el-button
|
||||
v-if="canApprove"
|
||||
class="btn-main"
|
||||
type="primary"
|
||||
:loading="acting"
|
||||
@click="act('approve')"
|
||||
>
|
||||
<el-icon><Check /></el-icon>
|
||||
同意
|
||||
</el-button>
|
||||
<el-button v-if="canApprove" class="btn-ghost" :loading="acting" @click="act('reject')">
|
||||
<el-icon><Close /></el-icon>
|
||||
拒绝
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="canApprove && detail?.allowAddSign !== false"
|
||||
class="btn-ghost"
|
||||
:loading="acting"
|
||||
@click="openAddSign"
|
||||
>
|
||||
<el-icon><Plus /></el-icon>
|
||||
加签
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="detail?.status === 'running' && isInitiator"
|
||||
class="btn-ghost"
|
||||
:loading="acting"
|
||||
@click="withdraw"
|
||||
>
|
||||
<el-icon><RefreshLeft /></el-icon>
|
||||
撤销
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-loading="loading" class="oa-detail__body">
|
||||
<section class="oa-detail__section">
|
||||
<h3>表单内容</h3>
|
||||
<div class="oa-detail__form-wrap">
|
||||
<form-create
|
||||
v-if="viewRule.length"
|
||||
v-model="viewFormData"
|
||||
:rule="viewRule"
|
||||
:option="viewOption"
|
||||
/>
|
||||
<pre v-else class="oa-detail__json">{{ prettyForm }}</pre>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="oa-detail__section">
|
||||
<h3>审批进度</h3>
|
||||
<div class="oa-detail__timeline-wrap">
|
||||
<el-timeline v-if="timelineItems.length">
|
||||
<el-timeline-item
|
||||
v-for="item in timelineItems"
|
||||
:key="item.key"
|
||||
:type="timelineType(item.status)"
|
||||
:timestamp="item.timestamp"
|
||||
placement="top"
|
||||
>
|
||||
<div class="oa-tl">
|
||||
<div class="oa-tl__title">
|
||||
{{ item.title }}
|
||||
<el-tag size="small" :type="statusType(item.status)">
|
||||
{{ statusText(item.status) }}
|
||||
</el-tag>
|
||||
<el-tag v-if="item.taskType === 'cc'" size="small" type="info">抄送</el-tag>
|
||||
</div>
|
||||
<div
|
||||
v-if="item.assignee && item.kind !== 'pending_node' && item.kind !== 'terminal'"
|
||||
class="oa-tl__meta"
|
||||
>
|
||||
处理人:{{ item.assignee }}
|
||||
</div>
|
||||
<div v-if="item.resolveSource" class="oa-tl__meta">
|
||||
解析来源:{{ resolveSourceText(item.resolveSource) }}
|
||||
</div>
|
||||
<div v-if="item.commentText" class="oa-tl__comment">意见:{{ item.commentText }}</div>
|
||||
</div>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
<p v-else class="oa-detail__empty">暂无任务记录</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<el-dialog
|
||||
v-model="addSignVisible"
|
||||
title="加签"
|
||||
width="520px"
|
||||
class="dialog-form-layout"
|
||||
align-center
|
||||
append-to-body
|
||||
modal-class="protocol-dialog-mask"
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form label-position="top">
|
||||
<el-form-item label="加签人">
|
||||
<el-select
|
||||
v-model="addSignUserIds"
|
||||
multiple
|
||||
filterable
|
||||
collapse-tags
|
||||
collapse-tags-tooltip
|
||||
placeholder="选择加签用户"
|
||||
style="width: 100%"
|
||||
:loading="userLoading"
|
||||
>
|
||||
<el-option
|
||||
v-for="u in userOptions"
|
||||
:key="u.id"
|
||||
:label="`${u.nickName}(${u.userNo})`"
|
||||
:value="u.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="说明(可选)">
|
||||
<el-input v-model="addSignComment" type="textarea" :rows="2" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button class="btn-ghost" @click="addSignVisible = false">
|
||||
<el-icon><Close /></el-icon>
|
||||
取消
|
||||
</el-button>
|
||||
<el-button class="btn-main" type="primary" :loading="acting" @click="submitAddSign">
|
||||
<el-icon><Check /></el-icon>
|
||||
确定
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { ArrowLeft, Check, Close, RefreshLeft, Plus } from '@element-plus/icons-vue'
|
||||
import { fetchUserList } from '@/api/admin'
|
||||
import {
|
||||
fetchAddSignOaTask,
|
||||
fetchApproveOaTask,
|
||||
fetchOaInstanceDetail,
|
||||
fetchRejectOaTask,
|
||||
fetchWithdrawOaInstance
|
||||
} from '@/api/oaApproval'
|
||||
import { MAX_ADMIN_LIST_PAGE_SIZE } from '@/utils/fetch-paged-list'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
|
||||
defineOptions({ name: 'OaApprovalInstanceDetail' })
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
const loading = ref(false)
|
||||
const acting = ref(false)
|
||||
const detail = ref<any>(null)
|
||||
const viewFormData = ref<Record<string, any>>({})
|
||||
const viewRule = ref<any[]>([])
|
||||
const viewOption = ref({
|
||||
form: { disabled: true, labelPosition: 'top' },
|
||||
submitBtn: false,
|
||||
resetBtn: false
|
||||
})
|
||||
const addSignVisible = ref(false)
|
||||
const addSignUserIds = ref<string[]>([])
|
||||
const addSignComment = ref('')
|
||||
const userLoading = ref(false)
|
||||
const userOptions = ref<Array<{ id: string; nickName: string; userNo: string }>>([])
|
||||
|
||||
const prettyForm = computed(() => JSON.stringify(detail.value?.formDataJson || {}, null, 2))
|
||||
|
||||
/** 进度以详情 API progress 投影为准(勿回落无序 tasks) */
|
||||
const timelineItems = computed(() => {
|
||||
const list = detail.value?.progress
|
||||
if (!Array.isArray(list) || !list.length) return []
|
||||
return list.map((p: any, idx: number) => ({
|
||||
key: `${p.kind}-${p.taskId || p.nodeKey || idx}-${p.roundNo || 1}`,
|
||||
title: p.title || '-',
|
||||
status: p.status || 'info',
|
||||
timestamp: p.timestamp || '',
|
||||
assignee: p.assigneeUserName || p.assigneeUserId || '',
|
||||
resolveSource: p.resolveSource || '',
|
||||
commentText: p.commentText || '',
|
||||
taskType: p.taskType || '',
|
||||
kind: p.kind || '',
|
||||
roundNo: p.roundNo || 1
|
||||
}))
|
||||
})
|
||||
|
||||
const currentUserId = computed(() => {
|
||||
const info = userStore.info || userStore.getUserInfo
|
||||
return String(info?.id || info?.userId || '').trim()
|
||||
})
|
||||
|
||||
const isInitiator = computed(
|
||||
() => !!detail.value && detail.value.initiatorUserId === currentUserId.value
|
||||
)
|
||||
|
||||
const myPendingTask = computed(() => {
|
||||
const uid = currentUserId.value
|
||||
const list = detail.value?.tasks || []
|
||||
return list.find(
|
||||
(t: any) => t.taskType === 'approval' && t.status === 'pending' && t.assigneeUserId === uid
|
||||
)
|
||||
})
|
||||
|
||||
const canApprove = computed(() => !!myPendingTask.value)
|
||||
|
||||
function statusType(s: string) {
|
||||
if (s === 'pending' || s === 'running') return 'warning'
|
||||
if (s === 'approved' || s === 'completed' || s === 'done' || s === 'submitted') return 'success'
|
||||
if (s === 'rejected' || s === 'cancelled') return 'danger'
|
||||
if (s === 'not_reached') return 'info'
|
||||
return 'info'
|
||||
}
|
||||
|
||||
function statusText(s: string) {
|
||||
const map: Record<string, string> = {
|
||||
pending: '待处理',
|
||||
running: '进行中',
|
||||
approved: '已同意',
|
||||
rejected: '已拒绝',
|
||||
completed: '已完成',
|
||||
cancelled: '已撤销',
|
||||
done: '已结束',
|
||||
submitted: '已提交',
|
||||
not_reached: '未到达'
|
||||
}
|
||||
return map[s] || s
|
||||
}
|
||||
|
||||
function timelineType(s: string) {
|
||||
if (s === 'approved' || s === 'completed' || s === 'done' || s === 'submitted') return 'success'
|
||||
if (s === 'rejected' || s === 'cancelled') return 'danger'
|
||||
if (s === 'pending') return 'warning'
|
||||
if (s === 'not_reached') return 'info'
|
||||
return 'primary'
|
||||
}
|
||||
|
||||
function resolveSourceText(src: string) {
|
||||
const map: Record<string, string> = {
|
||||
users: '指定成员',
|
||||
oa_roles: '工作流角色',
|
||||
dept_leader: '部门负责人',
|
||||
direct_supervisor: '直属主管',
|
||||
supervisor_fallback_dept_leader: '部门负责人(主管回退)',
|
||||
initiator_select: '发起人自选'
|
||||
}
|
||||
return map[src] || src
|
||||
}
|
||||
|
||||
function applySchema(schema: any, data: any) {
|
||||
viewFormData.value = { ...(data || {}) }
|
||||
const rule = Array.isArray(schema?.rule) ? schema.rule : Array.isArray(schema) ? schema : []
|
||||
viewRule.value = rule
|
||||
viewOption.value = {
|
||||
...(schema?.option || {}),
|
||||
form: { ...(schema?.option?.form || {}), disabled: true, labelPosition: 'top' },
|
||||
submitBtn: false,
|
||||
resetBtn: false
|
||||
}
|
||||
}
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
detail.value = await fetchOaInstanceDetail(String(route.params.id || ''))
|
||||
applySchema(detail.value?.formSchemaJson, detail.value?.formDataJson)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function act(kind: 'approve' | 'reject') {
|
||||
const task = myPendingTask.value
|
||||
if (!task) return
|
||||
let comment = ''
|
||||
try {
|
||||
const res = await ElMessageBox.prompt(
|
||||
'请输入审批意见(可选)',
|
||||
kind === 'approve' ? '同意' : '拒绝',
|
||||
{
|
||||
inputPlaceholder: '意见',
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消'
|
||||
}
|
||||
)
|
||||
comment = res.value || ''
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
acting.value = true
|
||||
try {
|
||||
if (kind === 'approve') await fetchApproveOaTask(task.id, comment || undefined)
|
||||
else await fetchRejectOaTask(task.id, comment || undefined)
|
||||
await load()
|
||||
} finally {
|
||||
acting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function withdraw() {
|
||||
await ElMessageBox.confirm('确定撤销该审批吗?', '提示', { type: 'warning' })
|
||||
acting.value = true
|
||||
try {
|
||||
await fetchWithdrawOaInstance(String(route.params.id || ''))
|
||||
await load()
|
||||
} finally {
|
||||
acting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadUsers() {
|
||||
userLoading.value = true
|
||||
try {
|
||||
const pageSize = MAX_ADMIN_LIST_PAGE_SIZE
|
||||
const raw: any[] = []
|
||||
let page = 1
|
||||
for (;;) {
|
||||
const res = await fetchUserList({ page, pageSize, status: '1' })
|
||||
const list = res?.list || []
|
||||
raw.push(...list)
|
||||
if (list.length < pageSize) break
|
||||
if (typeof res?.total === 'number' && raw.length >= res.total) break
|
||||
page += 1
|
||||
}
|
||||
userOptions.value = raw.map((u) => ({
|
||||
id: u.id,
|
||||
nickName: u.nickName || '',
|
||||
userNo: u.userNo || ''
|
||||
}))
|
||||
} finally {
|
||||
userLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function openAddSign() {
|
||||
addSignUserIds.value = []
|
||||
addSignComment.value = ''
|
||||
addSignVisible.value = true
|
||||
if (!userOptions.value.length) await loadUsers()
|
||||
}
|
||||
|
||||
async function submitAddSign() {
|
||||
const task = myPendingTask.value
|
||||
if (!task) return
|
||||
if (!addSignUserIds.value.length) {
|
||||
ElMessage.warning('请选择加签人')
|
||||
return
|
||||
}
|
||||
acting.value = true
|
||||
try {
|
||||
await fetchAddSignOaTask(task.id, {
|
||||
userIds: addSignUserIds.value,
|
||||
comment: addSignComment.value || undefined
|
||||
})
|
||||
addSignVisible.value = false
|
||||
await load()
|
||||
} finally {
|
||||
acting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
router.back()
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.oa-detail__top {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.oa-detail__title-wrap {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.oa-detail__actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.oa-detail__body {
|
||||
padding: 0 20px 20px;
|
||||
}
|
||||
|
||||
.oa-detail__section {
|
||||
max-width: 960px;
|
||||
margin: 0 auto;
|
||||
|
||||
h3 {
|
||||
margin: 16px 0 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--mint-text, #0f172a);
|
||||
}
|
||||
}
|
||||
|
||||
.oa-detail__form-wrap {
|
||||
box-sizing: border-box;
|
||||
padding: 4px 0 8px;
|
||||
}
|
||||
|
||||
.oa-detail__json {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: auto;
|
||||
font-size: 12px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.oa-detail__empty {
|
||||
color: var(--mint-text-muted, #94a3b8);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.oa-tl__title {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.oa-tl__meta {
|
||||
margin-top: 4px;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.oa-tl__comment {
|
||||
margin-top: 6px;
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user