| | |
| | | class="name-input-inline" |
| | | placeholder="请输入模板名称" |
| | | maxlength="50" |
| | | clearable /> |
| | | :disabled="isSystemTemplate" |
| | | :clearable="!isSystemTemplate" /> |
| | | </up-form-item> |
| | | <up-form-item label="审批类型" |
| | | prop="businessType" |
| | | required |
| | | class="form-item-select" |
| | | :class="{ 'form-item-select--disabled': isSystemTemplate }" |
| | | @click="openBusinessTypeSheet"> |
| | | <up-input :model-value="businessTypeText" |
| | | placeholder="请选择审批类型" |
| | | readonly /> |
| | | <template #right> |
| | | readonly |
| | | :disabled="isSystemTemplate" /> |
| | | <template v-if="!isSystemTemplate" |
| | | #right> |
| | | <up-icon name="arrow-right" |
| | | @click.stop="openBusinessTypeSheet" /> |
| | | </template> |
| | |
| | | |
| | | <view class="section-card"> |
| | | <view class="section-head section-head--between"> |
| | | <text class="section-title">填报配置</text> |
| | | <view class="section-head-left"> |
| | | <text class="section-title">填报项配置</text> |
| | | <text class="section-count">共 {{ formConfig.fields.length }} 项</text> |
| | | </view> |
| | | <view class="head-actions"> |
| | | <text class="head-link" |
| | | @click="showPresetSheet = true">预设</text> |
| | | <text class="head-link head-link--import" |
| | | :class="{ 'head-link--disabled': !canImportTemplate }" |
| | | @click="openTemplateImport">从已有模板导入</text> |
| | | <text class="head-link head-link--primary" |
| | | @click="openFieldEditor()">添加</text> |
| | | @click="openFieldEditor()">+ 添加填报项</text> |
| | | </view> |
| | | </view> |
| | | <view class="section-body"> |
| | |
| | | class="field-list"> |
| | | <view v-for="(field, index) in formConfig.fields" |
| | | :key="field.key" |
| | | class="field-item"> |
| | | class="field-item" |
| | | :class="{ 'field-item--locked': isFieldLocked(field) }" |
| | | @click="onFieldItemClick(field, index)"> |
| | | <view class="field-order">{{ index + 1 }}</view> |
| | | <view class="field-main"> |
| | | <view class="field-title-row"> |
| | | <text class="field-name">{{ field.label }}</text> |
| | | <text class="type-tag" |
| | | :class="fieldTypeTagClass(field.type)"> |
| | | {{ fieldTypeLabel(field.type) }} |
| | | </text> |
| | | <text v-if="field.required" |
| | | class="req-tag">必填</text> |
| | | <view class="field-tags"> |
| | | <text class="type-tag" |
| | | :class="fieldTypeTagClass(field.type)"> |
| | | {{ fieldTypeLabel(field.type) }} |
| | | </text> |
| | | <text v-if="field.required" |
| | | class="req-tag">必填</text> |
| | | </view> |
| | | </view> |
| | | <text class="field-key">{{ field.key }}</text> |
| | | <text v-if="field.defaultValue" |
| | | class="field-default">默认:{{ field.defaultValue }}</text> |
| | | class="field-default"> |
| | | 默认:{{ formatFieldDefaultPreview(field) }} |
| | | </text> |
| | | </view> |
| | | <view class="field-actions"> |
| | | <view v-if="!isFieldLocked(field)" |
| | | class="field-actions" |
| | | @click.stop> |
| | | <view class="icon-btn icon-btn--edit" |
| | | @click="openFieldEditor(field, index)"> |
| | | @click.stop="openFieldEditor(field, index)"> |
| | | <up-icon name="edit-pen" |
| | | size="16" |
| | | color="#2979ff" /> |
| | | </view> |
| | | <view class="icon-btn icon-btn--del" |
| | | @click="removeField(index)"> |
| | | @click.stop="removeField(index)"> |
| | | <up-icon name="trash" |
| | | size="16" |
| | | color="#f56c6c" /> |
| | | </view> |
| | | </view> |
| | | <view v-else |
| | | class="field-lock-tag">内置</view> |
| | | </view> |
| | | </view> |
| | | <view v-else |
| | |
| | | @cancel="goBack" |
| | | @confirm="handleSubmit" /> |
| | | |
| | | <up-action-sheet :show="showPresetSheet" |
| | | title="从预设导入" |
| | | :actions="presetActions" |
| | | @select="onSelectPreset" |
| | | @close="showPresetSheet = false" /> |
| | | <up-action-sheet :show="showTemplateImportSheet" |
| | | title="从已有模板导入" |
| | | :actions="templateImportActions" |
| | | @select="onSelectImportTemplate" |
| | | @close="showTemplateImportSheet = false" /> |
| | | |
| | | <up-popup :show="showFieldEditor" |
| | | mode="bottom" |
| | |
| | | @close="closeFieldEditor"> |
| | | <view class="field-editor"> |
| | | <view class="sheet-handle" /> |
| | | <text class="editor-title">{{ editingFieldIndex >= 0 ? "编辑填报项" : "添加填报项" }}</text> |
| | | <view class="editor-form"> |
| | | <view class="editor-row"> |
| | | <text class="editor-label required">字段名称</text> |
| | | <up-input v-model="fieldDraft.label" |
| | | placeholder="请输入" |
| | | clearable /> |
| | | </view> |
| | | <view class="editor-row editor-row--block"> |
| | | <text class="editor-label required">字段类型</text> |
| | | <view class="type-chip-grid"> |
| | | <view v-for="opt in FIELD_TYPE_OPTIONS" |
| | | :key="opt.value" |
| | | class="type-chip" |
| | | :class="{ active: fieldDraft.type === opt.value }" |
| | | @click="selectFieldType(opt.value)"> |
| | | {{ opt.name }} |
| | | <view class="editor-header"> |
| | | <text class="editor-title">{{ editingFieldIndex >= 0 ? "编辑填报项" : "添加填报项" }}</text> |
| | | <text class="editor-subtitle">配置字段属性、校验与默认值</text> |
| | | </view> |
| | | <scroll-view class="editor-scroll" |
| | | scroll-y |
| | | :show-scrollbar="false"> |
| | | <view class="editor-form"> |
| | | <view class="editor-section-card"> |
| | | <view class="editor-section-head"> |
| | | <text class="editor-section-title">基础信息</text> |
| | | </view> |
| | | <view class="editor-cell"> |
| | | <text class="editor-label required">显示名称</text> |
| | | <view class="editor-input-box"> |
| | | <up-input v-model="fieldDraft.label" |
| | | placeholder="如:报销说明" |
| | | border="none" |
| | | clearable /> |
| | | </view> |
| | | </view> |
| | | <view class="editor-cell"> |
| | | <text class="editor-label required">字段标识</text> |
| | | <view class="editor-input-box"> |
| | | <up-input v-model="fieldDraft.key" |
| | | placeholder="如:summary" |
| | | border="none" |
| | | clearable /> |
| | | </view> |
| | | </view> |
| | | <view class="editor-cell editor-cell--tap" |
| | | @click="openFieldTypePicker"> |
| | | <text class="editor-label required">控件类型</text> |
| | | <view class="picker-value-row"> |
| | | <text class="picker-value" |
| | | :class="{ 'picker-value--placeholder': !fieldDraft.type }"> |
| | | {{ fieldDraftTypeText || "请选择" }} |
| | | </text> |
| | | <up-icon name="arrow-right" |
| | | size="14" |
| | | color="#b0b8c4" /> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <view class="editor-section-card"> |
| | | <view class="editor-section-head"> |
| | | <text class="editor-section-title">校验与格式</text> |
| | | </view> |
| | | <view class="editor-cell editor-cell--switch"> |
| | | <view class="switch-label-wrap"> |
| | | <text class="editor-label">是否必填</text> |
| | | <text class="switch-hint">提交审批时须填写该项</text> |
| | | </view> |
| | | <up-switch v-model="fieldDraft.required" |
| | | active-color="#2979ff" /> |
| | | </view> |
| | | </view> |
| | | |
| | | <view v-if="isSelectDraft" |
| | | class="editor-section-card"> |
| | | <view class="editor-section-head"> |
| | | <text class="editor-section-title">下拉选项</text> |
| | | </view> |
| | | <view class="editor-cell editor-cell--tap" |
| | | @click="openOptionSourcePicker"> |
| | | <text class="editor-label">选项来源</text> |
| | | <view class="picker-value-row"> |
| | | <text class="picker-value">{{ fieldDraftOptionSourceText }}</text> |
| | | <up-icon name="arrow-right" |
| | | size="14" |
| | | color="#b0b8c4" /> |
| | | </view> |
| | | </view> |
| | | <view v-if="fieldDraft.optionSource === 'manual'" |
| | | class="manual-options"> |
| | | <text class="manual-options-title">手动选项</text> |
| | | <view class="manual-options-table"> |
| | | <view class="option-table-head"> |
| | | <text class="option-col option-col--idx" /> |
| | | <text class="option-col option-col--label">显示文本</text> |
| | | <text class="option-col option-col--value">选项值</text> |
| | | <text class="option-col option-col--action" /> |
| | | </view> |
| | | <view v-for="(opt, optIndex) in fieldDraft.options" |
| | | :key="optIndex" |
| | | class="option-card"> |
| | | <text class="option-idx">{{ optIndex + 1 }}</text> |
| | | <view class="option-input-wrap"> |
| | | <up-input v-model="opt.label" |
| | | placeholder="如:工作日加班" |
| | | border="none" |
| | | clearable /> |
| | | </view> |
| | | <view class="option-input-wrap option-input-wrap--value"> |
| | | <up-input v-model="opt.value" |
| | | placeholder="如:0" |
| | | border="none" |
| | | clearable /> |
| | | </view> |
| | | <view class="option-del" |
| | | hover-class="option-del--active" |
| | | @click.stop="removeDraftOption(optIndex)"> |
| | | <up-icon name="trash" |
| | | size="16" |
| | | color="#f56c6c" /> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <view class="add-option-btn" |
| | | hover-class="add-option-btn--active" |
| | | @click="addDraftOption"> |
| | | <up-icon name="plus-circle" |
| | | size="16" |
| | | color="#2979ff" /> |
| | | <text>添加选项</text> |
| | | </view> |
| | | </view> |
| | | <view v-else |
| | | class="option-source-tip"> |
| | | <up-icon name="info-circle" |
| | | size="14" |
| | | color="#909399" /> |
| | | <text>发起审批时将自动加载{{ fieldDraftOptionSourceText }}</text> |
| | | </view> |
| | | </view> |
| | | |
| | | <view class="editor-section-card"> |
| | | <view class="editor-section-head"> |
| | | <text class="editor-section-title">默认值</text> |
| | | </view> |
| | | <text class="default-hint"> |
| | | 选择该模板提交审批时自动预填,用户仍可修改 |
| | | </text> |
| | | <view class="editor-cell editor-cell--value"> |
| | | <up-textarea v-if="fieldDraft.type === 'textarea'" |
| | | v-model="fieldDraft.defaultValue" |
| | | placeholder="选填" |
| | | maxlength="500" |
| | | border="surround" |
| | | height="72" /> |
| | | <view v-else-if="fieldDraft.type === 'date'" |
| | | class="picker-value-row picker-value-row--tap" |
| | | @click="openDefaultDatePicker"> |
| | | <text class="picker-value" |
| | | :class="{ 'picker-value--placeholder': !fieldDraft.defaultValue }"> |
| | | {{ fieldDraft.defaultValue || "选择日期" }} |
| | | </text> |
| | | <up-icon name="calendar" |
| | | size="18" |
| | | color="#909399" /> |
| | | </view> |
| | | <view v-else-if="isDatetimerangeDraft" |
| | | class="daterange-default-wrap"> |
| | | <view class="daterange-default-item" |
| | | @click="openDefaultRangePicker('start')"> |
| | | <text class="daterange-default-label">开始时间</text> |
| | | <view class="picker-value-row picker-value-row--tap"> |
| | | <text class="picker-value" |
| | | :class="{ 'picker-value--placeholder': !defaultRangeStart }"> |
| | | {{ defaultRangeStart || "选择开始时间" }} |
| | | </text> |
| | | <up-icon name="calendar" |
| | | size="18" |
| | | color="#909399" /> |
| | | </view> |
| | | </view> |
| | | <view class="daterange-default-item" |
| | | @click="openDefaultRangePicker('end')"> |
| | | <text class="daterange-default-label">结束时间</text> |
| | | <view class="picker-value-row picker-value-row--tap"> |
| | | <text class="picker-value" |
| | | :class="{ 'picker-value--placeholder': !defaultRangeEnd }"> |
| | | {{ defaultRangeEnd || "选择结束时间" }} |
| | | </text> |
| | | <up-icon name="calendar" |
| | | size="18" |
| | | color="#909399" /> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <view v-else-if="isSelectDraft" |
| | | class="picker-value-row picker-value-row--tap" |
| | | @click="openDefaultSelectSheet"> |
| | | <text class="picker-value" |
| | | :class="{ 'picker-value--placeholder': !fieldDraft.defaultValue }"> |
| | | {{ defaultSelectDisplayText || "选填" }} |
| | | </text> |
| | | <up-icon name="arrow-right" |
| | | size="14" |
| | | color="#b0b8c4" /> |
| | | </view> |
| | | <view v-else |
| | | class="editor-input-box"> |
| | | <up-input v-model="fieldDraft.defaultValue" |
| | | :type="fieldDraft.type === 'number' ? 'digit' : 'text'" |
| | | placeholder="选填" |
| | | border="none" |
| | | clearable /> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <view class="editor-row editor-row--block"> |
| | | <text class="editor-label">默认值</text> |
| | | <up-textarea v-if="fieldDraft.type === 'textarea'" |
| | | v-model="fieldDraft.defaultValue" |
| | | placeholder="选填" |
| | | maxlength="500" |
| | | height="72" /> |
| | | <view v-else-if="fieldDraft.type === 'date'" |
| | | class="default-date-row" |
| | | @click="showDefaultDatePicker = true"> |
| | | <up-input :model-value="fieldDraft.defaultValue" |
| | | placeholder="选择日期" |
| | | readonly /> |
| | | <up-icon name="calendar" |
| | | size="18" |
| | | color="#909399" /> |
| | | </view> |
| | | <up-input v-else |
| | | v-model="fieldDraft.defaultValue" |
| | | :type="fieldDraft.type === 'number' ? 'digit' : 'text'" |
| | | placeholder="选填" |
| | | clearable /> |
| | | </view> |
| | | <view class="editor-row editor-row--switch"> |
| | | <text class="editor-label">是否必填</text> |
| | | <up-switch v-model="fieldDraft.required" /> |
| | | </view> |
| | | </view> |
| | | </scroll-view> |
| | | <view class="editor-footer"> |
| | | <view class="editor-btn editor-btn--cancel" |
| | | @click="closeFieldEditor">取消</view> |
| | | <view class="editor-btn editor-btn--confirm" |
| | | @click="confirmFieldEditor">确定</view> |
| | | </view> |
| | | |
| | | <view v-if="inlinePickerShow" |
| | | class="editor-picker-layer"> |
| | | <view class="editor-picker-mask" |
| | | @click="closeInlinePicker" /> |
| | | <view class="editor-picker-panel"> |
| | | <view class="editor-picker-head"> |
| | | <text class="editor-picker-cancel" |
| | | @click="closeInlinePicker">取消</text> |
| | | <text class="editor-picker-title">{{ inlinePickerTitle }}</text> |
| | | <text class="editor-picker-placeholder" /> |
| | | </view> |
| | | <scroll-view class="editor-picker-scroll" |
| | | scroll-y> |
| | | <view v-for="(item, pickerIndex) in inlinePickerOptions" |
| | | :key="`${inlinePickerMode}-${pickerIndex}-${item.value}`" |
| | | class="editor-picker-item" |
| | | :class="{ 'editor-picker-item--active': isInlinePickerItemActive(item) }" |
| | | @click="onInlinePickerSelect(item)"> |
| | | <text>{{ item.name }}</text> |
| | | <up-icon v-if="isInlinePickerItemActive(item)" |
| | | name="checkmark" |
| | | size="18" |
| | | color="#2979ff" /> |
| | | </view> |
| | | </scroll-view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </up-popup> |
| | | |
| | | <up-popup :show="showDefaultDatePicker" |
| | | mode="bottom" |
| | | @close="showDefaultDatePicker = false"> |
| | | @close="closeDefaultDatePicker"> |
| | | <up-datetime-picker :show="true" |
| | | v-model="defaultDateTs" |
| | | mode="date" |
| | | @confirm="onDefaultDateConfirm" |
| | | @cancel="showDefaultDatePicker = false" /> |
| | | :mode="defaultDatePickerMode" |
| | | @confirm="onDefaultDatePickerConfirm" |
| | | @cancel="closeDefaultDatePicker" /> |
| | | </up-popup> |
| | | |
| | | <up-popup :show="showUserPicker" |
| | |
| | | import FooterButtons from "@/components/FooterButtons.vue"; |
| | | import { |
| | | addApprovalTemplate, |
| | | getApprovalTemplateDetail, |
| | | listApprovalTemplatePage, |
| | | updateApprovalTemplate, |
| | | } from "@/api/oa/approvalTemplate.js"; |
| | | import { getDept } from "@/api/collaborativeApproval/approvalProcess.js"; |
| | | import { userListNoPageByTenantId } from "@/api/system/user"; |
| | | import { formatDateToYMD } from "@/utils/ruoyi"; |
| | | import { fetchApprovalTemplateTypes } from "../../_utils/approvalTemplateType.js"; |
| | | import { |
| | | buildFieldConfigPayload, |
| | | createEmptyFieldOption, |
| | | parseApprovalFormConfig, |
| | | FIELD_EDITOR_TYPE_OPTIONS, |
| | | FIELD_OPTION_SOURCE_OPTIONS, |
| | | getFieldEditorTypeLabel, |
| | | getFieldOptionLabel, |
| | | getFieldOptionSource, |
| | | getFieldOptionSourceLabel, |
| | | isDatetimerangeField, |
| | | isSelectField, |
| | | formatDatetimerangeDisplay, |
| | | formatFieldDateValue, |
| | | joinDatetimerangeValue, |
| | | parseDatetimerangeValue, |
| | | parseFieldDateToTs, |
| | | resolveFieldOptions, |
| | | } from "../../_utils/approvalFormField.js"; |
| | | import { |
| | | fetchApprovalTemplateTypes, |
| | | isSystemApprovalTemplate, |
| | | } from "../../_utils/approvalTemplateType.js"; |
| | | |
| | | const EDIT_STORAGE_KEY = "oa_approve_template_edit_row"; |
| | | |
| | | const LEVEL_TEXT = ["", "一", "二", "三", "四", "五", "六", "七", "八", "九", "十"]; |
| | | |
| | | const FORM_PRESETS = [ |
| | | { |
| | | name: "通用报销", |
| | | prompt: "请填写报销事由、金额等", |
| | | fields: [ |
| | | { key: "reason", label: "报销事由", type: "textarea", required: true }, |
| | | { key: "amount", label: "报销金额(元)", type: "number", required: true }, |
| | | { key: "applyDate", label: "申请日期", type: "date", required: true }, |
| | | ], |
| | | }, |
| | | { |
| | | name: "请假申请", |
| | | prompt: "请填写请假类型、起止时间等", |
| | | fields: [ |
| | | { key: "leaveType", label: "请假类型", type: "text", required: true }, |
| | | { key: "startTime", label: "开始时间", type: "date", required: true }, |
| | | { key: "endTime", label: "结束时间", type: "date", required: true }, |
| | | { key: "reason", label: "请假事由", type: "textarea", required: true }, |
| | | ], |
| | | }, |
| | | { |
| | | name: "采购申请", |
| | | prompt: "请填写采购事由、预估金额等", |
| | | fields: [ |
| | | { key: "title", label: "采购事由", type: "textarea", required: true }, |
| | | { key: "amount", label: "预估金额(元)", type: "number", required: true }, |
| | | ], |
| | | }, |
| | | ]; |
| | | |
| | | const FIELD_TYPE_OPTIONS = [ |
| | | { name: "单行文本", value: "text" }, |
| | | { name: "多行文本", value: "textarea" }, |
| | | { name: "数字", value: "number" }, |
| | | { name: "日期", value: "date" }, |
| | | ]; |
| | | |
| | | const formRef = ref(); |
| | | const submitting = ref(false); |
| | | const userList = ref([]); |
| | | const templateId = ref(null); |
| | | |
| | | const showPresetSheet = ref(false); |
| | | const showTemplateImportSheet = ref(false); |
| | | const importTemplateList = ref([]); |
| | | const showFieldEditor = ref(false); |
| | | const inlinePickerShow = ref(false); |
| | | const inlinePickerTitle = ref(""); |
| | | const inlinePickerOptions = ref([]); |
| | | const inlinePickerMode = ref(""); |
| | | const showUserPicker = ref(false); |
| | | const showDefaultDatePicker = ref(false); |
| | | const defaultDatePickerMode = ref("date"); |
| | | const defaultRangePickerPart = ref("start"); |
| | | const defaultDateTs = ref(Date.now()); |
| | | const deptList = ref([]); |
| | | |
| | | const editingFieldIndex = ref(-1); |
| | | const editingNodeIndex = ref(-1); |
| | | const pickerSelectedIds = ref([]); |
| | | /** 系统模板加载时锁定的填报项 key,不可编辑/删除 */ |
| | | const lockedFieldKeys = ref(new Set()); |
| | | |
| | | const form = reactive({ |
| | | templateName: "", |
| | |
| | | |
| | | const fieldDraft = reactive({ |
| | | label: "", |
| | | key: "", |
| | | type: "text", |
| | | defaultValue: "", |
| | | required: true, |
| | | optionSource: "manual", |
| | | options: [createEmptyFieldOption()], |
| | | }); |
| | | |
| | | let nodeKeySeed = 1; |
| | |
| | | return matched?.name || ""; |
| | | }); |
| | | |
| | | const presetActions = FORM_PRESETS.map(item => ({ |
| | | name: item.name, |
| | | value: item.name, |
| | | })); |
| | | const canImportTemplate = computed(() => !isSystemTemplate.value); |
| | | |
| | | const templateImportActions = computed(() => |
| | | importTemplateList.value.map(item => { |
| | | const typeTag = isSystemApprovalTemplate(item) ? "系统" : "自定义"; |
| | | return { |
| | | name: `【${typeTag}】${item.templateName || `模板${item.id}`}`, |
| | | value: String(item.id), |
| | | }; |
| | | }) |
| | | ); |
| | | |
| | | const isSelectDraft = computed(() => isSelectField(fieldDraft)); |
| | | |
| | | const isDatetimerangeDraft = computed(() => isDatetimerangeField(fieldDraft)); |
| | | |
| | | const defaultRangeParts = computed(() => |
| | | parseDatetimerangeValue(fieldDraft.defaultValue) |
| | | ); |
| | | |
| | | const defaultRangeStart = computed(() => defaultRangeParts.value.start); |
| | | |
| | | const defaultRangeEnd = computed(() => defaultRangeParts.value.end); |
| | | |
| | | const fieldDraftTypeText = computed(() => getFieldEditorTypeLabel(fieldDraft.type)); |
| | | |
| | | const fieldDraftOptionSourceText = computed(() => |
| | | getFieldOptionSourceLabel(fieldDraft.optionSource) |
| | | ); |
| | | |
| | | const defaultSelectActions = computed(() => { |
| | | const options = resolveFieldOptions(fieldDraft, { |
| | | users: userList.value, |
| | | depts: deptList.value, |
| | | }); |
| | | return [ |
| | | { name: "不设置", value: "" }, |
| | | ...options.map(opt => ({ |
| | | name: opt.label, |
| | | value: opt.value, |
| | | })), |
| | | ]; |
| | | }); |
| | | |
| | | const defaultSelectDisplayText = computed(() => { |
| | | if (!fieldDraft.defaultValue) return ""; |
| | | return ( |
| | | getFieldOptionLabel(fieldDraft, fieldDraft.defaultValue) || |
| | | String(fieldDraft.defaultValue) |
| | | ); |
| | | }); |
| | | |
| | | const enabledBool = computed({ |
| | | get: () => form.enabled === "1", |
| | |
| | | |
| | | const isEditMode = computed(() => templateId.value != null && templateId.value !== ""); |
| | | |
| | | const isSystemTemplate = computed(() => isSystemApprovalTemplate(form)); |
| | | |
| | | const isFieldLocked = field => |
| | | isSystemTemplate.value && lockedFieldKeys.value.has(field?.key); |
| | | |
| | | const pageTitle = computed(() => |
| | | isEditMode.value ? "编辑审批模板" : "新建审批模板" |
| | | ); |
| | | |
| | | 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.map(f => ({ ...f })) : [], |
| | | }; |
| | | } catch { |
| | | return { prompt: "", fields: [] }; |
| | | } |
| | | }; |
| | | |
| | | const mapNodesFromRow = nodes => { |
| | | if (!Array.isArray(nodes) || !nodes.length) { |
| | |
| | | form.enabled = String(row.enabled ?? "1"); |
| | | form.description = row.description || ""; |
| | | |
| | | const config = parseFormConfig(row.formConfig); |
| | | const config = parseApprovalFormConfig(row.formConfig); |
| | | formConfig.prompt = config.prompt; |
| | | formConfig.fields = config.fields; |
| | | lockedFieldKeys.value = isSystemApprovalTemplate(row) |
| | | ? new Set(config.fields.map(f => f.key).filter(Boolean)) |
| | | : new Set(); |
| | | flowNodes.value = mapNodesFromRow(row.nodes); |
| | | }; |
| | | |
| | |
| | | |
| | | const levelLabel = n => LEVEL_TEXT[n] || String(n); |
| | | |
| | | const fieldTypeLabel = type => |
| | | FIELD_TYPE_OPTIONS.find(item => item.value === type)?.name || type; |
| | | const fieldTypeLabel = type => getFieldEditorTypeLabel(type); |
| | | |
| | | const formatFieldDefaultPreview = field => { |
| | | if (isDatetimerangeField(field)) { |
| | | return formatDatetimerangeDisplay(field.defaultValue) || field.defaultValue; |
| | | } |
| | | return field.defaultValue; |
| | | }; |
| | | |
| | | const fieldTypeTagClass = type => { |
| | | const map = { |
| | |
| | | textarea: "type-tag--area", |
| | | number: "type-tag--num", |
| | | date: "type-tag--date", |
| | | datetimerange: "type-tag--date", |
| | | select: "type-tag--select", |
| | | }; |
| | | return map[type] || "type-tag--text"; |
| | | }; |
| | |
| | | }; |
| | | |
| | | const openBusinessTypeSheet = () => { |
| | | if (isSystemTemplate.value) return; |
| | | if (!businessTypeOptions.value.length) { |
| | | uni.showToast({ title: "审批类型加载中", icon: "none" }); |
| | | return; |
| | |
| | | formRef.value?.validateField?.("businessType"); |
| | | }; |
| | | |
| | | const onSelectPreset = action => { |
| | | const preset = FORM_PRESETS.find(item => item.name === action.value); |
| | | if (!preset) return; |
| | | formConfig.prompt = preset.prompt; |
| | | formConfig.fields = preset.fields.map(field => ({ ...field })); |
| | | showPresetSheet.value = false; |
| | | uni.showToast({ title: "已导入预设", icon: "success" }); |
| | | const applyImportedFormConfig = (config, sourceName = "") => { |
| | | const parsed = { |
| | | prompt: config?.prompt || "", |
| | | fields: (config?.fields || []).map(field => ({ ...field })), |
| | | }; |
| | | formConfig.prompt = parsed.prompt; |
| | | formConfig.fields = parsed.fields; |
| | | const tip = sourceName ? `已导入「${sourceName}」` : "已导入填报配置"; |
| | | uni.showToast({ title: tip, icon: "success" }); |
| | | }; |
| | | |
| | | const selectFieldType = type => { |
| | | if (fieldDraft.type === type) return; |
| | | fieldDraft.type = type; |
| | | const doImportFormConfig = (config, sourceName) => { |
| | | const hasExisting = |
| | | !!formConfig.prompt?.trim() || formConfig.fields.length > 0; |
| | | if (!hasExisting) { |
| | | applyImportedFormConfig(config, sourceName); |
| | | return; |
| | | } |
| | | uni.showModal({ |
| | | title: "导入确认", |
| | | content: `将使用「${sourceName}」的填报配置覆盖当前内容,是否继续?`, |
| | | success: res => { |
| | | if (res.confirm) { |
| | | applyImportedFormConfig(config, sourceName); |
| | | } |
| | | }, |
| | | }); |
| | | }; |
| | | |
| | | const applyTemplateImport = templateIdValue => { |
| | | const row = importTemplateList.value.find( |
| | | item => String(item.id) === String(templateIdValue) |
| | | ); |
| | | const sourceName = row?.templateName || "所选模板"; |
| | | const applyFromDetail = detail => { |
| | | const config = parseApprovalFormConfig(detail?.formConfig); |
| | | if (!config.fields.length && !config.prompt) { |
| | | uni.showToast({ title: "该模板无填报配置", icon: "none" }); |
| | | return; |
| | | } |
| | | doImportFormConfig(config, sourceName); |
| | | }; |
| | | |
| | | if (row?.formConfig) { |
| | | applyFromDetail(row); |
| | | return; |
| | | } |
| | | |
| | | uni.showLoading({ title: "加载配置...", mask: true }); |
| | | getApprovalTemplateDetail(templateIdValue) |
| | | .then(res => applyFromDetail(res?.data)) |
| | | .catch(() => { |
| | | uni.showToast({ title: "获取模板配置失败", icon: "none" }); |
| | | }) |
| | | .finally(() => { |
| | | uni.hideLoading(); |
| | | }); |
| | | }; |
| | | |
| | | const openTemplateImport = () => { |
| | | if (!canImportTemplate.value) { |
| | | uni.showToast({ title: "系统内置模板不可导入", icon: "none" }); |
| | | return; |
| | | } |
| | | uni.showLoading({ title: "加载中...", mask: true }); |
| | | listApprovalTemplatePage({ |
| | | page: { current: 1, size: 200 }, |
| | | approvalTemplateDto: {}, |
| | | }) |
| | | .then(res => { |
| | | const records = res?.data?.records || []; |
| | | importTemplateList.value = records.filter( |
| | | item => |
| | | item?.id != null && String(item.id) !== String(templateId.value) |
| | | ); |
| | | if (!importTemplateList.value.length) { |
| | | uni.showToast({ title: "暂无可导入的模板", icon: "none" }); |
| | | return; |
| | | } |
| | | showTemplateImportSheet.value = true; |
| | | }) |
| | | .catch(() => { |
| | | uni.showToast({ title: "加载模板列表失败", icon: "none" }); |
| | | }) |
| | | .finally(() => { |
| | | uni.hideLoading(); |
| | | }); |
| | | }; |
| | | |
| | | const onSelectImportTemplate = action => { |
| | | showTemplateImportSheet.value = false; |
| | | const value = String(action?.value ?? ""); |
| | | if (!value) return; |
| | | applyTemplateImport(value); |
| | | }; |
| | | |
| | | const resetFieldDraft = () => { |
| | | fieldDraft.label = ""; |
| | | fieldDraft.key = ""; |
| | | fieldDraft.type = "text"; |
| | | fieldDraft.defaultValue = ""; |
| | | fieldDraft.required = true; |
| | | fieldDraft.optionSource = "manual"; |
| | | fieldDraft.options = [createEmptyFieldOption()]; |
| | | }; |
| | | |
| | | const onDefaultDateConfirm = e => { |
| | | fieldDraft.defaultValue = formatDateToYMD(e.value); |
| | | const resolveActionValue = (action, options) => { |
| | | if (action?.value !== undefined && action?.value !== null) { |
| | | return action.value; |
| | | } |
| | | const name = action?.name; |
| | | if (name == null) return undefined; |
| | | return options.find(opt => opt.name === name)?.value; |
| | | }; |
| | | |
| | | const onSelectFieldType = action => { |
| | | const nextType = resolveActionValue(action, FIELD_EDITOR_TYPE_OPTIONS); |
| | | if (!nextType || fieldDraft.type === nextType) return; |
| | | fieldDraft.type = nextType; |
| | | fieldDraft.defaultValue = ""; |
| | | if (!isSelectField(fieldDraft)) { |
| | | fieldDraft.optionSource = "manual"; |
| | | fieldDraft.options = [createEmptyFieldOption()]; |
| | | } else if (!fieldDraft.options?.length) { |
| | | fieldDraft.options = [createEmptyFieldOption()]; |
| | | } |
| | | }; |
| | | |
| | | const openInlinePicker = (title, options, mode) => { |
| | | inlinePickerTitle.value = title; |
| | | inlinePickerOptions.value = options; |
| | | inlinePickerMode.value = mode; |
| | | inlinePickerShow.value = true; |
| | | }; |
| | | |
| | | const closeInlinePicker = () => { |
| | | inlinePickerShow.value = false; |
| | | inlinePickerMode.value = ""; |
| | | inlinePickerOptions.value = []; |
| | | }; |
| | | |
| | | const isInlinePickerItemActive = item => { |
| | | if (inlinePickerMode.value === "fieldType") { |
| | | return String(fieldDraft.type) === String(item.value); |
| | | } |
| | | if (inlinePickerMode.value === "optionSource") { |
| | | return String(fieldDraft.optionSource) === String(item.value); |
| | | } |
| | | if (inlinePickerMode.value === "defaultValue") { |
| | | const val = fieldDraft.defaultValue; |
| | | if (val === "" || val === undefined || val === null) { |
| | | return item.value === "" || item.value === undefined || item.value === null; |
| | | } |
| | | return String(val) === String(item.value); |
| | | } |
| | | return false; |
| | | }; |
| | | |
| | | const onInlinePickerSelect = item => { |
| | | if (inlinePickerMode.value === "fieldType") { |
| | | onSelectFieldType(item); |
| | | } else if (inlinePickerMode.value === "optionSource") { |
| | | onSelectOptionSource(item); |
| | | } else if (inlinePickerMode.value === "defaultValue") { |
| | | onSelectDefaultOption(item); |
| | | } |
| | | closeInlinePicker(); |
| | | }; |
| | | |
| | | const openFieldTypePicker = () => { |
| | | openInlinePicker( |
| | | "控件类型", |
| | | FIELD_EDITOR_TYPE_OPTIONS.map(item => ({ |
| | | name: item.name, |
| | | value: item.value, |
| | | })), |
| | | "fieldType" |
| | | ); |
| | | }; |
| | | |
| | | const onSelectOptionSource = action => { |
| | | const nextSource = resolveActionValue(action, FIELD_OPTION_SOURCE_OPTIONS); |
| | | if (!nextSource) return; |
| | | fieldDraft.optionSource = nextSource; |
| | | fieldDraft.defaultValue = ""; |
| | | if (nextSource === "manual" && !fieldDraft.options?.length) { |
| | | fieldDraft.options = [createEmptyFieldOption()]; |
| | | } |
| | | }; |
| | | |
| | | const openOptionSourcePicker = () => { |
| | | openInlinePicker( |
| | | "选项来源", |
| | | FIELD_OPTION_SOURCE_OPTIONS.map(item => ({ |
| | | name: item.name, |
| | | value: item.value, |
| | | })), |
| | | "optionSource" |
| | | ); |
| | | }; |
| | | |
| | | const addDraftOption = () => { |
| | | fieldDraft.options.push(createEmptyFieldOption()); |
| | | }; |
| | | |
| | | const removeDraftOption = index => { |
| | | if (fieldDraft.options.length <= 1) { |
| | | fieldDraft.options[0] = createEmptyFieldOption(); |
| | | return; |
| | | } |
| | | fieldDraft.options.splice(index, 1); |
| | | }; |
| | | |
| | | const openDefaultSelectSheet = () => { |
| | | const options = resolveFieldOptions(fieldDraft, { |
| | | users: userList.value, |
| | | depts: deptList.value, |
| | | }); |
| | | if (!options.length) { |
| | | uni.showToast({ title: "请先配置下拉选项", icon: "none" }); |
| | | return; |
| | | } |
| | | openInlinePicker("默认值", defaultSelectActions.value, "defaultValue"); |
| | | }; |
| | | |
| | | const onSelectDefaultOption = action => { |
| | | fieldDraft.defaultValue = |
| | | action.value === undefined || action.value === null |
| | | ? "" |
| | | : String(action.value); |
| | | }; |
| | | |
| | | const closeDefaultDatePicker = () => { |
| | | showDefaultDatePicker.value = false; |
| | | defaultDatePickerMode.value = "date"; |
| | | defaultRangePickerPart.value = "start"; |
| | | }; |
| | | |
| | | const openDefaultDatePicker = () => { |
| | | defaultDatePickerMode.value = "date"; |
| | | const parsed = Date.parse(fieldDraft.defaultValue); |
| | | defaultDateTs.value = Number.isNaN(parsed) ? Date.now() : parsed; |
| | | showDefaultDatePicker.value = true; |
| | | }; |
| | | |
| | | const openDefaultRangePicker = part => { |
| | | defaultDatePickerMode.value = "datetime"; |
| | | defaultRangePickerPart.value = part; |
| | | const parts = parseDatetimerangeValue(fieldDraft.defaultValue); |
| | | const val = part === "start" ? parts.start : parts.end; |
| | | defaultDateTs.value = parseFieldDateToTs(val) ?? Date.now(); |
| | | showDefaultDatePicker.value = true; |
| | | }; |
| | | |
| | | const onDefaultDatePickerConfirm = e => { |
| | | const ts = e?.value ?? defaultDateTs.value; |
| | | if (defaultDatePickerMode.value === "datetime") { |
| | | const parts = parseDatetimerangeValue(fieldDraft.defaultValue); |
| | | const formatted = formatFieldDateValue({ type: "datetime" }, ts); |
| | | fieldDraft.defaultValue = joinDatetimerangeValue( |
| | | defaultRangePickerPart.value === "start" ? formatted : parts.start, |
| | | defaultRangePickerPart.value === "end" ? formatted : parts.end |
| | | ); |
| | | } else { |
| | | fieldDraft.defaultValue = formatDateToYMD(ts); |
| | | } |
| | | closeDefaultDatePicker(); |
| | | }; |
| | | |
| | | const onFieldItemClick = (field, index) => { |
| | | if (isFieldLocked(field)) return; |
| | | openFieldEditor(field, index); |
| | | }; |
| | | |
| | | const openFieldEditor = (field, index = -1) => { |
| | | if (field && isFieldLocked(field)) { |
| | | uni.showToast({ title: "系统内置填报项不可修改", icon: "none" }); |
| | | return; |
| | | } |
| | | editingFieldIndex.value = index; |
| | | if (field) { |
| | | fieldDraft.label = field.label; |
| | | fieldDraft.label = field.label || ""; |
| | | fieldDraft.key = field.key || ""; |
| | | fieldDraft.type = field.type || "text"; |
| | | fieldDraft.defaultValue = field.defaultValue ?? ""; |
| | | fieldDraft.required = !!field.required; |
| | | fieldDraft.optionSource = getFieldOptionSource(field); |
| | | fieldDraft.options = normalizeDraftOptions(field); |
| | | } else { |
| | | fieldDraft.label = ""; |
| | | fieldDraft.type = "text"; |
| | | fieldDraft.defaultValue = ""; |
| | | fieldDraft.required = true; |
| | | resetFieldDraft(); |
| | | } |
| | | if (fieldDraft.type === "date" && fieldDraft.defaultValue) { |
| | | const parsed = Date.parse(fieldDraft.defaultValue); |
| | | defaultDateTs.value = Number.isNaN(parsed) ? Date.now() : parsed; |
| | | } else { |
| | | defaultDateTs.value = Date.now(); |
| | | } |
| | | defaultDateTs.value = Date.now(); |
| | | showFieldEditor.value = true; |
| | | }; |
| | | |
| | | const closeFieldEditor = () => { |
| | | closeInlinePicker(); |
| | | showFieldEditor.value = false; |
| | | editingFieldIndex.value = -1; |
| | | }; |
| | | |
| | | const normalizeDraftOptions = field => { |
| | | const options = field?.options; |
| | | if (!Array.isArray(options) || !options.length) { |
| | | return [createEmptyFieldOption()]; |
| | | } |
| | | return options.map(opt => ({ |
| | | label: opt?.label ?? "", |
| | | value: opt?.value != null ? String(opt.value) : "", |
| | | })); |
| | | }; |
| | | |
| | | const buildFieldKey = label => { |
| | |
| | | }; |
| | | |
| | | const confirmFieldEditor = () => { |
| | | if (!fieldDraft.label?.trim()) { |
| | | uni.showToast({ title: "请输入字段名称", icon: "none" }); |
| | | if ( |
| | | editingFieldIndex.value >= 0 && |
| | | isFieldLocked(formConfig.fields[editingFieldIndex.value]) |
| | | ) { |
| | | uni.showToast({ title: "系统内置填报项不可修改", icon: "none" }); |
| | | return; |
| | | } |
| | | const defaultValue = String(fieldDraft.defaultValue ?? "").trim(); |
| | | if (!fieldDraft.label?.trim()) { |
| | | uni.showToast({ title: "请输入显示名称", icon: "none" }); |
| | | return; |
| | | } |
| | | const existingKey = |
| | | editingFieldIndex.value >= 0 |
| | | ? formConfig.fields[editingFieldIndex.value]?.key |
| | | : null; |
| | | const payload = { |
| | | key: existingKey || buildFieldKey(fieldDraft.label), |
| | | label: fieldDraft.label.trim(), |
| | | type: fieldDraft.type, |
| | | required: !!fieldDraft.required, |
| | | defaultValue, |
| | | }; |
| | | const draftKey = fieldDraft.key?.trim() || existingKey || buildFieldKey(fieldDraft.label); |
| | | if (!draftKey) { |
| | | uni.showToast({ title: "请输入字段标识", icon: "none" }); |
| | | return; |
| | | } |
| | | const duplicateKey = formConfig.fields.some( |
| | | (item, idx) => item.key === draftKey && idx !== editingFieldIndex.value |
| | | ); |
| | | if (duplicateKey) { |
| | | uni.showToast({ title: "字段标识已存在", icon: "none" }); |
| | | return; |
| | | } |
| | | if (isSelectField(fieldDraft) && fieldDraft.optionSource === "manual") { |
| | | const validOptions = (fieldDraft.options || []).filter( |
| | | opt => opt.label?.trim() && opt.value?.trim() |
| | | ); |
| | | if (!validOptions.length) { |
| | | uni.showToast({ title: "请至少配置一个下拉选项", icon: "none" }); |
| | | return; |
| | | } |
| | | } |
| | | const payload = buildFieldConfigPayload( |
| | | { ...fieldDraft, key: draftKey }, |
| | | existingKey |
| | | ); |
| | | if (editingFieldIndex.value >= 0) { |
| | | formConfig.fields.splice(editingFieldIndex.value, 1, payload); |
| | | } else { |
| | |
| | | }; |
| | | |
| | | const removeField = index => { |
| | | const field = formConfig.fields[index]; |
| | | if (isFieldLocked(field)) { |
| | | uni.showToast({ title: "系统内置填报项不可删除", icon: "none" }); |
| | | return; |
| | | } |
| | | formConfig.fields.splice(index, 1); |
| | | }; |
| | | |
| | |
| | | .catch(() => { |
| | | userList.value = []; |
| | | }); |
| | | getDept() |
| | | .then(res => { |
| | | deptList.value = res?.data || []; |
| | | }) |
| | | .catch(() => { |
| | | deptList.value = []; |
| | | }); |
| | | }); |
| | | </script> |
| | | |
| | |
| | | } |
| | | } |
| | | |
| | | .section-head-left { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 4px; |
| | | } |
| | | |
| | | .section-title { |
| | | font-size: 15px; |
| | | font-weight: 600; |
| | |
| | | padding-left: 10px; |
| | | border-left: 3px solid $primary; |
| | | line-height: 1.2; |
| | | } |
| | | |
| | | .section-count { |
| | | font-size: 12px; |
| | | color: $text-muted; |
| | | padding-left: 13px; |
| | | } |
| | | |
| | | .head-actions { |
| | |
| | | font-size: 14px; |
| | | color: $text-secondary; |
| | | |
| | | &--primary { |
| | | color: $primary; |
| | | font-weight: 500; |
| | | &--import { |
| | | color: $text-secondary; |
| | | padding: 6px 12px; |
| | | border: 1px solid #dce3ed; |
| | | border-radius: 8px; |
| | | background: #fff; |
| | | font-size: 13px; |
| | | } |
| | | |
| | | &--disabled { |
| | | color: #c0c4cc; |
| | | border-color: #ebeef5; |
| | | background: #f5f7fa; |
| | | } |
| | | |
| | | &--primary { |
| | | color: #fff; |
| | | font-weight: 500; |
| | | padding: 6px 14px; |
| | | border: none; |
| | | border-radius: 8px; |
| | | background: linear-gradient(135deg, #4d8dff 0%, #2979ff 100%); |
| | | box-shadow: 0 2px 8px rgba(41, 121, 255, 0.25); |
| | | } |
| | | } |
| | | |
| | | :deep(.form-item-select--disabled .u-form-item__body) { |
| | | opacity: 0.65; |
| | | } |
| | | |
| | | .section-body { |
| | |
| | | .field-item { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 10px; |
| | | padding: 10px 12px; |
| | | background: #f8fafc; |
| | | gap: 12px; |
| | | padding: 14px; |
| | | background: #fff; |
| | | border-radius: $radius-md; |
| | | border: 1px solid #eef2f6; |
| | | border: 1px solid #e8eef5; |
| | | box-shadow: 0 1px 4px rgba(31, 45, 61, 0.04); |
| | | transition: border-color 0.2s, box-shadow 0.2s; |
| | | |
| | | &:active:not(.field-item--locked) { |
| | | border-color: #c6daf5; |
| | | box-shadow: 0 2px 8px rgba(41, 121, 255, 0.08); |
| | | } |
| | | |
| | | &--locked { |
| | | background: #fafbfd; |
| | | } |
| | | } |
| | | |
| | | .field-order { |
| | | width: 28px; |
| | | height: 28px; |
| | | border-radius: 8px; |
| | | background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| | | color: #fff; |
| | | font-size: 13px; |
| | | font-weight: 600; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | flex-shrink: 0; |
| | | } |
| | | |
| | | .field-main { |
| | |
| | | .field-title-row { |
| | | display: flex; |
| | | align-items: center; |
| | | flex-wrap: wrap; |
| | | gap: 6px; |
| | | justify-content: space-between; |
| | | gap: 8px; |
| | | margin-bottom: 4px; |
| | | } |
| | | |
| | | .field-name { |
| | | font-size: 15px; |
| | | font-weight: 600; |
| | | color: $text; |
| | | flex: 1; |
| | | min-width: 0; |
| | | } |
| | | |
| | | .field-tags { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 6px; |
| | | flex-shrink: 0; |
| | | } |
| | | |
| | | .field-key { |
| | | display: block; |
| | | font-size: 12px; |
| | | color: $text-muted; |
| | | font-family: ui-monospace, monospace; |
| | | } |
| | | |
| | | .field-lock-tag { |
| | | flex-shrink: 0; |
| | | font-size: 11px; |
| | | color: #909399; |
| | | padding: 4px 8px; |
| | | background: #f0f2f5; |
| | | border-radius: 4px; |
| | | } |
| | | |
| | | .type-tag { |
| | |
| | | &--date { |
| | | color: #18a058; |
| | | background: #e8faf0; |
| | | } |
| | | |
| | | &--select { |
| | | color: #9c27b0; |
| | | background: #f6edfc; |
| | | } |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | .empty-mini { |
| | | padding: 20px 0; |
| | | padding: 32px 16px; |
| | | text-align: center; |
| | | font-size: 13px; |
| | | color: $text-muted; |
| | | background: #fafbfd; |
| | | border: 1px dashed #dce8f5; |
| | | border-radius: 10px; |
| | | } |
| | | |
| | | .flow-wrap { |
| | |
| | | } |
| | | |
| | | .sheet-handle { |
| | | width: 40px; |
| | | width: 36px; |
| | | height: 4px; |
| | | margin: 10px auto 6px; |
| | | background: #e4e7ed; |
| | | margin: 10px auto 4px; |
| | | background: #d8dde6; |
| | | border-radius: 2px; |
| | | } |
| | | |
| | | .field-editor, |
| | | .field-editor .sheet-handle { |
| | | background: #c8ced8; |
| | | } |
| | | |
| | | .field-editor { |
| | | position: relative; |
| | | display: flex; |
| | | flex-direction: column; |
| | | max-height: 88vh; |
| | | background: #f5f7fb; |
| | | border-radius: 16px 16px 0 0; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .user-picker { |
| | | position: relative; |
| | | padding: 0 18px calc(18px + env(safe-area-inset-bottom)); |
| | | background: #fff; |
| | | max-height: 85vh; |
| | | } |
| | | |
| | | .editor-title { |
| | | .editor-header { |
| | | padding: 4px 20px 12px; |
| | | background: #fff; |
| | | text-align: center; |
| | | border-bottom: 1px solid #f0f2f5; |
| | | } |
| | | |
| | | .editor-subtitle { |
| | | display: block; |
| | | margin-top: 4px; |
| | | font-size: 12px; |
| | | color: $text-muted; |
| | | } |
| | | |
| | | .editor-picker-layer { |
| | | position: absolute; |
| | | left: 0; |
| | | right: 0; |
| | | top: 0; |
| | | bottom: 0; |
| | | z-index: 20; |
| | | display: flex; |
| | | flex-direction: column; |
| | | justify-content: flex-end; |
| | | } |
| | | |
| | | .editor-picker-mask { |
| | | position: absolute; |
| | | left: 0; |
| | | right: 0; |
| | | top: 0; |
| | | bottom: 0; |
| | | background: rgba(0, 0, 0, 0.45); |
| | | } |
| | | |
| | | .editor-picker-panel { |
| | | position: relative; |
| | | z-index: 1; |
| | | background: #fff; |
| | | border-radius: 16px 16px 0 0; |
| | | max-height: 55vh; |
| | | padding-bottom: env(safe-area-inset-bottom); |
| | | } |
| | | |
| | | .editor-picker-head { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | padding: 14px 18px; |
| | | border-bottom: 1px solid #f0f0f0; |
| | | } |
| | | |
| | | .editor-picker-cancel { |
| | | font-size: 15px; |
| | | color: #909399; |
| | | min-width: 48px; |
| | | } |
| | | |
| | | .editor-picker-title { |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | color: $text; |
| | | text-align: center; |
| | | margin-bottom: 14px; |
| | | } |
| | | |
| | | .editor-picker-placeholder { |
| | | min-width: 48px; |
| | | } |
| | | |
| | | .editor-picker-scroll { |
| | | max-height: calc(55vh - 52px); |
| | | } |
| | | |
| | | .editor-picker-item { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | padding: 16px 18px; |
| | | font-size: 16px; |
| | | color: $text; |
| | | border-bottom: 1px solid #f5f7fa; |
| | | |
| | | &--active { |
| | | color: $primary; |
| | | background: #f5f9ff; |
| | | } |
| | | |
| | | &:last-child { |
| | | border-bottom: none; |
| | | } |
| | | } |
| | | |
| | | .editor-scroll { |
| | | flex: 1; |
| | | height: 0; |
| | | max-height: 62vh; |
| | | } |
| | | |
| | | .editor-form { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 12px; |
| | | gap: 10px; |
| | | padding: 12px 16px 16px; |
| | | } |
| | | |
| | | .editor-row { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 10px; |
| | | .editor-section-card { |
| | | background: #fff; |
| | | border-radius: 12px; |
| | | padding: 14px 14px 4px; |
| | | box-shadow: 0 1px 6px rgba(31, 45, 61, 0.05); |
| | | } |
| | | |
| | | .editor-section-head { |
| | | margin-bottom: 10px; |
| | | padding-bottom: 10px; |
| | | border-bottom: 1px solid #f2f4f7; |
| | | } |
| | | |
| | | .editor-section-title { |
| | | font-size: 14px; |
| | | font-weight: 600; |
| | | color: $text; |
| | | padding-left: 8px; |
| | | border-left: 3px solid $primary; |
| | | line-height: 1.2; |
| | | } |
| | | |
| | | .editor-cell { |
| | | margin-bottom: 14px; |
| | | |
| | | &--tap:active .picker-value-row { |
| | | background: #eef4ff; |
| | | border-color: #c6daf5; |
| | | } |
| | | |
| | | &--switch { |
| | | flex-direction: row; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | padding: 4px 0; |
| | | gap: 12px; |
| | | padding: 4px 0 10px; |
| | | margin-bottom: 4px; |
| | | } |
| | | |
| | | &--value { |
| | | margin-bottom: 10px; |
| | | } |
| | | } |
| | | |
| | | .switch-label-wrap { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 2px; |
| | | } |
| | | |
| | | .switch-hint { |
| | | font-size: 12px; |
| | | color: $text-muted; |
| | | } |
| | | |
| | | .editor-input-box { |
| | | background: #f7f9fc; |
| | | border: 1px solid #e8ecf2; |
| | | border-radius: 10px; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | :deep(.editor-input-box .u-input) { |
| | | background: transparent !important; |
| | | } |
| | | |
| | | .default-hint { |
| | | display: block; |
| | | font-size: 12px; |
| | | color: $text-muted; |
| | | line-height: 1.5; |
| | | margin: -4px 0 10px; |
| | | padding: 0 2px; |
| | | } |
| | | |
| | | .manual-options { |
| | | margin: 4px 0 12px; |
| | | padding-top: 4px; |
| | | } |
| | | |
| | | .manual-options-title { |
| | | display: block; |
| | | font-size: 12px; |
| | | color: $text-muted; |
| | | margin-bottom: 10px; |
| | | } |
| | | |
| | | .manual-options-table { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 8px; |
| | | } |
| | | |
| | | .option-table-head { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | padding: 0 4px 4px; |
| | | } |
| | | |
| | | .option-col { |
| | | font-size: 12px; |
| | | color: $text-muted; |
| | | font-weight: 500; |
| | | |
| | | &--idx { |
| | | width: 22px; |
| | | flex-shrink: 0; |
| | | } |
| | | |
| | | &--label { |
| | | flex: 1.4; |
| | | min-width: 0; |
| | | } |
| | | |
| | | &--value { |
| | | flex: 0.9; |
| | | min-width: 72px; |
| | | } |
| | | |
| | | &--action { |
| | | width: 32px; |
| | | flex-shrink: 0; |
| | | } |
| | | } |
| | | |
| | | .option-card { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | padding: 8px 10px; |
| | | background: #f8fafc; |
| | | border: 1px solid #e8ecf2; |
| | | border-radius: 10px; |
| | | } |
| | | |
| | | .option-idx { |
| | | width: 22px; |
| | | height: 22px; |
| | | flex-shrink: 0; |
| | | border-radius: 6px; |
| | | background: #eef2f8; |
| | | color: $text-muted; |
| | | font-size: 12px; |
| | | font-weight: 600; |
| | | line-height: 22px; |
| | | text-align: center; |
| | | } |
| | | |
| | | .option-input-wrap { |
| | | flex: 1.4; |
| | | min-width: 0; |
| | | background: #fff; |
| | | border: 1px solid #e4e8ef; |
| | | border-radius: 8px; |
| | | overflow: hidden; |
| | | |
| | | &--value { |
| | | flex: 0.9; |
| | | min-width: 72px; |
| | | } |
| | | } |
| | | |
| | | :deep(.option-input-wrap .u-input) { |
| | | background: transparent !important; |
| | | } |
| | | |
| | | :deep(.option-input-wrap input), |
| | | :deep(.option-input-wrap .u-input__content__field-wrapper__field) { |
| | | font-size: 14px !important; |
| | | height: 36px !important; |
| | | min-height: 36px !important; |
| | | padding: 0 10px !important; |
| | | } |
| | | |
| | | .option-del { |
| | | flex-shrink: 0; |
| | | width: 32px; |
| | | height: 32px; |
| | | border-radius: 8px; |
| | | background: #fff; |
| | | border: 1px solid #fde2e2; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | } |
| | | |
| | | .option-del--active { |
| | | background: #fef0f0; |
| | | } |
| | | |
| | | .add-option-btn { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | gap: 6px; |
| | | margin-top: 10px; |
| | | padding: 11px; |
| | | border: 1.5px dashed #b8d4ff; |
| | | border-radius: 10px; |
| | | background: linear-gradient(180deg, #f8fbff 0%, #f0f6ff 100%); |
| | | color: $primary; |
| | | font-size: 14px; |
| | | font-weight: 500; |
| | | } |
| | | |
| | | .add-option-btn--active { |
| | | background: #e8f2ff; |
| | | border-color: $primary; |
| | | } |
| | | |
| | | .option-source-tip { |
| | | display: flex; |
| | | align-items: flex-start; |
| | | gap: 6px; |
| | | padding: 10px 12px; |
| | | margin-bottom: 10px; |
| | | background: #f5f7fa; |
| | | border-radius: 8px; |
| | | font-size: 12px; |
| | | color: $text-muted; |
| | | line-height: 1.5; |
| | | } |
| | | |
| | | .editor-title { |
| | | display: block; |
| | | font-size: 17px; |
| | | font-weight: 600; |
| | | color: $text; |
| | | } |
| | | |
| | | .picker-value-row { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | min-height: 44px; |
| | | padding: 0 14px; |
| | | background: #f7f9fc; |
| | | border: 1px solid #e8ecf2; |
| | | border-radius: 10px; |
| | | gap: 8px; |
| | | transition: background 0.15s, border-color 0.15s; |
| | | |
| | | &--tap:active { |
| | | background: #eef4ff; |
| | | border-color: #c6daf5; |
| | | } |
| | | } |
| | | |
| | | .picker-value { |
| | | flex: 1; |
| | | min-width: 0; |
| | | font-size: 15px; |
| | | color: $text; |
| | | text-align: left; |
| | | line-height: 1.4; |
| | | |
| | | &--placeholder { |
| | | color: #c0c4cc; |
| | | } |
| | | } |
| | | |
| | | .editor-label { |
| | | font-size: 14px; |
| | | display: block; |
| | | font-size: 13px; |
| | | font-weight: 500; |
| | | color: $text-secondary; |
| | | margin-bottom: 8px; |
| | | |
| | | &.required::before { |
| | | content: "*"; |
| | | color: #f56c6c; |
| | | margin-right: 4px; |
| | | margin-right: 3px; |
| | | } |
| | | } |
| | | |
| | | .editor-row .input-box, |
| | | .editor-row .textarea-box { |
| | | background: #f7f9fc; |
| | | border-radius: 10px; |
| | | border: 1px solid #eef1f6; |
| | | .editor-cell--switch .editor-label { |
| | | margin-bottom: 0; |
| | | } |
| | | |
| | | .daterange-default-wrap { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 12px; |
| | | } |
| | | |
| | | .daterange-default-item { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 8px; |
| | | } |
| | | |
| | | .daterange-default-label { |
| | | font-size: 13px; |
| | | color: $text-secondary; |
| | | } |
| | | |
| | | .type-chip-grid { |
| | |
| | | } |
| | | } |
| | | |
| | | .default-date-row { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | padding: 0 12px; |
| | | min-height: 44px; |
| | | background: #f7f9fc; |
| | | border-radius: 10px; |
| | | border: 1px solid #eef1f6; |
| | | } |
| | | |
| | | .editor-footer { |
| | | display: flex; |
| | | gap: 10px; |
| | | margin-top: 16px; |
| | | padding-top: 14px; |
| | | border-top: 1px solid #f5f7fa; |
| | | gap: 12px; |
| | | padding: 12px 16px calc(12px + env(safe-area-inset-bottom)); |
| | | background: #fff; |
| | | border-top: 1px solid #eef0f4; |
| | | box-shadow: 0 -4px 12px rgba(31, 45, 61, 0.06); |
| | | } |
| | | |
| | | .editor-btn { |
| | | flex: 1; |
| | | text-align: center; |
| | | padding: 11px 0; |
| | | border-radius: 8px; |
| | | padding: 12px 0; |
| | | border-radius: 10px; |
| | | font-size: 15px; |
| | | font-weight: 500; |
| | | |
| | | &--cancel { |
| | | color: $text-secondary; |
| | | background: #f5f7fa; |
| | | border: 1px solid #e4e7ed; |
| | | } |
| | | |
| | | &--confirm { |
| | | color: #fff; |
| | | background: $primary; |
| | | background: linear-gradient(135deg, #4d8dff 0%, #2979ff 100%); |
| | | box-shadow: 0 4px 12px rgba(41, 121, 255, 0.35); |
| | | } |
| | | |
| | | &--confirm:active { |
| | | opacity: 0.9; |
| | | } |
| | | } |
| | | |