831 lines
21 KiB
Vue
831 lines
21 KiB
Vue
<!-- FormCreate 自定义控件:关联审批单(Fresh Mint) -->
|
||
<template>
|
||
<div class="oa-related-approval" :class="{ 'oa-related-approval--stack': isStack }">
|
||
<div v-if="!ids.length" class="oa-related-approval__empty">
|
||
<span class="oa-related-approval__empty-text">暂未关联审批单</span>
|
||
<el-button
|
||
v-if="!disabled"
|
||
type="primary"
|
||
link
|
||
class="oa-related-approval__add"
|
||
@click="openPicker"
|
||
>
|
||
<el-icon><Plus /></el-icon>
|
||
添加关联
|
||
</el-button>
|
||
</div>
|
||
|
||
<div v-else class="oa-related-approval__list">
|
||
<div
|
||
v-for="item in displayItems"
|
||
:key="item.id"
|
||
class="oa-related-approval__card"
|
||
>
|
||
<div class="oa-related-approval__head">
|
||
<button
|
||
type="button"
|
||
class="oa-related-approval__title"
|
||
:disabled="item.unavailable"
|
||
@click="goDetail(item.id)"
|
||
>
|
||
{{ item.unavailable ? '无法查看' : item.appName || '审批单' }}
|
||
<template v-if="!item.unavailable && item.applyTime">
|
||
· {{ formatShortTime(item.applyTime) }}
|
||
</template>
|
||
</button>
|
||
<div class="oa-related-approval__actions">
|
||
<!-- 原生 button:避免 form.disabled 时 el-button 继承禁用 -->
|
||
<button
|
||
v-if="!item.unavailable && (item.fields?.length || 0) > COLLAPSE_FIELD_COUNT"
|
||
type="button"
|
||
class="oa-related-approval__toggle"
|
||
@click.stop="toggleExpand(item.id)"
|
||
>
|
||
{{ expanded.has(item.id) ? '收起' : '展开' }}
|
||
</button>
|
||
<el-button
|
||
v-if="!disabled"
|
||
link
|
||
type="danger"
|
||
@click="removeId(item.id)"
|
||
>
|
||
删除
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
<div
|
||
v-if="!item.unavailable && visibleFields(item).length"
|
||
class="oa-related-approval__fields"
|
||
>
|
||
<span
|
||
v-for="(f, idx) in visibleFields(item)"
|
||
:key="`${item.id}-${idx}`"
|
||
class="oa-related-approval__field"
|
||
:class="{ 'oa-related-approval__field--full': f.fullWidth && expanded.has(item.id) }"
|
||
>
|
||
<span class="oa-related-approval__field-label">{{ f.label }}</span>
|
||
<span class="oa-related-approval__field-value">
|
||
<template v-if="splitUrls(f.value).length">
|
||
<a
|
||
v-for="(u, ui) in splitUrls(f.value)"
|
||
:key="`${item.id}-${idx}-${ui}`"
|
||
class="oa-related-approval__link"
|
||
:href="toFullUrl(u)"
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
@click.stop
|
||
>
|
||
{{ displayUrlLabel(u, ui, splitUrls(f.value).length) }}
|
||
</a>
|
||
</template>
|
||
<template v-else-if="(f.relatedIds?.length || 0) > 0">
|
||
<button
|
||
type="button"
|
||
class="oa-related-approval__link-btn"
|
||
@click.stop="goDetail(f.relatedIds![0])"
|
||
>
|
||
{{ f.value }}
|
||
</button>
|
||
</template>
|
||
<template v-else>{{ f.value }}</template>
|
||
</span>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<el-button
|
||
v-if="!disabled"
|
||
class="oa-related-approval__add-more"
|
||
@click="openPicker"
|
||
>
|
||
<el-icon><Plus /></el-icon>
|
||
添加关联
|
||
</el-button>
|
||
</div>
|
||
|
||
<el-dialog
|
||
v-model="pickerVisible"
|
||
title="选择关联审批单"
|
||
width="720px"
|
||
append-to-body
|
||
align-center
|
||
class="dialog-form-layout"
|
||
modal-class="protocol-dialog-mask"
|
||
destroy-on-close
|
||
@opened="loadCandidates"
|
||
>
|
||
<div class="oa-related-picker">
|
||
<div class="oa-related-picker__tabs" role="tablist">
|
||
<button
|
||
v-for="t in tabOptions"
|
||
:key="t.value"
|
||
type="button"
|
||
class="oa-related-picker__tab"
|
||
:class="{ 'is-active': activeTab === t.value }"
|
||
role="tab"
|
||
@click="onTabChange(t.value)"
|
||
>
|
||
{{ t.label }}
|
||
</button>
|
||
</div>
|
||
<div class="oa-related-picker__filters">
|
||
<el-input
|
||
v-model="keyword"
|
||
clearable
|
||
placeholder="搜索流程名 / 标题"
|
||
@keyup.enter="onSearch"
|
||
>
|
||
<template #append>
|
||
<el-button @click="onSearch">
|
||
<el-icon><Search /></el-icon>
|
||
</el-button>
|
||
</template>
|
||
</el-input>
|
||
</div>
|
||
<div v-loading="pickerLoading" class="oa-related-picker__list">
|
||
<div
|
||
v-for="row in candidates"
|
||
:key="row.id"
|
||
class="oa-related-picker__card"
|
||
:class="{
|
||
'is-selected': pendingIds.includes(row.id),
|
||
'is-disabled': !rowSelectable(row)
|
||
}"
|
||
@click="togglePending(row)"
|
||
>
|
||
<el-checkbox
|
||
:model-value="pendingIds.includes(row.id)"
|
||
:disabled="!rowSelectable(row)"
|
||
@click.stop
|
||
@change="() => togglePending(row)"
|
||
/>
|
||
<div
|
||
class="oa-related-picker__avatar"
|
||
:style="{ background: avatarColor(row.initiatorUserName) }"
|
||
>
|
||
{{ shortName(row.initiatorUserName) }}
|
||
</div>
|
||
<div class="oa-related-picker__main">
|
||
<div class="oa-related-picker__title">
|
||
{{ row.displayTitle || row.appName || '审批单' }}
|
||
</div>
|
||
<div v-if="row.initiatorDeptName" class="oa-related-picker__meta">
|
||
部门:{{ row.initiatorDeptName }}
|
||
</div>
|
||
<div
|
||
v-for="(f, i) in (row.previewFields || []).slice(0, 2)"
|
||
:key="i"
|
||
class="oa-related-picker__meta"
|
||
>
|
||
{{ f.label }}:{{ f.value }}
|
||
</div>
|
||
<div class="oa-related-picker__time">{{ row.applyTime }}</div>
|
||
</div>
|
||
<div class="oa-related-picker__right" :class="statusClass(row.status)">
|
||
<template v-if="row.status === 'running'">
|
||
<div v-if="row.currentApproverNames">
|
||
{{ row.currentApproverNames }}(审批人)
|
||
</div>
|
||
<div>{{ row.statusText || '处理中' }}</div>
|
||
</template>
|
||
<template v-else>{{ row.statusText || row.status }}</template>
|
||
</div>
|
||
</div>
|
||
<el-empty
|
||
v-if="!pickerLoading && !candidates.length"
|
||
description="暂无审批单"
|
||
:image-size="64"
|
||
/>
|
||
</div>
|
||
<div class="oa-related-picker__pager">
|
||
<div class="oa-related-picker__selected">
|
||
已选 <em>{{ pendingIds.length }}</em> 项
|
||
</div>
|
||
<el-pagination
|
||
v-if="pickerTotal > pageSize"
|
||
:current-page="page"
|
||
:page-size="pageSize"
|
||
layout="prev, pager, next"
|
||
:total="pickerTotal"
|
||
@current-change="onPageChange"
|
||
/>
|
||
</div>
|
||
</div>
|
||
<template #footer>
|
||
<el-button @click="pickerVisible = false">
|
||
<el-icon><Close /></el-icon>
|
||
取消
|
||
</el-button>
|
||
<el-button type="primary" :disabled="!pendingIds.length" @click="confirmPicker">
|
||
<el-icon><Check /></el-icon>
|
||
确定
|
||
</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<OaInstanceDetailDialog
|
||
v-model="detailVisible"
|
||
:instance-id="detailInstanceId"
|
||
/>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||
import { Check, Close, Plus, Search } from '@element-plus/icons-vue'
|
||
import {
|
||
fetchOaRelatedCandidates,
|
||
fetchOaRelatedSummaries,
|
||
type OaRelatedCandidate,
|
||
type OaRelatedSummary
|
||
} from '@/api/oaApproval'
|
||
import { attachmentFullUrl } from '@/composables/useAttachmentActions'
|
||
import OaInstanceDetailDialog from '@/components/oa-approval/OaInstanceDetailDialog.vue'
|
||
|
||
defineOptions({ name: 'relatedApproval' })
|
||
|
||
const COLLAPSE_FIELD_COUNT = 3
|
||
const AVATAR_COLORS = ['#3b82f6', '#10b981', '#8b5cf6', '#f59e0b', '#06b6d4', '#ef4444']
|
||
|
||
const props = defineProps<{
|
||
modelValue?: string[] | string | null
|
||
disabled?: boolean
|
||
formCreateInject?: Record<string, unknown>
|
||
}>()
|
||
|
||
const emit = defineEmits<{
|
||
(e: 'update:modelValue', v: string[]): void
|
||
(e: 'change', v: string[]): void
|
||
}>()
|
||
|
||
const expanded = ref(new Set<string>())
|
||
const summaries = ref<OaRelatedSummary[]>([])
|
||
const isStack = ref(false)
|
||
let mql: MediaQueryList | null = null
|
||
|
||
const detailVisible = ref(false)
|
||
const detailInstanceId = ref('')
|
||
|
||
const pickerVisible = ref(false)
|
||
const pickerLoading = ref(false)
|
||
const keyword = ref('')
|
||
const page = ref(1)
|
||
const pageSize = 10
|
||
const candidates = ref<OaRelatedCandidate[]>([])
|
||
const pickerTotal = ref(0)
|
||
const pendingIds = ref<string[]>([])
|
||
const activeTab = ref<'mine' | 'done' | 'cc'>('mine')
|
||
const tabOptions = [
|
||
{ value: 'mine' as const, label: '我发起的' },
|
||
{ value: 'done' as const, label: '我审批的' },
|
||
{ value: 'cc' as const, label: '抄送我的' }
|
||
]
|
||
const ids = computed(() => {
|
||
const raw = props.modelValue
|
||
if (Array.isArray(raw)) return raw.map((x) => String(x || '').trim()).filter(Boolean)
|
||
if (raw == null || raw === '') return []
|
||
return [String(raw).trim()].filter(Boolean)
|
||
})
|
||
|
||
const displayItems = computed(() => {
|
||
const map = new Map(summaries.value.map((s) => [s.id, s]))
|
||
return ids.value.map((id) => {
|
||
const s = map.get(id)
|
||
if (s) return s
|
||
return {
|
||
id,
|
||
unavailable: false,
|
||
appName: '加载中…',
|
||
applyTime: '',
|
||
status: '',
|
||
fields: []
|
||
} as OaRelatedSummary
|
||
})
|
||
})
|
||
|
||
function emitValue(next: string[]) {
|
||
const uniq: string[] = []
|
||
const seen = new Set<string>()
|
||
for (const id of next) {
|
||
const t = String(id || '').trim()
|
||
if (!t || seen.has(t)) continue
|
||
seen.add(t)
|
||
uniq.push(t)
|
||
}
|
||
emit('update:modelValue', uniq)
|
||
emit('change', uniq)
|
||
}
|
||
|
||
function visibleFields(item: OaRelatedSummary) {
|
||
const fields = item.fields || []
|
||
if (expanded.value.has(item.id)) return fields
|
||
return fields.slice(0, COLLAPSE_FIELD_COUNT)
|
||
}
|
||
|
||
/** 顿号/逗号分隔的 URL 列表;非 URL 文本返回空数组走纯文本展示 */
|
||
function splitUrls(value: string): string[] {
|
||
const raw = String(value || '').trim()
|
||
if (!raw) return []
|
||
const parts = raw
|
||
.split(/[、,,\s]+/)
|
||
.map((s) => s.trim())
|
||
.filter(Boolean)
|
||
if (!parts.length) return []
|
||
const looksLikeUrl = parts.every((p) => {
|
||
if (/^https?:\/\//i.test(p) || p.startsWith('/')) return true
|
||
// 纯数字/金额小数(如 100.00)不是附件 URL
|
||
if (/^\d+(\.\d+)?$/.test(p)) return false
|
||
// 扩展名须含字母,避免把 .00/.56 误判为文件后缀
|
||
return /\.[a-z][a-z0-9]{0,7}(\?|$)/i.test(p)
|
||
})
|
||
return looksLikeUrl ? parts : []
|
||
}
|
||
|
||
function toFullUrl(u: string) {
|
||
return attachmentFullUrl(u) || u
|
||
}
|
||
|
||
function displayUrlLabel(u: string, index: number, total: number) {
|
||
const name = u.split('?')[0].split('/').filter(Boolean).pop() || u
|
||
const label = decodeURIComponent(name)
|
||
return total > 1 ? `${index + 1}. ${label}` : label
|
||
}
|
||
|
||
function toggleExpand(id: string) {
|
||
const next = new Set(expanded.value)
|
||
if (next.has(id)) next.delete(id)
|
||
else next.add(id)
|
||
expanded.value = next
|
||
}
|
||
|
||
function removeId(id: string) {
|
||
emitValue(ids.value.filter((x) => x !== id))
|
||
}
|
||
|
||
function formatShortTime(t: string) {
|
||
if (!t) return ''
|
||
// YYYY-MM-DD HH:mm:ss → MM-DD
|
||
const m = t.match(/(\d{4})-(\d{2})-(\d{2})/)
|
||
if (m) return `${m[2]}-${m[3]}`
|
||
return t
|
||
}
|
||
|
||
function goDetail(id: string) {
|
||
if (!id) return
|
||
detailInstanceId.value = id
|
||
detailVisible.value = true
|
||
}
|
||
|
||
async function loadSummaries() {
|
||
const list = ids.value
|
||
if (!list.length) {
|
||
summaries.value = []
|
||
return
|
||
}
|
||
try {
|
||
const mode = props.disabled ? 'view' : 'view'
|
||
const res: any = await fetchOaRelatedSummaries(list, mode)
|
||
summaries.value = Array.isArray(res?.list) ? res.list : Array.isArray(res) ? res : []
|
||
} catch {
|
||
summaries.value = list.map((id) => ({
|
||
id,
|
||
unavailable: true,
|
||
appName: '',
|
||
applyTime: '',
|
||
status: '',
|
||
fields: []
|
||
}))
|
||
}
|
||
}
|
||
|
||
function openPicker() {
|
||
if (props.disabled) return
|
||
activeTab.value = 'mine'
|
||
keyword.value = ''
|
||
page.value = 1
|
||
pendingIds.value = []
|
||
pickerVisible.value = true
|
||
}
|
||
|
||
async function loadCandidates() {
|
||
pickerLoading.value = true
|
||
try {
|
||
const res: any = await fetchOaRelatedCandidates({
|
||
tab: activeTab.value,
|
||
keyword: keyword.value.trim() || undefined,
|
||
page: page.value,
|
||
pageSize
|
||
})
|
||
candidates.value = Array.isArray(res?.list) ? res.list : []
|
||
pickerTotal.value = Number(res?.total || 0)
|
||
} catch {
|
||
candidates.value = []
|
||
pickerTotal.value = 0
|
||
} finally {
|
||
pickerLoading.value = false
|
||
}
|
||
}
|
||
|
||
function onTabChange(tab: 'mine' | 'done' | 'cc') {
|
||
if (activeTab.value === tab) return
|
||
activeTab.value = tab
|
||
page.value = 1
|
||
pendingIds.value = []
|
||
void loadCandidates()
|
||
}
|
||
|
||
function onSearch() {
|
||
page.value = 1
|
||
void loadCandidates()
|
||
}
|
||
|
||
function rowSelectable(row: OaRelatedCandidate) {
|
||
return !ids.value.includes(String(row?.id || ''))
|
||
}
|
||
|
||
function togglePending(row: OaRelatedCandidate) {
|
||
if (!rowSelectable(row)) return
|
||
const id = String(row.id || '')
|
||
if (!id) return
|
||
const idx = pendingIds.value.indexOf(id)
|
||
if (idx >= 0) pendingIds.value = pendingIds.value.filter((x) => x !== id)
|
||
else pendingIds.value = [...pendingIds.value, id]
|
||
}
|
||
|
||
function shortName(name?: string) {
|
||
const s = String(name || '').trim()
|
||
if (!s) return '?'
|
||
return s.length <= 2 ? s : s.slice(-2)
|
||
}
|
||
|
||
function avatarColor(name?: string) {
|
||
const s = String(name || '')
|
||
let h = 0
|
||
for (let i = 0; i < s.length; i++) h = (h + s.charCodeAt(i) * 17) % AVATAR_COLORS.length
|
||
return AVATAR_COLORS[h]
|
||
}
|
||
|
||
function statusClass(status?: string) {
|
||
const s = String(status || '').trim()
|
||
if (s === 'completed') return 'is-success'
|
||
if (s === 'rejected') return 'is-danger'
|
||
if (s === 'cancelled') return 'is-muted'
|
||
return 'is-warning'
|
||
}
|
||
|
||
function onPageChange(p: number) {
|
||
page.value = p
|
||
void loadCandidates()
|
||
}
|
||
|
||
function confirmPicker() {
|
||
emitValue([...ids.value, ...pendingIds.value])
|
||
pickerVisible.value = false
|
||
}
|
||
|
||
function syncStack(e?: MediaQueryList | MediaQueryListEvent) {
|
||
const q = e && 'matches' in e ? e : mql
|
||
isStack.value = !!(q && q.matches)
|
||
}
|
||
|
||
watch(
|
||
() => ids.value.join(','),
|
||
() => {
|
||
void loadSummaries()
|
||
},
|
||
{ immediate: true }
|
||
)
|
||
|
||
onMounted(() => {
|
||
if (typeof window !== 'undefined' && window.matchMedia) {
|
||
mql = window.matchMedia('(max-width: 768px)')
|
||
syncStack(mql)
|
||
mql.addEventListener?.('change', syncStack)
|
||
}
|
||
})
|
||
|
||
onBeforeUnmount(() => {
|
||
mql?.removeEventListener?.('change', syncStack)
|
||
})
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.oa-related-approval {
|
||
width: 100%;
|
||
}
|
||
|
||
.oa-related-approval__empty {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
align-items: center;
|
||
padding: 10px 12px;
|
||
border: 1px dashed #a7f3d0;
|
||
border-radius: 8px;
|
||
background: #ecfdf5;
|
||
}
|
||
|
||
.oa-related-approval__empty-text {
|
||
color: var(--mint-text-muted, #94a3b8);
|
||
font-size: 13px;
|
||
}
|
||
|
||
.oa-related-approval__list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
|
||
.oa-related-approval__card {
|
||
padding: 10px 12px;
|
||
border: 1px solid #a7f3d0;
|
||
border-radius: 8px;
|
||
background: #ecfdf5;
|
||
}
|
||
|
||
.oa-related-approval__head {
|
||
display: flex;
|
||
gap: 8px;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.oa-related-approval__title {
|
||
padding: 0;
|
||
border: 0;
|
||
background: transparent;
|
||
color: var(--primary, #10b981);
|
||
font-weight: 600;
|
||
font-size: 13px;
|
||
text-align: left;
|
||
text-decoration: underline;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.oa-related-approval__title:disabled {
|
||
color: var(--mint-text-muted, #94a3b8);
|
||
text-decoration: none;
|
||
cursor: default;
|
||
}
|
||
|
||
.oa-related-approval__actions {
|
||
display: flex;
|
||
flex-shrink: 0;
|
||
gap: 4px;
|
||
align-items: center;
|
||
}
|
||
|
||
.oa-related-approval__toggle {
|
||
padding: 0;
|
||
border: 0;
|
||
background: transparent;
|
||
color: var(--primary, #10b981);
|
||
font-size: 13px;
|
||
line-height: 1.5;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.oa-related-approval__toggle:hover {
|
||
color: var(--primary-dark, #059669);
|
||
}
|
||
|
||
.oa-related-approval__fields {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 6px 16px;
|
||
margin-top: 8px;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.oa-related-approval__field {
|
||
display: inline-flex;
|
||
gap: 6px;
|
||
max-width: 100%;
|
||
font-size: 12.5px;
|
||
}
|
||
|
||
.oa-related-approval__field--full {
|
||
flex: 1 1 100%;
|
||
}
|
||
|
||
.oa-related-approval__field-label {
|
||
color: var(--mint-text-muted, #94a3b8);
|
||
}
|
||
|
||
.oa-related-approval__field-value {
|
||
display: inline-flex;
|
||
flex-wrap: wrap;
|
||
gap: 4px 10px;
|
||
color: var(--mint-text, #334155);
|
||
word-break: break-all;
|
||
}
|
||
|
||
.oa-related-approval__link {
|
||
color: var(--primary, #10b981);
|
||
text-decoration: underline;
|
||
}
|
||
|
||
.oa-related-approval__link:hover {
|
||
color: var(--primary-dark, #059669);
|
||
}
|
||
|
||
.oa-related-approval__link-btn {
|
||
padding: 0;
|
||
border: 0;
|
||
background: transparent;
|
||
color: var(--primary, #10b981);
|
||
font: inherit;
|
||
text-decoration: underline;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.oa-related-approval__link-btn:hover {
|
||
color: var(--primary-dark, #059669);
|
||
}
|
||
|
||
.oa-related-approval--stack .oa-related-approval__fields {
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
}
|
||
|
||
.oa-related-approval--stack .oa-related-approval__field {
|
||
display: flex;
|
||
}
|
||
|
||
.oa-related-approval--stack .oa-related-approval__field-label::after {
|
||
content: ':';
|
||
}
|
||
|
||
.oa-related-approval__add-more {
|
||
align-self: flex-start;
|
||
}
|
||
|
||
.oa-related-picker {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0;
|
||
}
|
||
|
||
.oa-related-picker__tabs {
|
||
display: flex;
|
||
gap: 0;
|
||
margin: 0 -4px 0;
|
||
border-bottom: 1px solid var(--mint-border, #e2e8f0);
|
||
}
|
||
|
||
.oa-related-picker__tab {
|
||
position: relative;
|
||
height: 40px;
|
||
padding: 0 14px;
|
||
border: 0;
|
||
background: transparent;
|
||
color: var(--mint-text-muted, #64748b);
|
||
font-size: 14px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.oa-related-picker__tab:hover {
|
||
color: var(--mint-text, #1e293b);
|
||
}
|
||
|
||
.oa-related-picker__tab.is-active {
|
||
color: var(--primary-dark, #059669);
|
||
font-weight: 600;
|
||
}
|
||
|
||
.oa-related-picker__tab.is-active::after {
|
||
content: '';
|
||
position: absolute;
|
||
right: 10px;
|
||
bottom: 0;
|
||
left: 10px;
|
||
height: 2px;
|
||
border-radius: 1px;
|
||
background: var(--primary, #10b981);
|
||
}
|
||
|
||
.oa-related-picker__filters {
|
||
padding: 12px 0;
|
||
}
|
||
|
||
.oa-related-picker__list {
|
||
max-height: 360px;
|
||
overflow: auto;
|
||
border: 1px solid var(--mint-border, #e2e8f0);
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.oa-related-picker__card {
|
||
display: flex;
|
||
gap: 12px;
|
||
align-items: flex-start;
|
||
padding: 12px 14px;
|
||
border-bottom: 1px solid #f1f5f9;
|
||
cursor: pointer;
|
||
transition: background 0.12s;
|
||
}
|
||
|
||
.oa-related-picker__card:last-child {
|
||
border-bottom: 0;
|
||
}
|
||
|
||
.oa-related-picker__card:hover {
|
||
background: #f8fafc;
|
||
}
|
||
|
||
.oa-related-picker__card.is-selected {
|
||
background: var(--primary-bg, #ecfdf5);
|
||
}
|
||
|
||
.oa-related-picker__card.is-disabled {
|
||
opacity: 0.55;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.oa-related-picker__avatar {
|
||
display: flex;
|
||
flex-shrink: 0;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 40px;
|
||
height: 40px;
|
||
margin-top: 2px;
|
||
border-radius: 8px;
|
||
color: #fff;
|
||
font-weight: 600;
|
||
font-size: 13px;
|
||
letter-spacing: 0.5px;
|
||
}
|
||
|
||
.oa-related-picker__main {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.oa-related-picker__title {
|
||
margin: 0 0 4px;
|
||
color: var(--mint-text, #1e293b);
|
||
font-weight: 600;
|
||
font-size: 14px;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.oa-related-picker__meta {
|
||
overflow: hidden;
|
||
color: var(--mint-text-muted, #64748b);
|
||
font-size: 12.5px;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.oa-related-picker__time {
|
||
margin-top: 6px;
|
||
color: var(--mint-text-muted, #94a3b8);
|
||
font-size: 12px;
|
||
}
|
||
|
||
.oa-related-picker__right {
|
||
flex-shrink: 0;
|
||
min-width: 96px;
|
||
padding-top: 2px;
|
||
font-size: 12.5px;
|
||
font-weight: 500;
|
||
text-align: right;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.oa-related-picker__right.is-warning {
|
||
color: #d97706;
|
||
}
|
||
|
||
.oa-related-picker__right.is-success {
|
||
color: #059669;
|
||
}
|
||
|
||
.oa-related-picker__right.is-danger {
|
||
color: #ef4444;
|
||
}
|
||
|
||
.oa-related-picker__right.is-muted {
|
||
color: #64748b;
|
||
}
|
||
|
||
.oa-related-picker__pager {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-top: 10px;
|
||
}
|
||
|
||
.oa-related-picker__selected {
|
||
color: var(--mint-text-muted, #64748b);
|
||
font-size: 13px;
|
||
}
|
||
|
||
.oa-related-picker__selected em {
|
||
font-style: normal;
|
||
font-weight: 600;
|
||
color: var(--primary-dark, #059669);
|
||
}
|
||
</style>
|