Files
qhmes/jeecgboot-vue3/src/components/DingTplLaunch/DingBindLaunchModal.vue

808 lines
30 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<a-modal
v-model:open="visible"
:title="`发起钉钉审批 · ${tplData?.templateName || ''}`"
:width="multiRow ? 1120 : 940"
:confirm-loading="submitting"
ok-text="发起审批"
cancel-text="取消"
destroy-on-close
:body-style="{ padding: 0 }"
@ok="handleSubmit"
@cancel="visible = false"
>
<div class="dal-body">
<!-- 左侧审批流时间轴 -->
<div class="dal-timeline-panel">
<div class="dal-panel-title">审批流程</div>
<div v-if="!selectedFlowId" class="dal-timeline-empty">
<div class="dal-timeline-empty-icon">🔗</div>
<div>请在右侧审批流配置<br>页签中选择审批流</div>
</div>
<a-spin v-else-if="previewLoading" style="display:flex;justify-content:center;padding:32px 0" />
<div v-else class="dal-timeline">
<div class="dal-ts-step">
<div class="dal-ts-left">
<div class="dal-ts-dot dal-ts-dot--start"></div>
<div class="dal-ts-line" v-if="approverPreview.length > 0"></div>
</div>
<div class="dal-ts-content">
<div class="dal-ts-name">发起人</div>
<div class="dal-ts-sub">所有人可发起</div>
</div>
</div>
<div v-for="(node, ni) in approverPreview" :key="node.nodeId || ni" class="dal-ts-step">
<div class="dal-ts-left">
<div class="dal-ts-dot"
:class="[node.nodeType==='cc'?'dal-ts-dot--cc':'dal-ts-dot--approver', !node.allResolved?'dal-ts-dot--warn':'']">
</div>
<div class="dal-ts-line" v-if="ni < approverPreview.length - 1"></div>
</div>
<div class="dal-ts-content">
<div class="dal-ts-tags">
<span class="dal-ts-badge" :class="node.nodeType==='cc'?'dal-ts-badge--cc':'dal-ts-badge--approver'">
{{ node.nodeType==='cc'?'抄送':'审批' }}
</span>
<span v-if="node.nodeType!=='cc'" class="dal-ts-mode">{{ modeLabel(node.multiMode) }}</span>
</div>
<div class="dal-ts-name">{{ node.nodeName }}</div>
<div class="dal-ts-users">
<template v-for="(u, ui) in node.users" :key="u.username">
<span :class="u.resolved?'dal-ts-user--ok':'dal-ts-user--err'">{{ u.realname }}</span>
<span v-if="ui < node.users.length-1" style="color:#ccc;margin:0 2px">·</span>
</template>
</div>
<div v-if="!node.allResolved" class="dal-ts-unresolved"> 有未解析成员请补充手机号</div>
</div>
</div>
<div class="dal-ts-step" v-if="approverPreview.length > 0">
<div class="dal-ts-left"><div class="dal-ts-dot dal-ts-dot--end"></div></div>
<div class="dal-ts-content"><div class="dal-ts-name" style="color:#888">结束</div></div>
</div>
</div>
</div>
<div class="dal-panel-divider"></div>
<!-- 中间主内容表单字段 + 审批流配置 -->
<div class="dal-content-panel">
<a-tabs v-model:activeKey="activeTab" size="small" class="dal-tabs">
<!-- 表单展示只读 -->
<a-tab-pane key="form" tab="表单字段">
<div class="dal-form-scroll">
<a-spin :spinning="loading" tip="加载表单字段中...">
<a-alert v-if="loadError" type="error" :message="loadError" show-icon style="margin-bottom:12px" />
<template v-else-if="!loading">
<a-alert
type="info"
show-icon
style="margin-bottom:12px"
message="字段已根据绑定配置从业务单据自动填充,不可手动修改。如需调整请先更新审批模板绑定中的字段映射。"
/>
<div v-if="dingFields.length === 0" class="dal-form-empty">该模板暂无表单字段</div>
<template v-for="field in dingFields" :key="field.label">
<div v-if="field.componentName === 'TextNote'" class="dal-form-note">{{ field.label }}</div>
<!-- 明细表只读展示 -->
<template v-else-if="field.componentName === 'TableField'">
<div class="dal-form-item">
<div class="dal-field-label" :class="{'dal-field-label--required': field.required}">{{ field.label }}</div>
<div class="dal-table-wrap">
<table class="dal-table">
<thead>
<tr>
<th style="width:40px;text-align:center">#</th>
<th v-for="child in field.children||[]" :key="child.label">{{ child.label }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, rowIdx) in getTableRows(field.label)" :key="rowIdx">
<td style="text-align:center;color:#aaa">{{ rowIdx+1 }}</td>
<td v-for="child in field.children||[]" :key="child.label">
<span class="dal-readonly-cell">{{ row[child.label] ?? '—' }}</span>
</td>
</tr>
<tr v-if="getTableRows(field.label).length === 0">
<td :colspan="(field.children?.length||0)+1" style="text-align:center;color:#bbb;padding:10px 0">暂无数据</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<!-- 普通字段只读 -->
<div v-else class="dal-form-item">
<div class="dal-field-label" :class="{'dal-field-label--required': field.required}">{{ field.label }}</div>
<a-input
:value="displayValue(field)"
disabled
class="dal-readonly-input"
/>
</div>
</template>
</template>
</a-spin>
</div>
</a-tab-pane>
<!-- 审批流配置只读查看 -->
<a-tab-pane key="flow">
<template #tab>
审批流配置
<a-badge v-if="hasUnresolved" color="red" style="margin-left:4px" />
</template>
<div class="dal-form-scroll">
<a-alert
type="info"
show-icon
style="margin-bottom:14px"
>
<template #message>
审批流程仅供查看不可在此修改如需调整请前往
<a class="flow-readonly-link" @click="goToTplConfig">钉钉审批模板配置</a>
中更改绑定的审批流
</template>
</a-alert>
<div class="flow-select-row">
<a-select
v-model:value="selectedFlowId"
style="flex:1;min-width:0"
placeholder="(未绑定审批流)"
:loading="flowLoading"
:options="flowSelectOptions"
disabled
>
<template #option="{ label, status, remark }">
<div class="flow-opt-item">
<span class="flow-opt-name">{{ label }}</span>
<span style="display:flex;align-items:center;gap:6px;flex-shrink:0">
<span v-if="remark" class="flow-opt-remark">{{ remark }}</span>
<a-tag :color="status==='1'?'green':status==='2'?'default':'orange'"
style="margin:0;font-size:11px;line-height:16px;padding:0 5px">
{{ status==='1'?'已发布':status==='2'?'已停用':'草稿' }}
</a-tag>
</span>
</div>
</template>
</a-select>
</div>
<template v-if="selectedFlowId">
<a-divider style="margin:14px 0 10px" />
<div class="preview-title">
审批节点 · 人员解析
<a-spin :spinning="previewLoading" size="small" style="margin-left:8px" />
</div>
<div v-if="!previewLoading && approverPreview.length === 0" style="color:#bbb;font-size:12px;margin-top:6px">
该审批流暂无审批人节点
</div>
<div v-for="(node, ni) in approverPreview" :key="node.nodeId||ni" class="preview-node"
:class="{'preview-node--cc': node.nodeType==='cc'}">
<div class="preview-node-hd">
<a-tag :color="node.nodeType==='cc'?'blue':'orange'" style="margin:0 6px 0 0;font-size:11px">
{{ node.nodeType==='cc'?'抄送':'审批' }}
</a-tag>
<span class="preview-node-name">{{ node.nodeName }}</span>
<span class="preview-node-mode">{{ node.nodeType==='cc'?'位置自动判断':modeLabel(node.multiMode) }}</span>
</div>
<div v-for="u in node.users" :key="u.username" class="preview-user">
<a-tag v-if="u.resolved" color="success" style="margin:0">{{ u.realname }}{{ u.username }}</a-tag>
<a-tag v-else-if="u.unsupported" color="default" style="margin:0">{{ u.realname }}不支持自动解析</a-tag>
<a-tag v-else color="error" style="margin:0">{{ u.realname }}{{ u.username }}未找到钉钉账号</a-tag>
</div>
<div v-if="!node.allResolved" class="preview-supplement">
<a-input
v-model:value="supplementPhones[node.nodeId||String(ni)]"
:placeholder="node.nodeType==='cc'?'补充抄送人手机号,多个用逗号分隔':'补充审批人手机号,多个用逗号分隔'"
allow-clear size="small"
/>
<div class="dal-field-hint" style="margin-top:3px">手机号需在企业钉钉注册与自动解析的成员合并</div>
</div>
</div>
<a-alert v-if="hasUnresolved" type="warning" show-icon style="margin-top:10px;font-size:12px"
message="部分节点有未解析成员,请补充手机号后再发起审批" />
</template>
</div>
</a-tab-pane>
</a-tabs>
</div>
<!-- 右侧已选数据列表多条时显示 -->
<template v-if="multiRow">
<div class="dal-panel-divider"></div>
<div class="dal-rows-panel">
<div class="dal-panel-title">
已选数据
<span class="dal-rows-badge">{{ allRows.length }}</span>
</div>
<div class="dal-rows-list">
<div
v-for="(row, idx) in allRows"
:key="idx"
class="dal-row-item"
:class="{ 'dal-row-item--active': idx === currentRowIndex }"
@click="switchRow(idx)"
>
<div class="dal-row-index">{{ idx + 1 }}</div>
<div class="dal-row-label">{{ getRowLabel(row, idx) }}</div>
</div>
</div>
</div>
</template>
</div>
</a-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, computed } from 'vue';
import { useRouter } from 'vue-router';
import { useMessage } from '/@/hooks/web/useMessage';
import {
getTemplateDetail,
queryById as queryTplById,
launchApproval,
getApprovalFlowList,
previewFlowApprovers,
} from '/@/views/xslmes/dingtalk/mesXslDingProcessTpl/MesXslDingProcessTpl.api';
import { checkCanLaunch } from '/@/views/approval/gate/approvalGate.api';
const emit = defineEmits(['success']);
const { createMessage } = useMessage();
const router = useRouter();
const visible = ref(false);
const loading = ref(false);
const submitting = ref(false);
const loadError = ref('');
const activeTab = ref('form');
interface BindInfo { id: string; templateId: string; templateName: string; fieldMappingJson?: string; bizCode?: string }
const tplData = ref<BindInfo | null>(null);
// 多条数据支持
const allRows = ref<any[]>([]);
const currentRowIndex = ref(0);
const multiRow = computed(() => allRows.value.length > 0);
const dingFields = ref<any[]>([]);
const formValues = reactive<Record<string, any>>({});
const tableValues = reactive<Record<string, Record<string, string>[]>>({});
const flowLoading = ref(false);
const flowList = ref<any[]>([]);
const selectedFlowId = ref('');
const previewLoading = ref(false);
const approverPreview = ref<any[]>([]);
const supplementPhones = reactive<Record<string, string>>({});
const hasUnresolved = computed(() => approverPreview.value.some(n => !n.allResolved));
const flowSelectOptions = computed(() =>
flowList.value.map(f => ({
value: f.id, label: f.flowName || f.name,
status: f.status, remark: f.remark || '',
}))
);
function modeLabel(mode: string) {
if (mode === 'none') return '单人';
if (mode === 'or') return '或签';
if (mode === 'sequence') return '依次';
return '会签';
}
// ══ 打开弹窗,支持单条或多条 ══
async function open(bindInfo: BindInfo, rowsOrRow: any[] | any) {
tplData.value = bindInfo;
allRows.value = Array.isArray(rowsOrRow) ? rowsOrRow : [rowsOrRow];
currentRowIndex.value = 0;
resetFieldValues();
loadError.value = '';
activeTab.value = 'form';
selectedFlowId.value = '';
approverPreview.value = [];
visible.value = true;
loading.value = true;
try {
const [detail, flows, tplRecord] = await Promise.all([
getTemplateDetail(bindInfo.templateId),
getApprovalFlowList({ pageSize: 200 }),
queryTplById(bindInfo.templateId),
]);
dingFields.value = detail?.dingFields || [];
if (detail?.schemaError) loadError.value = detail.schemaError;
flowList.value = flows?.records || flows || [];
for (const f of dingFields.value) {
if (f.componentName === 'TableField') tableValues[f.label] = [];
}
applyPrefillForRow(currentRowIndex.value);
const presetFlowId = tplRecord?.flowId;
if (presetFlowId) {
selectedFlowId.value = presetFlowId;
loadPreview(presetFlowId);
}
} catch (e: any) {
loadError.value = e?.message || '加载模板字段失败';
} finally {
loading.value = false;
}
}
function goToTplConfig() {
visible.value = false;
router.push('/xslmes/mesXslDingProcessTplList');
}
defineExpose({ open });
// ══ 切换数据行 ══
function switchRow(idx: number) {
if (idx === currentRowIndex.value) return;
currentRowIndex.value = idx;
resetFieldValues();
applyPrefillForRow(idx);
activeTab.value = 'form';
}
function resetFieldValues() {
Object.keys(formValues).forEach(k => delete formValues[k]);
Object.keys(tableValues).forEach(k => delete tableValues[k]);
Object.keys(supplementPhones).forEach(k => delete supplementPhones[k]);
for (const f of dingFields.value) {
if (f.componentName === 'TableField') tableValues[f.label] = [];
}
}
function applyPrefillForRow(idx: number) {
const rowData = allRows.value[idx];
if (rowData && tplData.value?.fieldMappingJson) {
applyPrefill(dingFields.value, tplData.value.fieldMappingJson, rowData);
}
}
// ══ 行标签(用于右侧列表显示) ══
const LABEL_SKIP = new Set([
'id', 'createBy', 'createTime', 'updateBy', 'updateTime',
'delFlag', 'sysOrgCode', 'tenantId', 'version',
]);
function getRowLabel(row: any, index: number): string {
for (const [key, val] of Object.entries(row ?? {})) {
if (LABEL_SKIP.has(key)) continue;
if (val && typeof val === 'string' && val.length <= 40) return val;
if (val !== null && val !== undefined && typeof val === 'number') return String(val);
}
return `条目 ${index + 1}`;
}
// ══ 预填充 ══
interface MappingItem {
componentId: string;
componentLabel: string;
componentName: string;
parentId?: string;
bizField?: string;
}
function applyPrefill(fields: any[], mappingJson: string, rowData: any) {
let mapping: MappingItem[] = [];
try { mapping = JSON.parse(mappingJson); } catch { return; }
const byId = new Map(mapping.map(m => [m.componentId, m]));
for (const field of fields) {
if (field.componentName === 'TextNote') continue;
const cid = field.id || field.label;
const m = byId.get(cid);
if (field.componentName === 'TableField') {
const slotName = m?.bizField;
if (!slotName) continue;
const arr: any[] = getNestedValue(rowData, slotName);
if (!Array.isArray(arr) || !arr.length) continue;
const childMappings = mapping.filter(x => x.parentId === cid && x.bizField);
tableValues[field.label] = arr.map(element => {
const row: Record<string, string> = {};
for (const child of childMappings) {
const parts = (child.bizField || '').split('.');
const colKey = parts.slice(1).join('.');
const val = colKey ? getNestedValue(element, colKey) : undefined;
row[child.componentLabel] = val !== undefined && val !== null ? String(val) : '';
}
for (const child of (field.children || [])) {
if (!(child.label in row)) row[child.label] = '';
}
return row;
});
} else {
if (!m?.bizField) continue;
const val = getNestedValue(rowData, m.bizField);
if (val === undefined || val === null) continue;
formValues[field.label] = formatForDisplay(val, field.componentName);
}
}
}
function getNestedValue(obj: any, path: string): any {
if (!obj || !path) return undefined;
return path.split('.').reduce((acc: any, k: string) => acc?.[k], obj);
}
function formatForDisplay(v: any, componentName: string): any {
if (v === null || v === undefined) return '';
if (['NumberField', 'MoneyField'].includes(componentName)) {
return typeof v === 'number' ? v : Number(v) || 0;
}
if (componentName === 'DDDateField') {
const s = String(v);
return s.includes('T') ? s.split('T')[0] : s.split(' ')[0];
}
if (Array.isArray(v)) return v.join(',');
return String(v);
}
function getTableRows(label: string): Record<string, string>[] {
return tableValues[label] || [];
}
function displayValue(field: any): string {
const v = formValues[field.label];
if (v === undefined || v === null || v === '') return '';
if (field.componentName === 'DDDateRangeField' && Array.isArray(v)) return v.join(' ~ ');
if (field.componentName === 'DDMultiSelectField' && Array.isArray(v)) return v.join(', ');
return String(v);
}
// ══ 审批流预览 ══
async function loadPreview(flowId: string) {
previewLoading.value = true;
try {
const res = await previewFlowApprovers(flowId);
approverPreview.value = Array.isArray(res) ? res : [];
} catch { approverPreview.value = []; }
finally { previewLoading.value = false; }
}
// ══ 提交 ══
async function handleSubmit() {
if (!selectedFlowId.value) {
activeTab.value = 'flow';
createMessage.warning('请在「审批流配置」页签中选择一个审批流');
return Promise.reject();
}
const unresolvedNodes = approverPreview.value.filter(n => !n.allResolved);
for (const node of unresolvedNodes) {
const key = node.nodeId || String(approverPreview.value.indexOf(node));
if (!supplementPhones[key]?.trim()) {
activeTab.value = 'flow';
createMessage.warning(`${node.nodeType==='cc'?'抄送节点':'审批节点'}${node.nodeName}」有未解析成员,请补充手机号`);
return Promise.reject();
}
}
const fvList: { name: string; value: string }[] = [];
for (const field of dingFields.value) {
if (field.componentName === 'TextNote') continue;
const label = field.label;
if (field.componentName === 'TableField') {
const validRows = getTableRows(label).filter(r => Object.values(r).some(v => v !== ''));
if (validRows.length === 0) continue;
fvList.push({
name: label,
value: JSON.stringify(validRows.map(row =>
Object.entries(row).map(([k, v]) => ({ name: k, value: String(v ?? '') }))
)),
});
continue;
}
let val = formValues[label];
if (field.componentName === 'DDDateRangeField' && Array.isArray(val)) {
val = val.join('~');
} else if (field.componentName === 'DDMultiSelectField' && Array.isArray(val)) {
val = val.length > 0 ? JSON.stringify(val) : null;
} else if (['InnerContactField', 'RelateField', 'DDPhotoField'].includes(field.componentName)) {
const raw = val ? String(val).trim() : '';
val = raw ? JSON.stringify(raw.split(',').map((s: string) => s.trim()).filter(Boolean)) : null;
} else {
val = val !== undefined && val !== null ? String(val) : null;
}
if (val === null || val === '') { if (!field.required) continue; val = ''; }
fvList.push({ name: label, value: val as string });
}
const approverOverrides = Object.entries(supplementPhones)
.filter(([, phones]) => phones?.trim())
.map(([nodeId, phones]) => ({ nodeId, phones: phones.trim() }));
const row = allRows.value[currentRowIndex.value];
const flow = flowList.value.find((f) => f.id === selectedFlowId.value);
const bizTable = flow?.bizTable;
const bizDataId = row?.id != null ? String(row.id) : '';
const bizTitle = getRowLabel(row, currentRowIndex.value);
//update-begin---author:GHT ---date:2026-06-04 for【QH-MES审批台账】钉钉发起前统一门禁校验-----
if (bizTable && bizDataId) {
const gate = await checkCanLaunch({ bizTable, bizDataId });
if (!gate?.allowed) {
createMessage.warning(gate?.reason || '当前不允许发起审批');
return Promise.reject();
}
}
//update-end---author:GHT ---date:2026-06-04 for【QH-MES审批台账】钉钉发起前统一门禁校验-----
submitting.value = true;
try {
const result = await launchApproval({
id: tplData.value!.templateId,
formValues: fvList,
flowId: selectedFlowId.value,
approverOverrides,
bizTable,
bizDataId,
bizTitle,
bizCode: tplData.value?.bizCode,
});
createMessage.success(typeof result === 'string' ? result : '审批发起成功!审批人将在钉钉「待我审批」中收到任务');
visible.value = false;
emit('success', result);
} catch (e: any) {
createMessage.error(e?.message || '发起失败');
return Promise.reject(e);
} finally {
submitting.value = false;
}
}
</script>
<style lang="less" scoped>
.dal-body {
display: flex;
min-height: 480px;
max-height: 70vh;
}
.dal-timeline-panel {
width: 210px;
flex-shrink: 0;
background: #fafafa;
border-right: 1px solid #f0f0f0;
padding: 16px 14px;
overflow-y: auto;
}
.dal-panel-title {
font-size: 12px;
font-weight: 600;
color: #888;
letter-spacing: .5px;
text-transform: uppercase;
margin-bottom: 14px;
display: flex;
align-items: center;
gap: 6px;
}
.dal-timeline-empty {
text-align: center; color: #bbb; font-size: 12px; padding-top: 32px; line-height: 1.8;
.dal-timeline-empty-icon { font-size: 24px; margin-bottom: 8px; }
}
.dal-ts-step { display: flex; align-items: flex-start; gap: 8px; }
.dal-ts-left { display: flex; flex-direction: column; align-items: center; flex-shrink: 0; width: 12px; padding-top: 2px; }
.dal-ts-dot {
width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0;
position: relative; z-index: 1; border: 2px solid currentColor; background: #fff;
&--start { color: #1677ff; background: #1677ff; border-color: #1677ff; }
&--end { color: #d9d9d9; background: #d9d9d9; border-color: #d9d9d9; width: 8px; height: 8px; }
&--approver { color: #fa8c16; }
&--cc { color: #1677ff; }
&--warn { color: #ff4d4f !important; }
}
.dal-ts-line { width: 2px; flex: 1; min-height: 22px; background: linear-gradient(to bottom, #e0e0e0 50%, transparent 100%); margin: 3px 0 0; }
.dal-ts-content { flex: 1; padding-bottom: 18px; min-width: 0; }
.dal-ts-sub { font-size: 11px; color: #aaa; }
.dal-ts-tags { display: flex; align-items: center; gap: 4px; margin-bottom: 2px; }
.dal-ts-badge {
font-size: 10px; padding: 0 5px; border-radius: 3px; line-height: 16px; font-weight: 500;
&--approver { background: #fff7e6; color: #d46b08; border: 1px solid #ffd591; }
&--cc { background: #e6f4ff; color: #0958d9; border: 1px solid #91caff; }
}
.dal-ts-mode { font-size: 10px; color: #aaa; background: #f5f5f5; padding: 0 4px; border-radius: 2px; line-height: 14px; }
.dal-ts-name { font-size: 12px; font-weight: 500; color: #333; line-height: 1.4; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.dal-ts-users { font-size: 11px; color: #888; margin-top: 2px; line-height: 1.5; }
.dal-ts-user--ok { color: #52c41a; }
.dal-ts-user--err { color: #ff4d4f; }
.dal-ts-unresolved { font-size: 10px; color: #ff7a00; margin-top: 2px; }
.dal-panel-divider { width: 1px; background: #f0f0f0; flex-shrink: 0; }
.dal-content-panel {
flex: 1; min-width: 0; display: flex; flex-direction: column; overflow: hidden;
}
.dal-tabs {
height: 100%; display: flex; flex-direction: column;
:deep(.ant-tabs-nav) { padding: 0 16px; margin-bottom: 0; }
:deep(.ant-tabs-content-holder) { flex: 1; overflow: hidden; }
:deep(.ant-tabs-content) { height: 100%; }
:deep(.ant-tabs-tabpane) { height: 100%; }
}
.dal-form-scroll { height: 100%; overflow-y: auto; padding: 14px 18px; }
.dal-form-item { margin-bottom: 14px; }
.dal-form-empty { color: #bbb; text-align: center; padding: 32px 0; font-size: 13px; }
.dal-field-label {
font-size: 13px; color: #555; margin-bottom: 5px; font-weight: 500;
&--required::before { content: '* '; color: #ff4d4f; }
}
.dal-field-hint { font-size: 11px; color: #aaa; margin-top: 3px; }
.dal-form-note {
background: #f8f8f8; border-left: 3px solid #ddd; padding: 6px 10px;
font-size: 12px; color: #777; margin-bottom: 12px; border-radius: 0 4px 4px 0;
}
.dal-readonly-input {
:deep(.ant-input[disabled]) {
color: rgba(0,0,0,.75) !important;
background: #f8f8f8 !important;
cursor: default;
}
}
.dal-readonly-cell {
display: block;
padding: 2px 6px;
color: rgba(0,0,0,.75);
font-size: 13px;
line-height: 1.5;
}
.dal-table-wrap { border: 1px solid #ebebeb; border-radius: 6px; overflow: hidden; }
.dal-table {
width: 100%; border-collapse: collapse; font-size: 13px;
th { background: #fafafa; padding: 7px 10px; border-bottom: 1px solid #ebebeb; font-weight: 500; color: #666; font-size: 12px; }
td { padding: 4px 6px; border-bottom: 1px solid #f5f5f5; vertical-align: middle; }
tr:last-child td { border-bottom: none; }
}
// 审批流配置
.flow-select-row { display: flex; gap: 8px; align-items: center; margin-bottom: 8px; }
.flow-readonly-link {
color: inherit;
text-decoration: none;
border-bottom: 1px dashed currentColor;
cursor: pointer;
transition: color .15s;
&:hover { color: #1677ff; }
}
.flow-opt-item { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.flow-opt-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.flow-opt-remark { font-size: 11px; color: #aaa; }
.preview-title { font-size: 12px; font-weight: 600; color: #555; margin-bottom: 8px; display: flex; align-items: center; }
.preview-node {
background: #fafafa; border: 1px solid #f0f0f0; border-radius: 6px; padding: 8px 10px; margin-bottom: 8px;
&--cc { background: #f0f7ff; border-color: #bae0ff; }
}
.preview-node-hd { display: flex; align-items: center; gap: 4px; margin-bottom: 6px; }
.preview-node-name { font-size: 12px; font-weight: 500; flex: 1; }
.preview-node-mode { font-size: 11px; color: #aaa; }
.preview-user { margin-bottom: 4px; }
.preview-supplement { margin-top: 8px; }
// ══ 右侧:已选数据列表 ══
.dal-rows-panel {
width: 170px;
flex-shrink: 0;
background: #fafafa;
padding: 16px 10px;
overflow-y: auto;
}
.dal-rows-badge {
display: inline-flex;
align-items: center;
justify-content: center;
background: #ff6900;
color: #fff;
border-radius: 10px;
font-size: 10px;
font-weight: 700;
min-width: 16px;
height: 16px;
padding: 0 4px;
line-height: 1;
}
.dal-rows-list {
display: flex;
flex-direction: column;
gap: 4px;
}
.dal-row-item {
display: flex;
align-items: flex-start;
gap: 6px;
padding: 7px 8px;
border-radius: 6px;
cursor: pointer;
border: 1px solid transparent;
transition: all .15s;
background: #fff;
border-color: #f0f0f0;
&:hover {
border-color: #ff6900;
background: #fff8f3;
}
&--active {
border-color: #ff6900 !important;
background: #fff3eb !important;
.dal-row-index {
background: #ff6900;
color: #fff;
}
}
}
.dal-row-index {
flex-shrink: 0;
width: 18px;
height: 18px;
border-radius: 50%;
background: #e8e8e8;
color: #666;
font-size: 10px;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
transition: all .15s;
margin-top: 1px;
}
.dal-row-label {
flex: 1;
min-width: 0;
font-size: 12px;
color: #333;
line-height: 1.5;
word-break: break-all;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>