| | |
| | | <text class="loading-text">加载中...</text> |
| | | </view> |
| | | <template v-else-if="detail"> |
| | | <view class="section"> |
| | | <view class="section-title">基本信息</view> |
| | | <view class="form-body"> |
| | | <view class="form-row"> |
| | | <text class="form-label required">审批标题</text> |
| | | <up-form :model="form" |
| | | label-width="88" |
| | | input-align="right"> |
| | | <u-cell-group title="基本信息" |
| | | class="form-section"> |
| | | <up-form-item label="审批标题" |
| | | required |
| | | class="form-item-name"> |
| | | <up-input v-model="form.title" |
| | | class="name-input-inline" |
| | | placeholder="请输入审批标题" |
| | | maxlength="100" |
| | | clearable /> |
| | | </view> |
| | | <view class="form-row"> |
| | | <text class="form-label">审批模板</text> |
| | | <text class="form-readonly">{{ templateName }}</text> |
| | | </view> |
| | | <view class="form-row"> |
| | | <text class="form-label">申请人</text> |
| | | <text class="form-readonly">{{ displayApplicantName }}</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </up-form-item> |
| | | <up-form-item label="审批模板" |
| | | class="form-item-readonly"> |
| | | <up-input :model-value="templateName" |
| | | readonly /> |
| | | </up-form-item> |
| | | <up-form-item label="申请人" |
| | | class="form-item-readonly"> |
| | | <up-input :model-value="displayApplicantName" |
| | | readonly /> |
| | | </up-form-item> |
| | | </u-cell-group> |
| | | </up-form> |
| | | |
| | | <view class="section"> |
| | | <view class="section-title">填报内容</view> |
| | | <view class="section-card"> |
| | | <view class="section-head"> |
| | | <text class="section-title">填报内容</text> |
| | | </view> |
| | | <view v-if="formConfigData.prompt" |
| | | class="form-prompt"> |
| | | {{ formConfigData.prompt }} |
| | | </view> |
| | | <view v-if="formConfigData.fields.length" |
| | | class="form-body"> |
| | | <view v-for="field in formConfigData.fields" |
| | | :key="field.key" |
| | | class="form-row form-row--field"> |
| | | <text class="form-label" |
| | | :class="{ required: field.required }">{{ field.label }}</text> |
| | | <up-textarea v-if="field.type === 'textarea'" |
| | | <up-form v-if="formConfigData.fields.length" |
| | | :model="formValues" |
| | | label-width="88" |
| | | input-align="right" |
| | | class="dynamic-form"> |
| | | <up-form-item v-for="field in formConfigData.fields" |
| | | :key="field.key" |
| | | :label="field.label" |
| | | :required="!!field.required" |
| | | :class="formItemClass(field)"> |
| | | <up-textarea v-if="isTextareaField(field)" |
| | | v-model="formValues[field.key]" |
| | | :placeholder="`请输入${field.label}`" |
| | | maxlength="500" |
| | | border="surround" |
| | | height="80" /> |
| | | <view v-else-if="field.type === 'date'" |
| | | class="date-trigger" |
| | | @click="openDatePicker(field.key)"> |
| | | <up-input :model-value="formValues[field.key]" |
| | | <view v-else-if="isDatetimerangeField(field)" |
| | | class="daterange-fill"> |
| | | <view class="range-fill-row" |
| | | @click="openRangePicker(field, 'start')"> |
| | | <text class="range-fill-label">开始</text> |
| | | <up-input :model-value="getRangePartDisplay(field, 'start')" |
| | | placeholder="开始时间" |
| | | readonly /> |
| | | <up-icon name="calendar" |
| | | size="16" |
| | | color="#909399" /> |
| | | </view> |
| | | <text class="range-fill-sep">至</text> |
| | | <view class="range-fill-row" |
| | | @click="openRangePicker(field, 'end')"> |
| | | <text class="range-fill-label">结束</text> |
| | | <up-input :model-value="getRangePartDisplay(field, 'end')" |
| | | placeholder="结束时间" |
| | | readonly /> |
| | | <up-icon name="calendar" |
| | | size="16" |
| | | color="#909399" /> |
| | | </view> |
| | | </view> |
| | | <view v-else-if="isDateLikeField(field)" |
| | | class="field-trigger" |
| | | @click="openDatePicker(field)"> |
| | | <up-input :model-value="formatFieldDisplayValue(field, formValues[field.key])" |
| | | :placeholder="`请选择${field.label}`" |
| | | readonly /> |
| | | <up-icon :name="getDatePickerMode(field) === 'time' ? 'clock' : 'calendar'" |
| | | size="18" |
| | | color="#909399" /> |
| | | </view> |
| | | <view v-else-if="isSelectField(field)" |
| | | class="field-trigger" |
| | | @click="openSelectPicker(field)"> |
| | | <up-input :model-value="getSelectDisplayText(field)" |
| | | :placeholder="`请选择${field.label}`" |
| | | readonly /> |
| | | <up-icon name="arrow-right" |
| | | size="16" |
| | | color="#c0c4cc" /> |
| | | </view> |
| | | <up-input v-else |
| | | v-model="formValues[field.key]" |
| | | :type="field.type === 'number' ? 'digit' : 'text'" |
| | | :type="isNumberField(field) ? 'digit' : 'text'" |
| | | :placeholder="`请输入${field.label}`" |
| | | clearable /> |
| | | </view> |
| | | </view> |
| | | </up-form-item> |
| | | </up-form> |
| | | <view v-else |
| | | class="empty-hint">该模板暂无填报项</view> |
| | | </view> |
| | | |
| | | <view class="section"> |
| | | <view class="section-title">审批流程</view> |
| | | <view class="section-card"> |
| | | <view class="section-head"> |
| | | <text class="section-title">审批流程</text> |
| | | </view> |
| | | <view v-if="detail.nodes?.length" |
| | | class="flow-list"> |
| | | <view v-for="(node, index) in detail.nodes" |
| | | :key="node.id || index" |
| | | class="flow-card"> |
| | | <view class="flow-card-head"> |
| | | <text class="flow-level">第{{ levelLabel(node.levelNo || index + 1) }}级</text> |
| | | <text class="flow-type">{{ approveTypeText(node.approveType) }}</text> |
| | | class="flow-wrap"> |
| | | <view v-for="(node, nodeIndex) in detail.nodes" |
| | | :key="node.id || nodeIndex" |
| | | class="flow-node-block"> |
| | | <view class="flow-node-card"> |
| | | <view class="node-header"> |
| | | <view class="node-level-badge">{{ node.levelNo || nodeIndex + 1 }}</view> |
| | | <text class="node-level-text">第{{ levelLabel(node.levelNo || nodeIndex + 1) }}级</text> |
| | | </view> |
| | | <view class="approve-type-row approve-type-row--readonly"> |
| | | <view class="type-btn" |
| | | :class="{ active: node.approveType !== 'OR' }"> |
| | | 会签 |
| | | </view> |
| | | <view class="type-btn" |
| | | :class="{ active: node.approveType === 'OR' }"> |
| | | 或签 |
| | | </view> |
| | | </view> |
| | | <view class="approver-list"> |
| | | <view v-for="(approver, aIdx) in node.approvers || []" |
| | | :key="approver.id || aIdx" |
| | | class="approver-chip"> |
| | | <view class="approver-avatar">{{ (approver.approverName || "?").charAt(0) }}</view> |
| | | <text class="approver-name">{{ approver.approverName || "-" }}</text> |
| | | </view> |
| | | <text v-if="!(node.approvers || []).length" |
| | | class="empty-hint inline">暂无审批人</text> |
| | | </view> |
| | | </view> |
| | | <view class="approver-tags"> |
| | | <text v-for="(approver, aIdx) in node.approvers || []" |
| | | :key="approver.id || aIdx" |
| | | class="approver-tag"> |
| | | {{ approver.approverName || "-" }} |
| | | </text> |
| | | <text v-if="!(node.approvers || []).length" |
| | | class="empty-hint inline">暂无审批人</text> |
| | | <view v-if="nodeIndex < detail.nodes.length - 1" |
| | | class="flow-connector"> |
| | | <view class="flow-connector-line" /> |
| | | </view> |
| | | </view> |
| | | </view> |
| | |
| | | @close="showDatePicker = false"> |
| | | <up-datetime-picker :show="true" |
| | | v-model="datePickerTs" |
| | | mode="date" |
| | | :mode="datePickerMode" |
| | | @confirm="onDateConfirm" |
| | | @cancel="showDatePicker = false" /> |
| | | @cancel="onDatePickerCancel" /> |
| | | </up-popup> |
| | | |
| | | <up-action-sheet :show="showSelectSheet" |
| | | :title="selectSheetTitle" |
| | | :actions="selectSheetActions" |
| | | @select="onSelectOption" |
| | | @close="showSelectSheet = false" /> |
| | | </view> |
| | | </template> |
| | | |
| | |
| | | updateApprovalInstance, |
| | | } from "@/api/oa/approvalInstance.js"; |
| | | import useUserStore from "@/store/modules/user"; |
| | | import { formatDateToYMD, parseTime } from "@/utils/ruoyi"; |
| | | import { parseTime } from "@/utils/ruoyi"; |
| | | import { getDept } from "@/api/collaborativeApproval/approvalProcess.js"; |
| | | import { userListNoPageByTenantId } from "@/api/system/user"; |
| | | import { |
| | | formatDatetimerangeDisplay, |
| | | formatFieldDateValue, |
| | | formatFieldDisplayValue, |
| | | getDatePickerMode, |
| | | getFieldInitialValue, |
| | | getFieldOptionLabel, |
| | | isDatetimerangeField, |
| | | isDateLikeField, |
| | | isNumberField, |
| | | isSelectField, |
| | | isTextareaField, |
| | | joinDatetimerangeValue, |
| | | mergeFormConfigForEdit, |
| | | parseDatetimerangeValue, |
| | | resolveFieldOptions, |
| | | parseApprovalFormConfig, |
| | | parseFieldDateToTs, |
| | | } from "../../_utils/approvalFormField.js"; |
| | | |
| | | const EDIT_STORAGE_KEY = "oa_approve_instance_edit_row"; |
| | | const LEVEL_TEXT = ["", "一", "二", "三", "四", "五", "六", "七", "八", "九", "十"]; |
| | |
| | | |
| | | const showDatePicker = ref(false); |
| | | const datePickerTs = ref(Date.now()); |
| | | const activeDateFieldKey = ref(""); |
| | | const activeDateField = ref(null); |
| | | const activeRangePart = ref("start"); |
| | | |
| | | const datePickerMode = computed(() => { |
| | | const field = activeDateField.value; |
| | | if (!field) return "date"; |
| | | if (isDatetimerangeField(field)) return "datetime"; |
| | | return getDatePickerMode(field); |
| | | }); |
| | | |
| | | const showSelectSheet = ref(false); |
| | | const activeSelectField = ref(null); |
| | | const pickerUserList = ref([]); |
| | | const pickerDeptList = ref([]); |
| | | |
| | | const isEditMode = computed(() => !!instanceId.value); |
| | | |
| | | const pageTitle = computed(() => (isEditMode.value ? "编辑审批" : "发起审批")); |
| | | const confirmText = computed(() => (isEditMode.value ? "保存" : "提交审批")); |
| | | const pageTitle = computed(() => (isEditMode.value ? "修改审批" : "发起审批")); |
| | | const confirmText = computed(() => (isEditMode.value ? "保存修改" : "提交审批")); |
| | | |
| | | const applicantName = computed( |
| | | () => userStore.nickName || userStore.name || "-" |
| | |
| | | () => detail.value?.templateName || instanceRow.value?.templateName || "-" |
| | | ); |
| | | |
| | | const parseFormConfig = raw => { |
| | | if (!raw) return { prompt: "", fields: [] }; |
| | | try { |
| | | const obj = typeof raw === "string" ? JSON.parse(raw) : raw; |
| | | return { |
| | | prompt: obj?.prompt || "", |
| | | fields: Array.isArray(obj?.fields) ? obj.fields : [], |
| | | }; |
| | | } catch { |
| | | return { prompt: "", fields: [] }; |
| | | } |
| | | }; |
| | | |
| | | const formConfigData = computed(() => { |
| | | const raw = isEditMode.value |
| | | ? instanceRow.value?.formConfig |
| | | : detail.value?.formConfig; |
| | | return parseFormConfig(raw); |
| | | if (isEditMode.value) { |
| | | return mergeFormConfigForEdit( |
| | | detail.value?.formConfig, |
| | | instanceRow.value?.formConfig |
| | | ); |
| | | } |
| | | return parseApprovalFormConfig(detail.value?.formConfig); |
| | | }); |
| | | |
| | | const levelLabel = n => LEVEL_TEXT[Number(n)] || String(n); |
| | | const approveTypeText = type => (type === "OR" ? "或签" : "会签"); |
| | | |
| | | const selectSheetTitle = computed( |
| | | () => (activeSelectField.value?.label ? `选择${activeSelectField.value.label}` : "请选择") |
| | | ); |
| | | |
| | | const selectSheetActions = computed(() => { |
| | | const field = activeSelectField.value; |
| | | if (!field) return []; |
| | | return resolveFieldOptions(field, { |
| | | users: pickerUserList.value, |
| | | depts: pickerDeptList.value, |
| | | }).map(opt => ({ |
| | | name: opt.label, |
| | | value: opt.value, |
| | | })); |
| | | }); |
| | | |
| | | const formItemClass = field => { |
| | | if (isTextareaField(field)) return "form-item-textarea"; |
| | | if (isDatetimerangeField(field)) return "form-item-daterange"; |
| | | if (isSelectField(field) || isDateLikeField(field)) return "form-item-select"; |
| | | return "form-item-inline"; |
| | | }; |
| | | |
| | | const getRangePartDisplay = (field, part) => { |
| | | const parts = parseDatetimerangeValue(formValues[field.key]); |
| | | const val = part === "start" ? parts.start : parts.end; |
| | | return val ? formatFieldDisplayValue({ type: "datetime" }, val) : ""; |
| | | }; |
| | | |
| | | const openRangePicker = (field, part) => { |
| | | activeDateField.value = field; |
| | | activeRangePart.value = part; |
| | | const parts = parseDatetimerangeValue(formValues[field.key]); |
| | | const val = part === "start" ? parts.start : parts.end; |
| | | datePickerTs.value = parseFieldDateToTs(val) ?? Date.now(); |
| | | showDatePicker.value = true; |
| | | }; |
| | | |
| | | const getSelectDisplayText = field => { |
| | | const stored = formValues[field.key]; |
| | | const options = resolveFieldOptions(field, { |
| | | users: pickerUserList.value, |
| | | depts: pickerDeptList.value, |
| | | }); |
| | | const matched = options.find( |
| | | opt => |
| | | String(opt.value) === String(stored) || String(opt.label) === String(stored) |
| | | ); |
| | | return ( |
| | | matched?.label || |
| | | getFieldOptionLabel(field, stored) || |
| | | (stored !== undefined && stored !== null ? String(stored) : "") |
| | | ); |
| | | }; |
| | | |
| | | const initFormValues = fields => { |
| | | Object.keys(formValues).forEach(key => { |
| | |
| | | }); |
| | | fields.forEach(field => { |
| | | if (!field?.key) return; |
| | | formValues[field.key] = field.value ?? field.defaultValue ?? ""; |
| | | formValues[field.key] = getFieldInitialValue(field); |
| | | }); |
| | | }; |
| | | |
| | | const openDatePicker = fieldKey => { |
| | | activeDateFieldKey.value = fieldKey; |
| | | const current = formValues[fieldKey]; |
| | | datePickerTs.value = current ? new Date(current).getTime() : Date.now(); |
| | | const openSelectPicker = field => { |
| | | const options = resolveFieldOptions(field, { |
| | | users: pickerUserList.value, |
| | | depts: pickerDeptList.value, |
| | | }); |
| | | if (!options.length) { |
| | | uni.showToast({ title: "该字段未配置下拉选项", icon: "none" }); |
| | | return; |
| | | } |
| | | activeSelectField.value = field; |
| | | showSelectSheet.value = true; |
| | | }; |
| | | |
| | | const onSelectOption = action => { |
| | | const key = activeSelectField.value?.key; |
| | | if (key) { |
| | | formValues[key] = action.value; |
| | | } |
| | | showSelectSheet.value = false; |
| | | activeSelectField.value = null; |
| | | }; |
| | | |
| | | const openDatePicker = field => { |
| | | activeDateField.value = field; |
| | | const current = formValues[field.key]; |
| | | datePickerTs.value = parseFieldDateToTs(current) ?? Date.now(); |
| | | showDatePicker.value = true; |
| | | }; |
| | | |
| | | const onDatePickerCancel = () => { |
| | | showDatePicker.value = false; |
| | | activeDateField.value = null; |
| | | }; |
| | | |
| | | const onDateConfirm = e => { |
| | | const ts = e?.value ?? datePickerTs.value; |
| | | if (activeDateFieldKey.value) { |
| | | formValues[activeDateFieldKey.value] = formatDateToYMD(ts); |
| | | const field = activeDateField.value; |
| | | if (field?.key) { |
| | | if (isDatetimerangeField(field)) { |
| | | const parts = parseDatetimerangeValue(formValues[field.key]); |
| | | const formatted = formatFieldDateValue({ type: "datetime" }, ts); |
| | | formValues[field.key] = joinDatetimerangeValue( |
| | | activeRangePart.value === "start" ? formatted : parts.start, |
| | | activeRangePart.value === "end" ? formatted : parts.end |
| | | ); |
| | | } else { |
| | | formValues[field.key] = formatFieldDateValue(field, ts); |
| | | } |
| | | } |
| | | showDatePicker.value = false; |
| | | onDatePickerCancel(); |
| | | }; |
| | | |
| | | const validateForm = () => { |
| | |
| | | if (!field.required) continue; |
| | | const val = formValues[field.key]; |
| | | if (val === undefined || val === null || String(val).trim() === "") { |
| | | uni.showToast({ title: `请填写${field.label}`, icon: "none" }); |
| | | const action = |
| | | isSelectField(field) || isDateLikeField(field) || isDatetimerangeField(field) |
| | | ? "请选择" |
| | | : "请填写"; |
| | | uni.showToast({ title: `${action}${field.label}`, icon: "none" }); |
| | | return false; |
| | | } |
| | | if (isDatetimerangeField(field)) { |
| | | const { start, end } = parseDatetimerangeValue(val); |
| | | if (!start || !end) { |
| | | uni.showToast({ title: `请完整选择${field.label}`, icon: "none" }); |
| | | return false; |
| | | } |
| | | } |
| | | if (isSelectField(field)) { |
| | | const options = resolveFieldOptions(field, { |
| | | users: pickerUserList.value, |
| | | depts: pickerDeptList.value, |
| | | }); |
| | | if ( |
| | | options.length && |
| | | !options.some( |
| | | opt => |
| | | String(opt.value) === String(val) || String(opt.label) === String(val) |
| | | ) |
| | | ) { |
| | | uni.showToast({ title: `${field.label}选项无效`, icon: "none" }); |
| | | return false; |
| | | } |
| | | } |
| | | } |
| | | if (!detail.value?.nodes?.length) { |
| | |
| | | templateId: row.templateId ?? detail.value?.id, |
| | | templateName: row.templateName ?? detail.value?.templateName, |
| | | businessId: row.businessId, |
| | | businessType: row.businessType, |
| | | businessType: row.businessType ?? detail.value?.businessType, |
| | | title: form.title.trim(), |
| | | status: row.status || "PENDING", |
| | | currentLevel: row.currentLevel, |
| | | applicantId: row.applicantId, |
| | | applicantName: row.applicantName, |
| | | applyTime: row.applyTime, |
| | | finishTime: row.finishTime, |
| | | createUser: row.createUser, |
| | | createTime: row.createTime, |
| | | updateUser: row.updateUser, |
| | | updateTime: row.updateTime, |
| | | deptId: row.deptId, |
| | | deleted: row.deleted, |
| | | formConfig: buildFormConfigPayload(), |
| | | approveAction: row.approveAction, |
| | | approveComment: row.approveComment, |
| | | }; |
| | | }; |
| | | |
| | |
| | | submitApi(payload) |
| | | .then(() => { |
| | | uni.showToast({ |
| | | title: isEditMode.value ? "保存成功" : "提交成功", |
| | | title: isEditMode.value ? "修改成功" : "提交成功", |
| | | icon: "success", |
| | | }); |
| | | if (isEditMode.value) { |
| | |
| | | }) |
| | | .catch(() => { |
| | | uni.showToast({ |
| | | title: isEditMode.value ? "保存失败" : "提交失败", |
| | | title: isEditMode.value ? "修改失败" : "提交失败", |
| | | icon: "none", |
| | | }); |
| | | }) |
| | |
| | | const row = uni.getStorageSync(EDIT_STORAGE_KEY); |
| | | if (!row || String(row.id) !== String(instanceId.value)) { |
| | | uni.showToast({ title: "未获取到审批数据", icon: "none" }); |
| | | setTimeout(() => uni.navigateBack(), 500); |
| | | return; |
| | | } |
| | | uni.removeStorageSync(EDIT_STORAGE_KEY); |
| | | instanceRow.value = row; |
| | | templateId.value = row.templateId; |
| | | form.title = row.title || ""; |
| | |
| | | detail.value = null; |
| | | try { |
| | | await loadTemplateDetail(); |
| | | if (!detail.value) return; |
| | | initFormValues(formConfigData.value.fields); |
| | | } finally { |
| | | loading.value = false; |
| | |
| | | uni.navigateBack(); |
| | | }; |
| | | |
| | | const loadPickerSourceData = () => { |
| | | userListNoPageByTenantId() |
| | | .then(res => { |
| | | pickerUserList.value = res?.data || []; |
| | | }) |
| | | .catch(() => { |
| | | pickerUserList.value = []; |
| | | }); |
| | | getDept() |
| | | .then(res => { |
| | | pickerDeptList.value = res?.data || []; |
| | | }) |
| | | .catch(() => { |
| | | pickerDeptList.value = []; |
| | | }); |
| | | }; |
| | | |
| | | onLoad(options => { |
| | | loadPickerSourceData(); |
| | | if (options?.id) { |
| | | instanceId.value = options.id; |
| | | loadForEdit(); |
| | |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | @import "@/static/scss/form-common.scss"; |
| | | |
| | | $primary: #2979ff; |
| | | $text: #1f2d3d; |
| | | $text-secondary: #606266; |
| | | $text-muted: #909399; |
| | | $bg-page: #f0f3f8; |
| | | $radius-lg: 12px; |
| | | $radius-md: 10px; |
| | | $shadow-card: 0 2px 12px rgba(31, 45, 61, 0.05); |
| | | |
| | | .approve-apply-page { |
| | | display: flex; |
| | | flex-direction: column; |
| | | min-height: 100vh; |
| | | background: #f0f3f8; |
| | | background: $bg-page; |
| | | } |
| | | |
| | | .form-scroll { |
| | |
| | | |
| | | .loading-text { |
| | | font-size: 14px; |
| | | color: $text-muted; |
| | | } |
| | | |
| | | .form-section { |
| | | margin-bottom: 10px; |
| | | border-radius: $radius-lg; |
| | | overflow: hidden; |
| | | box-shadow: $shadow-card; |
| | | } |
| | | |
| | | :deep(.form-section .u-cell-group__title) { |
| | | padding: 12px 16px 8px !important; |
| | | font-size: 15px !important; |
| | | font-weight: 600 !important; |
| | | color: $text !important; |
| | | background: #fff !important; |
| | | } |
| | | |
| | | :deep(.form-section .u-form-item) { |
| | | padding: 0 16px !important; |
| | | } |
| | | |
| | | :deep(.form-section .u-form-item__body) { |
| | | padding: 10px 0 !important; |
| | | min-height: auto !important; |
| | | } |
| | | |
| | | :deep(.form-item-name .u-form-item__body) { |
| | | flex-direction: row !important; |
| | | align-items: center !important; |
| | | } |
| | | |
| | | :deep(.form-item-name .u-form-item__content) { |
| | | flex: 1 !important; |
| | | min-width: 0 !important; |
| | | justify-content: flex-end !important; |
| | | } |
| | | |
| | | :deep(.name-input-inline), |
| | | :deep(.name-input-inline .u-input__content) { |
| | | width: 100% !important; |
| | | flex: 1 !important; |
| | | } |
| | | |
| | | :deep(.name-input-inline input), |
| | | :deep(.name-input-inline .u-input__content__field-wrapper__field) { |
| | | width: 100% !important; |
| | | text-align: right !important; |
| | | font-size: 15px !important; |
| | | } |
| | | |
| | | :deep(.form-item-readonly .u-form-item__body) { |
| | | align-items: center !important; |
| | | } |
| | | |
| | | :deep(.form-item-readonly .u-form-item__content) { |
| | | flex: 1 !important; |
| | | min-width: 0 !important; |
| | | justify-content: flex-end !important; |
| | | } |
| | | |
| | | :deep(.form-item-readonly .u-input__content__field-wrapper__field) { |
| | | text-align: right !important; |
| | | color: #303133 !important; |
| | | } |
| | | |
| | | .dynamic-form { |
| | | padding: 0 0 4px; |
| | | } |
| | | |
| | | :deep(.dynamic-form .u-form-item) { |
| | | padding: 0 16px !important; |
| | | } |
| | | |
| | | :deep(.dynamic-form .u-form-item__body) { |
| | | padding: 10px 0 !important; |
| | | min-height: auto !important; |
| | | } |
| | | |
| | | :deep(.form-item-inline .u-form-item__body) { |
| | | flex-direction: row !important; |
| | | align-items: center !important; |
| | | } |
| | | |
| | | :deep(.form-item-inline .u-form-item__content) { |
| | | flex: 1 !important; |
| | | min-width: 0 !important; |
| | | justify-content: flex-end !important; |
| | | } |
| | | |
| | | :deep(.form-item-inline input), |
| | | :deep(.form-item-inline .u-input__content__field-wrapper__field) { |
| | | text-align: right !important; |
| | | font-size: 15px !important; |
| | | } |
| | | |
| | | :deep(.form-item-select .u-form-item__body) { |
| | | align-items: center !important; |
| | | } |
| | | |
| | | :deep(.form-item-select .u-form-item__content) { |
| | | flex: 1 !important; |
| | | min-width: 0 !important; |
| | | justify-content: flex-end !important; |
| | | } |
| | | |
| | | :deep(.form-item-textarea .u-form-item__body) { |
| | | flex-direction: column !important; |
| | | align-items: stretch !important; |
| | | padding: 10px 0 12px !important; |
| | | } |
| | | |
| | | :deep(.form-item-textarea .u-form-item__content) { |
| | | width: 100% !important; |
| | | justify-content: stretch !important; |
| | | } |
| | | |
| | | .field-trigger { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: flex-end; |
| | | gap: 6px; |
| | | width: 100%; |
| | | min-width: 0; |
| | | } |
| | | |
| | | :deep(.field-trigger .u-input) { |
| | | flex: 1 !important; |
| | | min-width: 0 !important; |
| | | } |
| | | |
| | | :deep(.field-trigger .u-input__content__field-wrapper__field) { |
| | | text-align: right !important; |
| | | font-size: 15px !important; |
| | | } |
| | | |
| | | :deep(.form-item-daterange .u-form-item__body) { |
| | | flex-direction: column !important; |
| | | align-items: stretch !important; |
| | | } |
| | | |
| | | :deep(.form-item-daterange .u-form-item__content) { |
| | | width: 100% !important; |
| | | justify-content: stretch !important; |
| | | } |
| | | |
| | | .daterange-fill { |
| | | width: 100%; |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 8px; |
| | | } |
| | | |
| | | .range-fill-row { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | padding: 8px 10px; |
| | | background: #f7f9fc; |
| | | border: 1px solid #eef1f6; |
| | | border-radius: 8px; |
| | | } |
| | | |
| | | .range-fill-label { |
| | | flex-shrink: 0; |
| | | width: 36px; |
| | | font-size: 13px; |
| | | color: #909399; |
| | | } |
| | | |
| | | .section { |
| | | background: #fff; |
| | | border-radius: 12px; |
| | | .range-fill-sep { |
| | | font-size: 12px; |
| | | color: #c0c4cc; |
| | | text-align: center; |
| | | } |
| | | |
| | | :deep(.range-fill-row .u-input) { |
| | | flex: 1 !important; |
| | | min-width: 0 !important; |
| | | } |
| | | |
| | | .section-card { |
| | | margin-bottom: 10px; |
| | | background: #fff; |
| | | border-radius: $radius-lg; |
| | | overflow: hidden; |
| | | box-shadow: 0 2px 12px rgba(31, 45, 61, 0.05); |
| | | box-shadow: $shadow-card; |
| | | } |
| | | |
| | | .section-head { |
| | | padding: 12px 16px; |
| | | border-bottom: 1px solid #f2f4f7; |
| | | } |
| | | |
| | | .section-title { |
| | | padding: 12px 16px; |
| | | font-size: 15px; |
| | | font-weight: 600; |
| | | color: #1f2d3d; |
| | | border-bottom: 1px solid #f2f4f7; |
| | | border-left: 3px solid #2979ff; |
| | | padding-left: 13px; |
| | | } |
| | | |
| | | .form-body { |
| | | padding: 8px 16px 16px; |
| | | } |
| | | |
| | | .form-row { |
| | | padding: 10px 0; |
| | | border-bottom: 1px solid #f5f7fa; |
| | | |
| | | &:last-child { |
| | | border-bottom: none; |
| | | } |
| | | |
| | | &--field { |
| | | flex-direction: column; |
| | | align-items: stretch; |
| | | } |
| | | } |
| | | |
| | | .form-label { |
| | | display: block; |
| | | margin-bottom: 8px; |
| | | font-size: 14px; |
| | | color: #606266; |
| | | |
| | | &.required::before { |
| | | content: "*"; |
| | | color: #f56c6c; |
| | | margin-right: 4px; |
| | | } |
| | | } |
| | | |
| | | .form-readonly { |
| | | font-size: 14px; |
| | | color: #303133; |
| | | color: $text; |
| | | padding-left: 10px; |
| | | border-left: 3px solid $primary; |
| | | line-height: 1.2; |
| | | } |
| | | |
| | | .form-prompt { |
| | | margin: 12px 16px 0; |
| | | padding: 10px 12px; |
| | | font-size: 13px; |
| | | color: #606266; |
| | | color: $text-secondary; |
| | | background: #f8fafc; |
| | | border-radius: 8px; |
| | | line-height: 1.5; |
| | | } |
| | | |
| | | .date-trigger { |
| | | width: 100%; |
| | | .flow-wrap { |
| | | padding: 10px 16px 14px; |
| | | } |
| | | |
| | | .flow-list { |
| | | .flow-node-block { |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: stretch; |
| | | } |
| | | |
| | | .flow-node-card { |
| | | background: #fafbfd; |
| | | border: 1px solid #e8eef5; |
| | | border-radius: $radius-md; |
| | | padding: 12px; |
| | | } |
| | | |
| | | .flow-card { |
| | | padding: 12px; |
| | | margin-bottom: 8px; |
| | | background: #f8fafc; |
| | | .node-header { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 10px; |
| | | margin-bottom: 10px; |
| | | } |
| | | |
| | | .node-level-badge { |
| | | width: 26px; |
| | | height: 26px; |
| | | border-radius: 8px; |
| | | border: 1px solid #eef2f6; |
| | | background: $primary; |
| | | color: #fff; |
| | | font-size: 14px; |
| | | font-weight: 600; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | flex-shrink: 0; |
| | | } |
| | | |
| | | &:last-child { |
| | | margin-bottom: 0; |
| | | .node-level-text { |
| | | flex: 1; |
| | | font-size: 15px; |
| | | font-weight: 600; |
| | | color: $text; |
| | | } |
| | | |
| | | .approve-type-row { |
| | | display: flex; |
| | | background: #f0f3f8; |
| | | border-radius: 8px; |
| | | padding: 3px; |
| | | margin-bottom: 10px; |
| | | |
| | | &--readonly { |
| | | pointer-events: none; |
| | | } |
| | | } |
| | | |
| | | .flow-card-head { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | margin-bottom: 8px; |
| | | } |
| | | |
| | | .flow-level { |
| | | .type-btn { |
| | | flex: 1; |
| | | text-align: center; |
| | | padding: 8px 0; |
| | | font-size: 14px; |
| | | font-weight: 600; |
| | | color: #303133; |
| | | color: $text-secondary; |
| | | border-radius: 6px; |
| | | |
| | | &.active { |
| | | background: #fff; |
| | | color: $primary; |
| | | font-weight: 500; |
| | | } |
| | | } |
| | | |
| | | .flow-type { |
| | | font-size: 13px; |
| | | color: #2979ff; |
| | | } |
| | | |
| | | .approver-tags { |
| | | .approver-list { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | gap: 8px; |
| | | align-items: center; |
| | | } |
| | | |
| | | .approver-tag { |
| | | padding: 4px 10px; |
| | | font-size: 13px; |
| | | color: #303133; |
| | | .approver-chip { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | padding: 6px 12px 6px 6px; |
| | | background: #fff; |
| | | border: 1px solid #dce8f8; |
| | | border-radius: 16px; |
| | | border-radius: 24px; |
| | | box-shadow: 0 2px 6px rgba(41, 121, 255, 0.06); |
| | | } |
| | | |
| | | .approver-avatar { |
| | | width: 26px; |
| | | height: 26px; |
| | | border-radius: 50%; |
| | | background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| | | color: #fff; |
| | | font-size: 12px; |
| | | font-weight: 600; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | } |
| | | |
| | | .approver-name { |
| | | font-size: 13px; |
| | | color: $text; |
| | | max-width: 120px; |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | white-space: nowrap; |
| | | } |
| | | |
| | | .flow-connector { |
| | | display: flex; |
| | | justify-content: center; |
| | | padding: 4px 0; |
| | | } |
| | | |
| | | .flow-connector-line { |
| | | width: 2px; |
| | | height: 14px; |
| | | background: #d0dff0; |
| | | } |
| | | |
| | | .empty-hint { |
| | | padding: 12px 16px 16px; |
| | | font-size: 13px; |
| | | color: #909399; |
| | | color: $text-muted; |
| | | |
| | | &.inline { |
| | | padding: 0; |