| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134 |
- <!-- OA 竖向流程树设计器(钉钉式,无 X6) -->
- <template>
- <div class="oa-flow-designer">
- <p class="oa-flow-designer__hint">
- 点击节点配置选人;「+」插入审批 / 抄送 / 条件分支。预置角色(总经理 / 董事长)置顶。
- </p>
- <div class="oa-flow-designer__canvas">
- <div class="oa-flow-designer__canvas-inner">
- <FlowBranch
- :node="root"
- :candidates="candidates"
- @edit="openEdit"
- @remove="onRemove"
- @add="onAdd"
- />
- </div>
- </div>
- <el-drawer
- v-model="drawerVisible"
- size="640px"
- append-to-body
- class="oa-flow-drawer"
- :show-close="false"
- :close-on-click-modal="false"
- >
- <template #header>
- <div class="oa-flow-drawer__hd">
- <div class="oa-flow-drawer__hd-top">
- <div class="oa-flow-drawer__icon" aria-hidden="true">
- <el-icon :size="22"><UserFilled /></el-icon>
- </div>
- <div class="oa-flow-drawer__titles">
- <h3 class="oa-flow-drawer__title">{{ drawerTitle }}</h3>
- <p class="oa-flow-drawer__sub">{{ drawerSubtitle }}</p>
- </div>
- <button
- type="button"
- class="oa-flow-drawer__close"
- aria-label="关闭"
- @click="drawerVisible = false"
- >
- <el-icon :size="16"><Close /></el-icon>
- </button>
- </div>
- <div class="oa-flow-drawer__actions">
- <el-button class="btn-ghost" @click="drawerVisible = false">
- <el-icon><Close /></el-icon>
- 取消
- </el-button>
- <el-button class="btn-main" type="primary" @click="applyEdit">
- <el-icon><Check /></el-icon>
- 确定
- </el-button>
- </div>
- </div>
- </template>
- <el-form v-if="editing" label-position="top" class="oa-flow-drawer__form">
- <el-form-item label="节点名称">
- <el-input v-model="editing.name" maxlength="50" />
- </el-form-item>
- <template v-if="editing.type === 'approval' || editing.type === 'cc'">
- <el-form-item label="选人方式">
- <div class="assignee-seg">
- <button
- v-for="opt in assigneeTypeOptions"
- :key="opt.value"
- type="button"
- class="assignee-seg__item"
- :class="{
- 'is-on':
- editing.type === 'cc'
- ? (cfg.assigneeTypes || []).includes(opt.value)
- : cfg.assigneeType === opt.value
- }"
- @click="onAssigneeOptionClick(opt.value)"
- >
- {{ opt.label }}
- </button>
- </div>
- </el-form-item>
- <el-form-item v-if="showUsersPicker">
- <template #label>
- 指定成员
- <span class="form-label-tip">可多选,已选将作为本节点{{ editing.type === 'cc' ? '抄送' : '审批' }}人</span>
- </template>
- <div v-loading="userLoading" class="member-picker">
- <el-select
- v-model="memberPickBuffer"
- filterable
- clearable
- :loading="userLoading"
- placeholder="搜索姓名 / 工号,点击添加"
- style="width: 100%"
- popper-class="oa-flow-member-select-popper"
- @change="onMemberPick"
- >
- <el-option
- v-for="u in userOptions"
- :key="u.id"
- :label="memberOptionLabel(u)"
- :value="u.id"
- :disabled="(cfg.userIds || []).includes(u.id)"
- >
- <div class="member-option">
- <span class="member-avatar" :style="{ background: avatarColor(u.id) }">
- {{ avatarInitial(u.nickName) }}
- </span>
- <span class="member-option__meta">
- <span class="member-option__name">{{ u.nickName || '未命名' }}</span>
- <span v-if="memberOptionSub(u)" class="member-option__sub">
- {{ memberOptionSub(u) }}
- </span>
- </span>
- <el-tag
- v-if="(cfg.userIds || []).includes(u.id)"
- size="small"
- type="info"
- effect="plain"
- class="member-option__tag"
- >
- 已选
- </el-tag>
- </div>
- </el-option>
- </el-select>
- <div
- class="member-chips"
- :class="{ 'is-empty': !(cfg.userIds && cfg.userIds.length) }"
- >
- <template v-if="cfg.userIds && cfg.userIds.length">
- <span v-for="id in cfg.userIds" :key="id" class="member-chip">
- <span class="member-avatar member-avatar--sm" :style="{ background: avatarColor(id) }">
- {{ avatarInitial(resolveUser(id)?.nickName) }}
- </span>
- <span class="member-chip__name" :title="chipTitle(id)">
- {{ resolveUser(id)?.nickName || id }}
- </span>
- <button
- type="button"
- class="member-chip__rm"
- :aria-label="`移除 ${resolveUser(id)?.nickName || id}`"
- @click="removeMember(id)"
- >
- ×
- </button>
- </span>
- </template>
- <template v-else>暂未选择成员,请从上方搜索添加</template>
- </div>
- <p class="member-picked-count">已选 {{ cfg.userIds?.length || 0 }} 人</p>
- </div>
- </el-form-item>
- <el-form-item v-if="showRolesPicker" label="工作流角色">
- <el-select
- v-model="cfg.oaRoleIds"
- multiple
- filterable
- collapse-tags
- collapse-tags-tooltip
- placeholder="选择角色"
- style="width: 100%"
- :loading="roleLoading"
- >
- <el-option-group label="预置">
- <el-option
- v-for="r in presetRoles"
- :key="r.id"
- :label="`${r.roleName}(${r.roleCode})`"
- :value="r.id"
- />
- </el-option-group>
- <el-option-group label="自定义">
- <el-option
- v-for="r in customRoles"
- :key="r.id"
- :label="`${r.roleName}(${r.roleCode})`"
- :value="r.id"
- />
- </el-option-group>
- </el-select>
- </el-form-item>
- <el-form-item v-if="editing.type === 'approval'" label="多人审批方式">
- <div class="sign-mode">
- <label
- class="sign-mode__card"
- :class="{ 'is-on': cfg.signMode === 'or' }"
- >
- <el-radio v-model="cfg.signMode" value="or">
- <span class="sign-mode__text">
- <strong>或签</strong>
- <span>一人通过即可进入下一节点</span>
- </span>
- </el-radio>
- </label>
- <label
- class="sign-mode__card"
- :class="{ 'is-on': cfg.signMode === 'and' }"
- >
- <el-radio v-model="cfg.signMode" value="and">
- <span class="sign-mode__text">
- <strong>会签</strong>
- <span>所选成员全部通过后进入下一节点</span>
- </span>
- </el-radio>
- </label>
- </div>
- </el-form-item>
- </template>
- <template v-if="editing.type === 'condition'">
- <div
- v-for="(g, idx) in editing.conditionGroups || []"
- :key="idx"
- class="cond-group-editor"
- >
- <el-form-item :label="`分支 ${idx + 1} 名称`">
- <el-input v-model="g.name" />
- </el-form-item>
- <ConditionRuleEditor
- v-model="g.rules"
- :disabled="!!g.isDefault"
- :candidates="candidates"
- :legacy-when="g.when"
- />
- <el-form-item label="默认分支">
- <el-switch
- v-model="g.isDefault"
- @change="(v) => onDefaultChange(idx, !!v)"
- />
- </el-form-item>
- </div>
- <el-button class="btn-ghost" @click="addConditionGroup">
- <el-icon><Plus /></el-icon>
- 增加分支
- </el-button>
- </template>
- <template v-if="editing.type === 'start'">
- <p class="muted">发起范围在「基础设置」中配置;此处节点仅作流程起点。</p>
- </template>
- </el-form>
- </el-drawer>
- </div>
- </template>
- <script setup lang="ts">
- import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue'
- import { ElMessage, ElMessageBox } from 'element-plus'
- import { Plus, Close, Check, UserFilled } from '@element-plus/icons-vue'
- import { fetchUserList } from '@/api/admin'
- import { fetchOaRoleList } from '@/api/oaApproval'
- import { MAX_ADMIN_LIST_PAGE_SIZE } from '@/utils/fetch-paged-list'
- import type { ConditionCandidate } from '@/components/oa-approval/oa-condition-fields'
- import ConditionRuleEditor from './ConditionRuleEditor.vue'
- import FlowBranch from './FlowBranch.vue'
- import {
- createEndNode,
- createNode,
- ensureFlowRoot,
- findParent,
- normalizeCcAssigneeTypes,
- type ApproverConfig,
- type AssigneeType,
- type ConditionRule,
- type FlowNode
- } from './types'
- defineOptions({ name: 'OaFlowDesigner' })
- type UserOption = { id: string; nickName: string; userNo: string }
- const AVATAR_COLORS = [
- '#059669',
- '#0ea5e9',
- '#8b5cf6',
- '#d97706',
- '#ef4444',
- '#14b8a6',
- '#6366f1',
- '#ec4899'
- ]
- const props = withDefaults(
- defineProps<{
- modelValue: FlowNode | Record<string, unknown> | null
- candidates?: ConditionCandidate[]
- }>(),
- { candidates: () => [] }
- )
- const candidates = computed(() => props.candidates || [])
- const emit = defineEmits<{
- 'update:modelValue': [value: FlowNode]
- }>()
- const root = ref<FlowNode>(ensureFlowRoot(props.modelValue))
- watch(
- () => props.modelValue,
- (v) => {
- root.value = ensureFlowRoot(v)
- }
- )
- function commit() {
- emit('update:modelValue', root.value)
- }
- const drawerVisible = ref(false)
- const editing = ref<FlowNode | null>(null)
- const cfg = reactive<ApproverConfig>({
- assigneeType: 'dept_leader',
- assigneeTypes: [],
- userIds: [],
- oaRoleIds: [],
- signMode: 'or'
- })
- const assigneeTypeOptions: Array<{ label: string; value: AssigneeType }> = [
- { label: '指定成员', value: 'users' },
- { label: '工作流角色', value: 'oa_roles' },
- { label: '部门负责人', value: 'dept_leader' },
- { label: '发起人主管', value: 'direct_supervisor' },
- { label: '发起人自选', value: 'initiator_select' }
- ]
- const showUsersPicker = computed(() => {
- if (editing.value?.type === 'cc') return (cfg.assigneeTypes || []).includes('users')
- return cfg.assigneeType === 'users'
- })
- const showRolesPicker = computed(() => {
- if (editing.value?.type === 'cc') return (cfg.assigneeTypes || []).includes('oa_roles')
- return cfg.assigneeType === 'oa_roles'
- })
- const drawerTitle = computed(() => {
- const t = editing.value?.type
- if (t === 'start') return '发起人'
- if (t === 'approval') return '审批人设置'
- if (t === 'cc') return '抄送人设置'
- if (t === 'condition') return '条件分支'
- return '节点设置'
- })
- const drawerSubtitle = computed(() => {
- const t = editing.value?.type
- if (t === 'approval') return '配置本节点审批人与多人会签规则'
- if (t === 'cc') return '可同时选择多种方式,抄送时合并去重'
- if (t === 'condition') return '配置条件分支名称与可视化规则'
- if (t === 'start') return '流程起点节点'
- return '配置流程节点'
- })
- const roleLoading = ref(false)
- const userLoading = ref(false)
- const roleOptions = ref<any[]>([])
- const userOptions = ref<UserOption[]>([])
- const memberPickBuffer = ref('')
- const presetRoles = computed(() =>
- roleOptions.value.filter((r) => r.roleKind === 'preset' || ['GM', 'CHAIRMAN'].includes(r.roleCode))
- )
- const customRoles = computed(() =>
- roleOptions.value.filter((r) => r.roleKind !== 'preset' && !['GM', 'CHAIRMAN'].includes(r.roleCode))
- )
- function avatarColor(id: string) {
- let hash = 0
- for (let i = 0; i < id.length; i++) hash = (hash * 31 + id.charCodeAt(i)) >>> 0
- return AVATAR_COLORS[hash % AVATAR_COLORS.length]
- }
- function avatarInitial(name?: string) {
- const t = (name || '?').trim()
- return t ? t.slice(0, 1) : '?'
- }
- function resolveUser(id: string) {
- return userOptions.value.find((u) => u.id === id)
- }
- function chipTitle(id: string) {
- const u = resolveUser(id)
- return u ? memberOptionLabel(u) : id
- }
- function memberOptionLabel(u: UserOption) {
- const name = (u.nickName || '').trim() || '未命名'
- const no = (u.userNo || '').trim()
- if (no && no !== name) return `${name}(${no})`
- return name
- }
- /** 副文案:仅展示与姓名不同的账号,避免双行同名显得拥挤 */
- function memberOptionSub(u: UserOption) {
- const name = (u.nickName || '').trim()
- const no = (u.userNo || '').trim()
- if (!no || no === name) return ''
- return `账号 ${no}`
- }
- function onMemberPick(id: string) {
- if (!id) return
- if (!cfg.userIds) cfg.userIds = []
- if (!cfg.userIds.includes(id)) cfg.userIds.push(id)
- nextTick(() => {
- memberPickBuffer.value = ''
- })
- }
- function removeMember(id: string) {
- if (!cfg.userIds) return
- cfg.userIds = cfg.userIds.filter((x) => x !== id)
- }
- async function setAssigneeType(type: AssigneeType) {
- cfg.assigneeType = type
- await onAssigneeTypeChange()
- }
- async function onAssigneeOptionClick(type: AssigneeType) {
- if (editing.value?.type === 'cc') {
- const cur = [...(cfg.assigneeTypes || [])]
- const i = cur.indexOf(type)
- if (i >= 0) cur.splice(i, 1)
- else cur.push(type)
- cfg.assigneeTypes = cur
- if (cur[0]) cfg.assigneeType = cur[0]
- if (cur.includes('users')) await loadUsers()
- if (cur.includes('oa_roles')) await loadRoles()
- return
- }
- await setAssigneeType(type)
- }
- async function loadRoles() {
- roleLoading.value = true
- try {
- const res = await fetchOaRoleList({ status: '1' })
- roleOptions.value = res?.list || []
- } finally {
- roleLoading.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
- }
- }
- function findNode(node: FlowNode, id: string): FlowNode | null {
- if (node.id === id) return node
- if (node.child) {
- const f = findNode(node.child, id)
- if (f) return f
- }
- for (const g of node.conditionGroups || []) {
- if (g.child) {
- const f = findNode(g.child, id)
- if (f) return f
- }
- }
- return null
- }
- function onAdd(payload: {
- parentId: string
- type: 'approval' | 'cc' | 'condition'
- groupIndex?: number
- }) {
- const parent = findNode(root.value, payload.parentId)
- if (!parent) return
- const neu = createNode(payload.type)
- // 写入条件节点的某一分支
- if (
- parent.type === 'condition' &&
- typeof payload.groupIndex === 'number' &&
- parent.conditionGroups?.[payload.groupIndex]
- ) {
- const g = parent.conditionGroups[payload.groupIndex]
- const old = g.child || createEndNode()
- if (payload.type === 'condition') {
- const groups = neu.conditionGroups || []
- const def = groups.find((x) => x.isDefault) || groups[groups.length - 1]
- if (def) def.child = old
- } else {
- neu.child = old
- }
- g.child = neu
- commit()
- return
- }
- const old = parent.child || createEndNode()
- if (payload.type === 'condition') {
- const groups = neu.conditionGroups || []
- const def = groups.find((g) => g.isDefault) || groups[groups.length - 1]
- if (def) def.child = old
- parent.child = neu
- } else {
- neu.child = old.type === 'end' || old ? old : createEndNode()
- parent.child = neu
- }
- commit()
- }
- async function onRemove(nodeId: string) {
- const target = findNode(root.value, nodeId)
- if (!target || target.type === 'start' || target.type === 'end') {
- ElMessage.warning('该节点不可删除')
- return
- }
- await ElMessageBox.confirm(`确定删除节点「${target.name}」吗?`, '提示', { type: 'warning' })
- const parent = findParent(root.value, nodeId)
- if (!parent) return
- const splice = target.child || createEndNode()
- if (parent.type === 'condition') {
- for (const g of parent.conditionGroups || []) {
- if (g.child?.id === nodeId) {
- g.child = splice
- commit()
- return
- }
- }
- }
- if (parent.child?.id === nodeId) {
- parent.child = splice
- commit()
- return
- }
- ElMessage.warning('未能定位父节点,删除失败')
- }
- async function openEdit(node: FlowNode) {
- editing.value = JSON.parse(JSON.stringify(node)) as FlowNode
- if (editing.value.type === 'condition') {
- editing.value.conditionGroups = (editing.value.conditionGroups || []).map((g) => ({
- ...g,
- rules: Array.isArray(g.rules) ? g.rules : []
- }))
- }
- const ac = editing.value.approverConfig || { assigneeType: 'dept_leader' as const }
- if (editing.value.type === 'cc') {
- const types = normalizeCcAssigneeTypes(ac)
- cfg.assigneeTypes = types.length ? types : ['initiator_select']
- cfg.assigneeType = cfg.assigneeTypes[0]
- } else {
- cfg.assigneeTypes = []
- cfg.assigneeType = ac.assigneeType || 'dept_leader'
- }
- cfg.userIds = [...(ac.userIds || [])]
- cfg.oaRoleIds = [...(ac.oaRoleIds || [])]
- cfg.signMode = ac.signMode || 'or'
- memberPickBuffer.value = ''
- if (showUsersPicker.value) await loadUsers()
- if (showRolesPicker.value) await loadRoles()
- drawerVisible.value = true
- }
- async function onAssigneeTypeChange() {
- if (cfg.assigneeType === 'users' && !userOptions.value.length) await loadUsers()
- if (cfg.assigneeType === 'oa_roles' && !roleOptions.value.length) await loadRoles()
- }
- function onDefaultChange(idx: number, v: boolean) {
- if (!editing.value?.conditionGroups || !v) return
- editing.value.conditionGroups.forEach((g, i) => {
- g.isDefault = i === idx
- if (g.isDefault) {
- g.when = ''
- g.rules = []
- }
- })
- }
- function sanitizeConditionRules(rules?: ConditionRule[]): ConditionRule[] {
- return (rules || []).filter(
- (r) =>
- !!r &&
- String(r.field || '').trim() &&
- String(r.op || '').trim() &&
- r.value !== '' &&
- r.value != null
- )
- }
- function addConditionGroup() {
- if (!editing.value) return
- if (!editing.value.conditionGroups) editing.value.conditionGroups = []
- editing.value.conditionGroups.push({
- name: `条件${editing.value.conditionGroups.length}`,
- when: '',
- rules: [],
- isDefault: false,
- child: createEndNode()
- })
- }
- function applyEdit() {
- if (!editing.value) return
- const live = findNode(root.value, editing.value.id)
- if (!live) {
- drawerVisible.value = false
- return
- }
- live.name = editing.value.name
- if (live.type === 'cc') {
- const types = [...new Set(cfg.assigneeTypes || [])].filter(Boolean) as AssigneeType[]
- if (!types.length) {
- ElMessage.warning('请至少选择一种选人方式')
- return
- }
- if (types.includes('users') && !(cfg.userIds || []).length) {
- ElMessage.warning('已勾选指定成员,请选择至少一名成员')
- return
- }
- if (types.includes('oa_roles') && !(cfg.oaRoleIds || []).length) {
- ElMessage.warning('已勾选工作流角色,请选择至少一个角色')
- return
- }
- live.approverConfig = {
- assigneeTypes: types,
- assigneeType: types[0],
- userIds: [...(cfg.userIds || [])],
- oaRoleIds: [...(cfg.oaRoleIds || [])]
- }
- } else if (live.type === 'approval') {
- live.approverConfig = {
- assigneeType: cfg.assigneeType,
- userIds: cfg.assigneeType === 'users' ? [...(cfg.userIds || [])] : undefined,
- oaRoleIds: cfg.assigneeType === 'oa_roles' ? [...(cfg.oaRoleIds || [])] : undefined,
- signMode: cfg.signMode || 'or'
- }
- }
- if (live.type === 'condition' && editing.value.conditionGroups) {
- const oldGroups = live.conditionGroups || []
- live.conditionGroups = editing.value.conditionGroups.map((g, i) => {
- const isDefault = !!g.isDefault
- return {
- ...g,
- isDefault,
- when: '',
- rules: isDefault ? [] : sanitizeConditionRules(g.rules),
- child: oldGroups[i]?.child || g.child || createEndNode()
- }
- })
- }
- commit()
- drawerVisible.value = false
- }
- onMounted(() => {
- commit()
- })
- </script>
- <style scoped lang="scss">
- .oa-flow-designer {
- display: flex;
- flex-direction: column;
- width: 100%;
- height: calc(100vh - 200px);
- min-height: 480px;
- }
- .oa-flow-designer__hint {
- flex-shrink: 0;
- margin: 0 0 12px;
- color: var(--mint-text-muted, #94a3b8);
- font-size: 13px;
- }
- .oa-flow-designer__canvas {
- flex: 1 1 auto;
- width: 100%;
- min-width: 0;
- min-height: 0;
- overflow: auto;
- background: #f8fafc;
- border: 1px solid var(--mint-border, #e2e8f0);
- border-radius: 10px;
- }
- .oa-flow-designer__canvas-inner {
- display: flex;
- justify-content: center;
- box-sizing: border-box;
- width: max-content;
- min-width: 100%;
- padding: 24px 16px;
- }
- .oa-flow-drawer__form {
- :deep(.el-form-item) {
- margin-bottom: 18px;
- }
- :deep(.el-form-item__content) {
- width: 100%;
- }
- }
- .assignee-seg {
- display: grid;
- grid-template-columns: repeat(5, minmax(0, 1fr));
- gap: 10px;
- width: 100%;
- }
- .assignee-seg__item {
- box-sizing: border-box;
- width: 100%;
- min-width: 0;
- padding: 12px 6px;
- color: var(--mint-text-muted, #64748b);
- font: inherit;
- font-size: 12px;
- line-height: 1.35;
- text-align: center;
- white-space: nowrap;
- background: #fff;
- border: 1px solid var(--mint-border, #e2e8f0);
- border-radius: 8px;
- cursor: pointer;
- transition: 0.15s ease;
- &:hover {
- color: var(--mint-text, #0f172a);
- border-color: #a7f3d0;
- }
- &.is-on {
- color: var(--primary-dark, #059669);
- font-weight: 600;
- background: var(--primary-bg, #ecfdf5);
- border-color: var(--primary, #10b981);
- }
- }
- .member-picker {
- width: 100%;
- }
- .member-option {
- display: flex;
- gap: 12px;
- align-items: center;
- width: 100%;
- min-height: 40px;
- padding: 4px 0;
- }
- .member-option__meta {
- display: flex;
- flex: 1;
- flex-direction: column;
- gap: 2px;
- justify-content: center;
- min-width: 0;
- line-height: 1.35;
- }
- .member-option__name {
- overflow: hidden;
- font-size: 13px;
- font-weight: 600;
- white-space: nowrap;
- text-overflow: ellipsis;
- }
- .member-option__sub {
- overflow: hidden;
- color: var(--mint-text-muted, #64748b);
- font-size: 12px;
- white-space: nowrap;
- text-overflow: ellipsis;
- }
- .member-option__tag {
- flex-shrink: 0;
- margin-left: auto;
- }
- .member-avatar {
- display: inline-flex;
- flex-shrink: 0;
- align-items: center;
- justify-content: center;
- width: 36px;
- height: 36px;
- color: #fff;
- font-size: 13px;
- font-weight: 700;
- line-height: 1;
- text-align: center;
- border-radius: 50%;
- &--sm {
- width: 28px;
- height: 28px;
- font-size: 11px;
- }
- }
- .member-chips {
- display: flex;
- flex-wrap: wrap;
- gap: 8px;
- min-height: 44px;
- margin-top: 12px;
- padding: 12px;
- background: #f8fafc;
- border: 1px dashed #cbd5e1;
- border-radius: 8px;
- &.is-empty {
- align-items: center;
- justify-content: center;
- color: var(--mint-text-muted, #94a3b8);
- font-size: 13px;
- }
- }
- .member-chip {
- display: inline-flex;
- gap: 8px;
- align-items: center;
- max-width: 100%;
- padding: 4px 8px 4px 4px;
- background: #fff;
- border: 1px solid var(--mint-border, #e2e8f0);
- border-radius: 999px;
- }
- .member-chip__name {
- max-width: 120px;
- overflow: hidden;
- font-size: 13px;
- font-weight: 500;
- white-space: nowrap;
- text-overflow: ellipsis;
- }
- .member-chip__rm {
- display: grid;
- flex-shrink: 0;
- place-items: center;
- width: 20px;
- height: 20px;
- padding: 0;
- color: var(--mint-text-muted, #64748b);
- font-size: 14px;
- line-height: 1;
- background: #f1f5f9;
- border: 0;
- border-radius: 50%;
- cursor: pointer;
- &:hover {
- color: #dc2626;
- background: #fee2e2;
- }
- }
- .member-picked-count {
- margin: 8px 0 0;
- color: var(--mint-text-muted, #94a3b8);
- font-size: 12px;
- }
- .sign-mode {
- display: flex;
- flex-direction: column;
- gap: 8px;
- width: 100%;
- }
- .sign-mode__card {
- display: block;
- padding: 12px 14px;
- cursor: pointer;
- background: #fff;
- border: 1px solid var(--mint-border, #e2e8f0);
- border-radius: 8px;
- transition: 0.15s ease;
- &:hover {
- border-color: #a7f3d0;
- }
- &.is-on {
- background: var(--primary-bg, #ecfdf5);
- border-color: var(--primary, #10b981);
- }
- :deep(.el-radio) {
- width: 100%;
- height: auto;
- margin-right: 0;
- align-items: flex-start;
- white-space: normal;
- }
- :deep(.el-radio__label) {
- padding-left: 10px;
- }
- }
- .sign-mode__text {
- display: flex;
- flex-direction: column;
- gap: 2px;
- line-height: 1.4;
- strong {
- font-size: 13px;
- font-weight: 600;
- }
- span {
- color: var(--mint-text-muted, #64748b);
- font-size: 12px;
- font-weight: 400;
- }
- }
- .cond-group-editor {
- padding: 12px;
- margin-bottom: 12px;
- background: #f8fafc;
- border: 1px solid var(--mint-border, #e2e8f0);
- border-radius: 8px;
- }
- .muted {
- color: var(--mint-text-muted, #94a3b8);
- font-size: 13px;
- }
- </style>
- <!-- append-to-body 后需非 scoped,才能作用到抽屉 root -->
- <style lang="scss">
- .oa-flow-drawer {
- .el-drawer__header {
- margin-bottom: 0;
- padding: 0;
- }
- .el-drawer__body {
- padding: 20px 28px 28px;
- }
- .oa-flow-drawer__hd {
- padding: 18px 28px 14px;
- background: linear-gradient(180deg, #ecfdf5 0%, #fff 72%);
- border-bottom: 1px solid var(--mint-border, #e2e8f0);
- }
- .oa-flow-drawer__hd-top {
- display: flex;
- gap: 12px;
- align-items: flex-start;
- margin-bottom: 14px;
- }
- .oa-flow-drawer__icon {
- display: grid;
- flex-shrink: 0;
- place-items: center;
- width: 40px;
- height: 40px;
- color: #fff;
- background: var(--primary, #10b981);
- border-radius: 10px;
- box-shadow: 0 4px 12px rgba(16, 185, 129, 0.28);
- }
- .oa-flow-drawer__titles {
- flex: 1;
- min-width: 0;
- }
- .oa-flow-drawer__title {
- margin: 0;
- font-size: 16px;
- font-weight: 700;
- line-height: 1.35;
- letter-spacing: 0.01em;
- }
- .oa-flow-drawer__sub {
- margin: 2px 0 0;
- color: var(--mint-text-muted, #64748b);
- font-size: 12px;
- line-height: 1.4;
- }
- .oa-flow-drawer__close {
- display: grid;
- flex-shrink: 0;
- place-items: center;
- width: 32px;
- height: 32px;
- padding: 0;
- color: var(--mint-text-muted, #64748b);
- background: transparent;
- border: 0;
- border-radius: 8px;
- cursor: pointer;
- &:hover {
- color: var(--mint-text, #0f172a);
- background: #f1f5f9;
- }
- }
- .oa-flow-drawer__actions {
- display: flex;
- gap: 8px;
- justify-content: flex-end;
- }
- }
- /* 下拉挂 body,须非 scoped */
- .oa-flow-member-select-popper {
- .el-select-dropdown__list {
- padding: 8px;
- }
- .el-select-dropdown__item {
- height: auto !important;
- min-height: 56px;
- margin-bottom: 8px;
- padding: 10px 12px !important;
- line-height: normal !important;
- border-radius: 8px;
- &:last-child {
- margin-bottom: 0;
- }
- &.is-disabled {
- opacity: 0.55;
- }
- &.hover,
- &:hover {
- background: var(--primary-bg, #ecfdf5);
- }
- }
- .member-option {
- display: flex;
- gap: 12px;
- align-items: center;
- width: 100%;
- min-height: 36px;
- }
- .member-option__meta {
- display: flex;
- flex: 1;
- flex-direction: column;
- gap: 2px;
- justify-content: center;
- min-width: 0;
- }
- .member-option__name {
- overflow: hidden;
- color: var(--mint-text, #0f172a);
- font-size: 13px;
- font-weight: 600;
- white-space: nowrap;
- text-overflow: ellipsis;
- }
- .member-option__sub {
- overflow: hidden;
- color: var(--mint-text-muted, #64748b);
- font-size: 12px;
- white-space: nowrap;
- text-overflow: ellipsis;
- }
- .member-option__tag {
- flex-shrink: 0;
- margin-left: auto;
- }
- .member-avatar {
- display: inline-flex;
- flex-shrink: 0;
- align-items: center;
- justify-content: center;
- width: 36px;
- height: 36px;
- color: #fff;
- font-size: 13px;
- font-weight: 700;
- line-height: 1;
- text-align: center;
- border-radius: 50%;
- }
- }
- </style>
|