From b014cdaf7fcf42cd2b310968f9d47d4420444a6a Mon Sep 17 00:00:00 2001
From: yyb <995253665@qq.com>
Date: 星期四, 21 五月 2026 11:46:38 +0800
Subject: [PATCH] 审批模板增加配置模板导入,按钮权限控制,新建页面ui优化
---
src/pages/oa/ApproveManage/approve-template/edit.vue | 1443 +++++++++++++++++++++++++++++++++++++++++++++++++--------
1 files changed, 1,230 insertions(+), 213 deletions(-)
diff --git a/src/pages/oa/ApproveManage/approve-template/edit.vue b/src/pages/oa/ApproveManage/approve-template/edit.vue
index aba2103..53a2d2e 100644
--- a/src/pages/oa/ApproveManage/approve-template/edit.vue
+++ b/src/pages/oa/ApproveManage/approve-template/edit.vue
@@ -26,17 +26,21 @@
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>
@@ -62,12 +66,16 @@
<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">
@@ -83,34 +91,46 @@
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
@@ -196,11 +216,11 @@
@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"
@@ -208,71 +228,255 @@
@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"
@@ -326,67 +530,65 @@
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: "",
@@ -403,9 +605,12 @@
const fieldDraft = reactive({
label: "",
+ key: "",
type: "text",
defaultValue: "",
required: true,
+ optionSource: "manual",
+ options: [createEmptyFieldOption()],
});
let nodeKeySeed = 1;
@@ -451,10 +656,57 @@
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",
@@ -465,22 +717,14 @@
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) {
@@ -515,9 +759,12 @@
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);
};
@@ -530,8 +777,14 @@
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 = {
@@ -539,6 +792,8 @@
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";
};
@@ -548,6 +803,7 @@
};
const openBusinessTypeSheet = () => {
+ if (isSystemTemplate.value) return;
if (!businessTypeOptions.value.length) {
uni.showToast({ title: "瀹℃壒绫诲瀷鍔犺浇涓�", icon: "none" });
return;
@@ -561,51 +817,316 @@
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 => {
@@ -622,22 +1143,46 @@
};
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 {
@@ -647,6 +1192,11 @@
};
const removeField = index => {
+ const field = formConfig.fields[index];
+ if (isFieldLocked(field)) {
+ uni.showToast({ title: "绯荤粺鍐呯疆濉姤椤逛笉鍙垹闄�", icon: "none" });
+ return;
+ }
formConfig.fields.splice(index, 1);
};
@@ -850,6 +1400,13 @@
.catch(() => {
userList.value = [];
});
+ getDept()
+ .then(res => {
+ deptList.value = res?.data || [];
+ })
+ .catch(() => {
+ deptList.value = [];
+ });
});
</script>
@@ -899,6 +1456,12 @@
}
}
+ .section-head-left {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ }
+
.section-title {
font-size: 15px;
font-weight: 600;
@@ -906,6 +1469,12 @@
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 {
@@ -918,10 +1487,34 @@
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 {
@@ -1141,11 +1734,36 @@
.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 {
@@ -1156,14 +1774,40 @@
.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 {
@@ -1189,6 +1833,11 @@
&--date {
color: #18a058;
background: #e8faf0;
+ }
+
+ &--select {
+ color: #9c27b0;
+ background: #f6edfc;
}
}
@@ -1231,10 +1880,13 @@
}
.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 {
@@ -1410,65 +2062,433 @@
}
.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 {
@@ -1494,40 +2514,37 @@
}
}
- .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;
}
}
--
Gitblit v1.9.3