<!--
|
OA / 审批管理 / 新建审批模板
|
路由:/pages/oa/ApproveManage/approve-template/edit
|
-->
|
<template>
|
<view class="template-edit-page">
|
<PageHeader :title="pageTitle"
|
@back="goBack" />
|
|
<scroll-view class="form-scroll"
|
scroll-y
|
:show-scrollbar="false">
|
<up-form ref="formRef"
|
:model="form"
|
:rules="rules"
|
label-width="88"
|
input-align="right"
|
error-message-align="right">
|
<u-cell-group title="基本信息"
|
class="form-section">
|
<up-form-item label="模板名称"
|
prop="templateName"
|
required
|
class="form-item-name">
|
<up-input v-model="form.templateName"
|
class="name-input-inline"
|
placeholder="请输入模板名称"
|
maxlength="50"
|
: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
|
:disabled="isSystemTemplate" />
|
<template v-if="!isSystemTemplate"
|
#right>
|
<up-icon name="arrow-right"
|
@click.stop="openBusinessTypeSheet" />
|
</template>
|
</up-form-item>
|
<up-form-item label="启用状态"
|
class="form-item-switch">
|
<view class="switch-wrap">
|
<up-switch v-model="enabledBool" />
|
</view>
|
</up-form-item>
|
<up-form-item label="模板说明"
|
class="form-item-desc"
|
label-position="top">
|
<view class="desc-input-shell">
|
<up-textarea v-model="form.description"
|
placeholder="选填"
|
maxlength="200"
|
border="none"
|
height="72" />
|
</view>
|
</up-form-item>
|
</u-cell-group>
|
|
<view class="section-card">
|
<view class="section-head section-head--between">
|
<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 head-link--import"
|
:class="{ 'head-link--disabled': !canImportTemplate }"
|
@click="openTemplateImport">从已有模板导入</text>
|
<text class="head-link head-link--primary"
|
@click="openFieldEditor()">+ 添加填报项</text>
|
</view>
|
</view>
|
<view class="section-body">
|
<view class="prompt-row">
|
<text class="prompt-label">填报提示</text>
|
<up-input v-model="formConfig.prompt"
|
class="prompt-input"
|
placeholder="选填"
|
maxlength="200"
|
clearable />
|
</view>
|
<view v-if="formConfig.fields.length"
|
class="field-list">
|
<view v-for="(field, index) in formConfig.fields"
|
:key="field.key"
|
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>
|
<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">
|
默认:{{ formatFieldDefaultPreview(field) }}
|
</text>
|
</view>
|
<view v-if="!isFieldLocked(field)"
|
class="field-actions"
|
@click.stop>
|
<view class="icon-btn icon-btn--edit"
|
@click.stop="openFieldEditor(field, index)">
|
<up-icon name="edit-pen"
|
size="16"
|
color="#2979ff" />
|
</view>
|
<view class="icon-btn icon-btn--del"
|
@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
|
class="empty-mini">
|
<text>暂无填报项</text>
|
</view>
|
</view>
|
</view>
|
|
<view class="section-card">
|
<view class="section-head">
|
<text class="section-title">审批流程</text>
|
</view>
|
<view class="flow-wrap">
|
<view v-for="(node, nodeIndex) in flowNodes"
|
:key="node._key"
|
class="flow-node-block">
|
<view class="flow-node-card">
|
<view class="node-header">
|
<view class="node-level-badge">{{ nodeIndex + 1 }}</view>
|
<text class="node-level-text">第{{ levelLabel(nodeIndex + 1) }}级</text>
|
<view v-if="flowNodes.length > 1"
|
class="node-delete"
|
@click="removeNode(nodeIndex)">
|
<up-icon name="trash"
|
size="16"
|
color="#f56c6c" />
|
</view>
|
</view>
|
<view class="approve-type-row">
|
<view class="type-btn"
|
:class="{ active: node.approveType === 'AND' }"
|
@click="node.approveType = 'AND'">
|
会签
|
</view>
|
<view class="type-btn"
|
:class="{ active: node.approveType === 'OR' }"
|
@click="node.approveType = 'OR'">
|
或签
|
</view>
|
</view>
|
<view class="approver-list">
|
<view v-for="(approver, approverIndex) in node.approvers"
|
:key="`${node._key}-${approver.approverId}-${approverIndex}`"
|
class="approver-chip">
|
<view class="approver-avatar">{{ (approver.approverName || "?").charAt(0) }}</view>
|
<text class="approver-name">{{ approver.approverName }}</text>
|
<view class="approver-remove"
|
hover-class="approver-remove--active"
|
@tap.stop="removeApprover(nodeIndex, approverIndex)"
|
@click.stop="removeApprover(nodeIndex, approverIndex)">
|
<text class="remove-icon">×</text>
|
</view>
|
</view>
|
<view class="add-approver"
|
@click="openUserPicker(nodeIndex)">
|
<up-icon name="plus"
|
size="14"
|
color="#2979ff" />
|
<text>添加</text>
|
</view>
|
</view>
|
</view>
|
<view v-if="nodeIndex < flowNodes.length - 1"
|
class="flow-connector">
|
<view class="flow-connector-line" />
|
</view>
|
</view>
|
<view class="add-node-bar"
|
@click="addNode">
|
<up-icon name="plus-circle"
|
size="20"
|
color="#2979ff" />
|
<text>添加级次</text>
|
</view>
|
</view>
|
</view>
|
</up-form>
|
</scroll-view>
|
|
<FooterButtons :loading="submitting"
|
confirm-text="保存"
|
@cancel="goBack"
|
@confirm="handleSubmit" />
|
|
<up-action-sheet :show="showTemplateImportSheet"
|
title="从已有模板导入"
|
:actions="templateImportActions"
|
@select="onSelectImportTemplate"
|
@close="showTemplateImportSheet = false" />
|
|
<up-popup :show="showFieldEditor"
|
mode="bottom"
|
round="16"
|
@close="closeFieldEditor">
|
<view class="field-editor">
|
<view class="sheet-handle" />
|
<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>
|
</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="closeDefaultDatePicker">
|
<up-datetime-picker :show="true"
|
v-model="defaultDateTs"
|
:mode="defaultDatePickerMode"
|
@confirm="onDefaultDatePickerConfirm"
|
@cancel="closeDefaultDatePicker" />
|
</up-popup>
|
|
<up-popup :show="showUserPicker"
|
mode="bottom"
|
round="16"
|
@close="closeUserPicker">
|
<view class="user-picker">
|
<view class="sheet-handle" />
|
<view class="picker-head">
|
<text class="picker-cancel"
|
@click="closeUserPicker">取消</text>
|
<text class="picker-title">选择审批人</text>
|
<text class="picker-confirm"
|
@click="confirmUserPicker">
|
确定{{ pickerSelectedIds.length ? `(${pickerSelectedIds.length})` : "" }}
|
</text>
|
</view>
|
<scroll-view class="user-scroll"
|
scroll-y>
|
<view v-for="user in availableUsers"
|
:key="user.userId"
|
class="user-item"
|
:class="{ selected: isUserSelected(user.userId) }"
|
@click="toggleUser(user)">
|
<view class="user-avatar">{{ (user.nickName || "?").charAt(0) }}</view>
|
<text class="user-name">{{ user.nickName }}</text>
|
<view class="user-check"
|
:class="{ checked: isUserSelected(user.userId) }">
|
<up-icon v-if="isUserSelected(user.userId)"
|
name="checkmark"
|
size="14"
|
color="#fff" />
|
</view>
|
</view>
|
</scroll-view>
|
</view>
|
</up-popup>
|
|
<up-action-sheet :show="showBusinessTypeSheet"
|
title="选择审批类型"
|
:actions="businessTypeActions"
|
@select="onSelectBusinessType"
|
@close="showBusinessTypeSheet = false" />
|
</view>
|
</template>
|
|
<script setup>
|
import { computed, onMounted, reactive, ref } from "vue";
|
import { onLoad } from "@dcloudio/uni-app";
|
import PageHeader from "@/components/PageHeader.vue";
|
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 {
|
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 formRef = ref();
|
const submitting = ref(false);
|
const userList = ref([]);
|
const templateId = ref(null);
|
|
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: "",
|
businessType: null,
|
templateType: 1,
|
enabled: "1",
|
description: "",
|
});
|
|
const formConfig = reactive({
|
prompt: "",
|
fields: [],
|
});
|
|
const fieldDraft = reactive({
|
label: "",
|
key: "",
|
type: "text",
|
defaultValue: "",
|
required: true,
|
optionSource: "manual",
|
options: [createEmptyFieldOption()],
|
});
|
|
let nodeKeySeed = 1;
|
|
const createNode = () => ({
|
_key: `node_${nodeKeySeed++}`,
|
approveType: "AND",
|
approvers: [],
|
});
|
|
const flowNodes = ref([createNode()]);
|
|
const rules = {
|
templateName: [{ required: true, message: "请输入模板名称", trigger: "blur" }],
|
businessType: [
|
{
|
validator: (_rule, value, callback) => {
|
if (value === "" || value === null || value === undefined) {
|
callback(new Error("请选择审批类型"));
|
return;
|
}
|
callback();
|
},
|
trigger: "change",
|
},
|
],
|
};
|
|
const businessTypeOptions = ref([]);
|
const showBusinessTypeSheet = ref(false);
|
|
const businessTypeActions = computed(() =>
|
businessTypeOptions.value.map(opt => ({
|
name: opt.name,
|
value: opt.value,
|
}))
|
);
|
|
const businessTypeText = computed(() => {
|
const matched = businessTypeOptions.value.find(
|
opt => String(opt.value) === String(form.businessType)
|
);
|
return matched?.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",
|
set: val => {
|
form.enabled = val ? "1" : "0";
|
},
|
});
|
|
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 mapNodesFromRow = nodes => {
|
if (!Array.isArray(nodes) || !nodes.length) {
|
return [createNode()];
|
}
|
return nodes.map(node => ({
|
_key: `node_${nodeKeySeed++}`,
|
id: node.id,
|
templateId: node.templateId,
|
approveType: node.approveType || "AND",
|
approvers: (node.approvers || []).map((approver, idx) => ({
|
id: approver.id,
|
nodeId: approver.nodeId,
|
templateId: approver.templateId,
|
approverId: approver.approverId,
|
approverName: approver.approverName,
|
sortNo: approver.sortNo ?? idx + 1,
|
})),
|
}));
|
};
|
|
const fillFormFromRow = row => {
|
if (!row) return;
|
templateId.value = row.id;
|
form.templateName = row.templateName || "";
|
const parsedBusiness = Number(row.businessType);
|
form.businessType = Number.isNaN(parsedBusiness)
|
? row.businessType
|
: parsedBusiness;
|
const parsedTemplateType = Number(row.templateType);
|
form.templateType = Number.isNaN(parsedTemplateType) ? 1 : parsedTemplateType;
|
form.enabled = String(row.enabled ?? "1");
|
form.description = row.description || "";
|
|
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 availableUsers = computed(() => {
|
const node = flowNodes.value[editingNodeIndex.value];
|
if (!node) return userList.value;
|
const selectedIds = new Set(node.approvers.map(a => a.approverId));
|
return userList.value.filter(user => !selectedIds.has(user.userId));
|
});
|
|
const levelLabel = n => LEVEL_TEXT[n] || String(n);
|
|
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 = {
|
text: "type-tag--text",
|
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 goBack = () => {
|
uni.navigateBack();
|
};
|
|
const openBusinessTypeSheet = () => {
|
if (isSystemTemplate.value) return;
|
if (!businessTypeOptions.value.length) {
|
uni.showToast({ title: "审批类型加载中", icon: "none" });
|
return;
|
}
|
showBusinessTypeSheet.value = true;
|
};
|
|
const onSelectBusinessType = action => {
|
form.businessType = action.value;
|
showBusinessTypeSheet.value = false;
|
formRef.value?.validateField?.("businessType");
|
};
|
|
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 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 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.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 {
|
resetFieldDraft();
|
}
|
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 base = (label || "field")
|
.trim()
|
.replace(/\s+/g, "_")
|
.replace(/[^\w\u4e00-\u9fa5]/g, "");
|
let key = base || "field";
|
let index = 1;
|
while (formConfig.fields.some((item, idx) => item.key === key && idx !== editingFieldIndex.value)) {
|
key = `${base}_${index++}`;
|
}
|
return key;
|
};
|
|
const confirmFieldEditor = () => {
|
if (
|
editingFieldIndex.value >= 0 &&
|
isFieldLocked(formConfig.fields[editingFieldIndex.value])
|
) {
|
uni.showToast({ title: "系统内置填报项不可修改", icon: "none" });
|
return;
|
}
|
if (!fieldDraft.label?.trim()) {
|
uni.showToast({ title: "请输入显示名称", icon: "none" });
|
return;
|
}
|
const existingKey =
|
editingFieldIndex.value >= 0
|
? formConfig.fields[editingFieldIndex.value]?.key
|
: null;
|
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 {
|
formConfig.fields.push(payload);
|
}
|
closeFieldEditor();
|
};
|
|
const removeField = index => {
|
const field = formConfig.fields[index];
|
if (isFieldLocked(field)) {
|
uni.showToast({ title: "系统内置填报项不可删除", icon: "none" });
|
return;
|
}
|
formConfig.fields.splice(index, 1);
|
};
|
|
const addNode = () => {
|
flowNodes.value.push(createNode());
|
};
|
|
const removeNode = index => {
|
if (flowNodes.value.length <= 1) {
|
uni.showToast({ title: "至少保留一个审批节点", icon: "none" });
|
return;
|
}
|
flowNodes.value.splice(index, 1);
|
};
|
|
const openUserPicker = nodeIndex => {
|
editingNodeIndex.value = nodeIndex;
|
pickerSelectedIds.value = [];
|
showUserPicker.value = true;
|
};
|
|
const closeUserPicker = () => {
|
showUserPicker.value = false;
|
editingNodeIndex.value = -1;
|
pickerSelectedIds.value = [];
|
};
|
|
const isUserSelected = userId => pickerSelectedIds.value.includes(userId);
|
|
const toggleUser = user => {
|
const ids = pickerSelectedIds.value;
|
const index = ids.indexOf(user.userId);
|
if (index >= 0) {
|
ids.splice(index, 1);
|
} else {
|
ids.push(user.userId);
|
}
|
};
|
|
const confirmUserPicker = () => {
|
const node = flowNodes.value[editingNodeIndex.value];
|
if (!node) {
|
closeUserPicker();
|
return;
|
}
|
const selectedUsers = userList.value.filter(user =>
|
pickerSelectedIds.value.includes(user.userId)
|
);
|
if (!selectedUsers.length) {
|
uni.showToast({ title: "请选择审批人", icon: "none" });
|
return;
|
}
|
const startSort = node.approvers.length;
|
selectedUsers.forEach((user, idx) => {
|
node.approvers.push({
|
approverId: user.userId,
|
approverName: user.nickName,
|
sortNo: startSort + idx + 1,
|
});
|
});
|
closeUserPicker();
|
};
|
|
const removeApprover = (nodeIndex, approverIndex) => {
|
const node = flowNodes.value[nodeIndex];
|
if (!node?.approvers?.length) return;
|
const next = node.approvers
|
.filter((_, idx) => idx !== approverIndex)
|
.map((item, idx) => ({
|
...item,
|
sortNo: idx + 1,
|
}));
|
node.approvers = next;
|
};
|
|
const validateFlow = () => {
|
if (!flowNodes.value.length) {
|
uni.showToast({ title: "请配置审批流程", icon: "none" });
|
return false;
|
}
|
const emptyNode = flowNodes.value.find(node => !node.approvers.length);
|
if (emptyNode) {
|
uni.showToast({ title: "请为每个审批节点添加审批人", icon: "none" });
|
return false;
|
}
|
return true;
|
};
|
|
const buildSubmitPayload = () => {
|
const tid = templateId.value;
|
const payload = {
|
templateName: form.templateName.trim(),
|
enabled: form.enabled,
|
description: form.description?.trim() || "",
|
businessType: form.businessType,
|
templateType: form.templateType,
|
formConfig: JSON.stringify({
|
prompt: formConfig.prompt?.trim() || "",
|
fields: formConfig.fields,
|
}),
|
nodes: flowNodes.value.map((node, index) => {
|
const nodePayload = {
|
levelNo: index + 1,
|
approveType: node.approveType,
|
approvers: node.approvers.map((approver, approverIndex) => {
|
const approverPayload = {
|
approverId: approver.approverId,
|
approverName: approver.approverName,
|
sortNo: approverIndex + 1,
|
};
|
if (isEditMode.value) {
|
if (approver.id != null) approverPayload.id = approver.id;
|
if (approver.nodeId != null) approverPayload.nodeId = approver.nodeId;
|
else if (node.id != null) approverPayload.nodeId = node.id;
|
if (approver.templateId != null) approverPayload.templateId = approver.templateId;
|
else if (tid != null) approverPayload.templateId = tid;
|
}
|
return approverPayload;
|
}),
|
};
|
if (isEditMode.value) {
|
if (node.id != null) nodePayload.id = node.id;
|
if (node.templateId != null) nodePayload.templateId = node.templateId;
|
else if (tid != null) nodePayload.templateId = tid;
|
}
|
return nodePayload;
|
}),
|
};
|
|
if (isEditMode.value) {
|
payload.id = tid;
|
}
|
|
return payload;
|
};
|
|
const handleSubmit = async () => {
|
const valid = await formRef.value.validate().catch(() => false);
|
if (!valid || !validateFlow()) return;
|
|
submitting.value = true;
|
const submitApi = isEditMode.value ? updateApprovalTemplate : addApprovalTemplate;
|
submitApi(buildSubmitPayload())
|
.then(() => {
|
uni.showToast({
|
title: isEditMode.value ? "修改成功" : "保存成功",
|
icon: "success",
|
});
|
uni.removeStorageSync(EDIT_STORAGE_KEY);
|
setTimeout(() => {
|
uni.navigateBack();
|
}, 300);
|
})
|
.catch(() => {
|
uni.showToast({
|
title: isEditMode.value ? "修改失败" : "保存失败",
|
icon: "none",
|
});
|
})
|
.finally(() => {
|
submitting.value = false;
|
});
|
};
|
|
onLoad(options => {
|
if (options?.id) {
|
const row = uni.getStorageSync(EDIT_STORAGE_KEY);
|
if (row && String(row.id) === String(options.id)) {
|
fillFormFromRow(row);
|
} else {
|
templateId.value = options.id;
|
uni.showToast({ title: "未获取到模板数据", icon: "none" });
|
}
|
uni.removeStorageSync(EDIT_STORAGE_KEY);
|
}
|
});
|
|
const loadTemplateTypes = () =>
|
fetchApprovalTemplateTypes()
|
.then(opts => {
|
businessTypeOptions.value = opts;
|
if (!templateId.value && opts.length) {
|
const matched = opts.some(
|
opt => String(opt.value) === String(form.businessType)
|
);
|
if (!matched) {
|
form.businessType = opts[0].value;
|
}
|
}
|
})
|
.catch(() => {
|
uni.showToast({ title: "获取审批类型失败", icon: "none" });
|
});
|
|
onMounted(() => {
|
loadTemplateTypes();
|
userListNoPageByTenantId()
|
.then(res => {
|
userList.value = res?.data || [];
|
})
|
.catch(() => {
|
userList.value = [];
|
});
|
getDept()
|
.then(res => {
|
deptList.value = res?.data || [];
|
})
|
.catch(() => {
|
deptList.value = [];
|
});
|
});
|
</script>
|
|
<style scoped lang="scss">
|
@import "@/static/scss/form-common.scss";
|
|
$primary: #2979ff;
|
$primary-light: #ecf5ff;
|
$text: #1f2d3d;
|
$text-secondary: #606266;
|
$text-muted: #909399;
|
$border: #ebeef5;
|
$bg-page: #f0f3f8;
|
$radius-lg: 12px;
|
$radius-md: 10px;
|
$shadow-card: 0 2px 12px rgba(31, 45, 61, 0.05);
|
|
.template-edit-page {
|
display: flex;
|
flex-direction: column;
|
min-height: 100vh;
|
background: $bg-page;
|
}
|
|
.form-scroll {
|
flex: 1;
|
height: 0;
|
padding: 10px 12px calc(96px + env(safe-area-inset-bottom));
|
}
|
|
.section-card {
|
margin-bottom: 10px;
|
background: #fff;
|
border-radius: $radius-lg;
|
overflow: hidden;
|
box-shadow: $shadow-card;
|
}
|
|
.section-head {
|
padding: 12px 16px;
|
border-bottom: 1px solid #f2f4f7;
|
|
&--between {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
}
|
}
|
|
.section-head-left {
|
display: flex;
|
flex-direction: column;
|
gap: 4px;
|
}
|
|
.section-title {
|
font-size: 15px;
|
font-weight: 600;
|
color: $text;
|
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 {
|
display: flex;
|
align-items: center;
|
gap: 16px;
|
}
|
|
.head-link {
|
font-size: 14px;
|
color: $text-secondary;
|
|
&--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 {
|
padding: 2px 16px 14px;
|
}
|
|
.form-section {
|
margin-bottom: 10px;
|
border-radius: $radius-lg;
|
overflow: hidden;
|
box-shadow: $shadow-card;
|
}
|
|
:deep(.form-section .u-cell-group__title) {
|
padding: 12px 16px 8px !important;
|
font-size: 15px !important;
|
font-weight: 600 !important;
|
color: $text !important;
|
background: #fff !important;
|
}
|
|
:deep(.form-section .u-form-item) {
|
padding: 0 16px !important;
|
}
|
|
:deep(.form-section .u-form-item__body) {
|
padding: 10px 0 !important;
|
min-height: auto !important;
|
}
|
|
:deep(.form-item-name .u-form-item__body) {
|
flex-direction: row !important;
|
align-items: center !important;
|
}
|
|
:deep(.form-item-name .u-form-item__content) {
|
flex: 1 !important;
|
min-width: 0 !important;
|
justify-content: flex-end !important;
|
}
|
|
:deep(.name-input-inline),
|
:deep(.name-input-inline .u-input__content) {
|
width: 100% !important;
|
flex: 1 !important;
|
}
|
|
:deep(.name-input-inline input),
|
:deep(.name-input-inline .u-input__content__field-wrapper__field) {
|
width: 100% !important;
|
text-align: right !important;
|
font-size: 15px !important;
|
}
|
|
:deep(.form-item-select .u-form-item__body) {
|
align-items: center !important;
|
}
|
|
:deep(.form-item-select .u-form-item__content) {
|
flex: 1 !important;
|
min-width: 0 !important;
|
justify-content: flex-end !important;
|
}
|
|
:deep(.form-item-select .u-input__content__field-wrapper__field) {
|
text-align: right !important;
|
}
|
|
:deep(.form-item-switch .u-form-item__body) {
|
flex-direction: row !important;
|
align-items: center !important;
|
}
|
|
:deep(.form-item-switch .u-form-item__content) {
|
flex: 1 !important;
|
min-width: 0 !important;
|
display: flex !important;
|
justify-content: flex-end !important;
|
}
|
|
.switch-wrap {
|
display: flex;
|
justify-content: flex-end;
|
align-items: center;
|
width: 100%;
|
}
|
|
:deep(.form-item-desc .u-form-item__body) {
|
flex-direction: column !important;
|
align-items: stretch !important;
|
padding: 10px 0 12px !important;
|
}
|
|
:deep(.form-item-desc .u-form-item__content) {
|
justify-content: stretch !important;
|
width: 100% !important;
|
}
|
|
.desc-input-shell {
|
width: 100%;
|
box-sizing: border-box;
|
padding: 8px 12px;
|
background: #fff;
|
border: 1px solid #dcdfe6;
|
border-radius: 6px;
|
}
|
|
:deep(.desc-input-shell .u-textarea),
|
:deep(.desc-input-shell textarea) {
|
width: 100% !important;
|
font-size: 15px !important;
|
text-align: left !important;
|
}
|
|
.form-row-item {
|
margin: 0 !important;
|
padding: 0 !important;
|
}
|
|
:deep(.form-row-item .u-form-item__body) {
|
padding: 0;
|
}
|
|
:deep(.form-row-item .u-form-item__body__right__message) {
|
margin-top: 4px;
|
padding-left: 0;
|
}
|
|
.form-row {
|
padding: 10px 0;
|
border-bottom: 1px solid #f5f7fa;
|
|
&:last-child {
|
border-bottom: none;
|
}
|
|
&--column {
|
flex-direction: column;
|
align-items: stretch;
|
gap: 8px;
|
}
|
|
&--compact {
|
padding-top: 8px;
|
}
|
}
|
|
.form-row-label {
|
display: block;
|
font-size: 14px;
|
color: $text-secondary;
|
margin-bottom: 8px;
|
|
&.required::before {
|
content: "*";
|
color: #f56c6c;
|
margin-right: 3px;
|
}
|
}
|
|
.form-row--column .form-row-label {
|
margin-bottom: 0;
|
}
|
|
.prompt-row {
|
display: flex;
|
align-items: center;
|
padding: 12px 0;
|
margin-bottom: 4px;
|
border-bottom: 1px solid #f5f7fa;
|
gap: 8px;
|
}
|
|
.prompt-label {
|
flex-shrink: 0;
|
width: 88px;
|
font-size: 14px;
|
color: $text-secondary;
|
}
|
|
.prompt-input {
|
flex: 1;
|
min-width: 0;
|
}
|
|
:deep(.prompt-input),
|
:deep(.prompt-input .u-input__content) {
|
width: 100% !important;
|
}
|
|
:deep(.prompt-input input),
|
:deep(.prompt-input .u-input__content__field-wrapper__field) {
|
width: 100% !important;
|
text-align: right !important;
|
font-size: 15px !important;
|
}
|
|
.input-box,
|
.textarea-box {
|
background: #f7f9fc;
|
border-radius: 10px;
|
border: 1px solid #eef1f6;
|
overflow: hidden;
|
}
|
|
.textarea-box {
|
padding: 4px 0;
|
}
|
|
.field-list {
|
display: flex;
|
flex-direction: column;
|
gap: 8px;
|
margin-top: 8px;
|
}
|
|
.field-item {
|
display: flex;
|
align-items: center;
|
gap: 12px;
|
padding: 14px;
|
background: #fff;
|
border-radius: $radius-md;
|
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 {
|
flex: 1;
|
min-width: 0;
|
}
|
|
.field-title-row {
|
display: flex;
|
align-items: center;
|
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 {
|
font-size: 11px;
|
padding: 2px 8px;
|
border-radius: 4px;
|
|
&--text {
|
color: #2979ff;
|
background: #ecf5ff;
|
}
|
|
&--area {
|
color: #7c5cfc;
|
background: #f3efff;
|
}
|
|
&--num {
|
color: #e6a23c;
|
background: #fdf6ec;
|
}
|
|
&--date {
|
color: #18a058;
|
background: #e8faf0;
|
}
|
|
&--select {
|
color: #9c27b0;
|
background: #f6edfc;
|
}
|
}
|
|
.req-tag {
|
font-size: 11px;
|
padding: 2px 6px;
|
color: #f56c6c;
|
background: #fef0f0;
|
border-radius: 4px;
|
}
|
|
.field-default {
|
display: block;
|
margin-top: 4px;
|
font-size: 12px;
|
color: $text-muted;
|
}
|
|
.field-actions {
|
display: flex;
|
gap: 6px;
|
flex-shrink: 0;
|
}
|
|
.icon-btn {
|
width: 32px;
|
height: 32px;
|
border-radius: 8px;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
|
&--edit {
|
background: #ecf5ff;
|
}
|
|
&--del {
|
background: #fef0f0;
|
}
|
}
|
|
.empty-mini {
|
padding: 32px 16px;
|
text-align: center;
|
font-size: 13px;
|
color: $text-muted;
|
background: #fafbfd;
|
border: 1px dashed #dce8f5;
|
border-radius: 10px;
|
}
|
|
.flow-wrap {
|
padding: 10px 16px 14px;
|
}
|
|
.flow-node-block {
|
display: flex;
|
flex-direction: column;
|
align-items: stretch;
|
}
|
|
.flow-node-card {
|
background: #fafbfd;
|
border: 1px solid #e8eef5;
|
border-radius: $radius-md;
|
padding: 12px;
|
}
|
|
.node-header {
|
display: flex;
|
align-items: center;
|
gap: 10px;
|
margin-bottom: 10px;
|
}
|
|
.node-level-badge {
|
width: 26px;
|
height: 26px;
|
border-radius: 8px;
|
background: $primary;
|
color: #fff;
|
font-size: 14px;
|
font-weight: 600;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
flex-shrink: 0;
|
}
|
|
.node-level-text {
|
flex: 1;
|
font-size: 15px;
|
font-weight: 600;
|
color: $text;
|
}
|
|
.node-delete {
|
padding: 6px;
|
flex-shrink: 0;
|
}
|
|
.approve-type-row {
|
display: flex;
|
background: #f0f3f8;
|
border-radius: 8px;
|
padding: 3px;
|
margin-bottom: 10px;
|
}
|
|
.type-btn {
|
flex: 1;
|
text-align: center;
|
padding: 8px 0;
|
font-size: 14px;
|
color: $text-secondary;
|
border-radius: 6px;
|
|
&.active {
|
background: #fff;
|
color: $primary;
|
font-weight: 500;
|
}
|
}
|
|
.approver-list {
|
display: flex;
|
flex-wrap: wrap;
|
gap: 8px;
|
}
|
|
.approver-chip {
|
display: flex;
|
align-items: center;
|
gap: 8px;
|
padding: 6px 12px 6px 6px;
|
background: #fff;
|
border: 1px solid #dce8f8;
|
border-radius: 24px;
|
box-shadow: 0 2px 6px rgba(41, 121, 255, 0.06);
|
}
|
|
.approver-avatar {
|
width: 26px;
|
height: 26px;
|
border-radius: 50%;
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
color: #fff;
|
font-size: 12px;
|
font-weight: 600;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
}
|
|
.approver-name {
|
font-size: 13px;
|
color: $text;
|
max-width: 80px;
|
overflow: hidden;
|
text-overflow: ellipsis;
|
white-space: nowrap;
|
}
|
|
.approver-remove {
|
flex-shrink: 0;
|
width: 22px;
|
height: 22px;
|
margin-left: 2px;
|
border-radius: 50%;
|
background: #f2f3f5;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
}
|
|
.approver-remove--active {
|
background: #fde2e2;
|
}
|
|
.remove-icon {
|
font-size: 16px;
|
line-height: 1;
|
color: #909399;
|
font-weight: 300;
|
}
|
|
.add-approver {
|
display: flex;
|
align-items: center;
|
gap: 4px;
|
padding: 8px 14px;
|
border: 1.5px dashed #a8cfff;
|
border-radius: 24px;
|
background: $primary-light;
|
color: $primary;
|
font-size: 13px;
|
}
|
|
.flow-connector {
|
display: flex;
|
justify-content: center;
|
padding: 4px 0;
|
}
|
|
.flow-connector-line {
|
width: 2px;
|
height: 14px;
|
background: #d0dff0;
|
}
|
|
.add-node-bar {
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
gap: 6px;
|
margin-top: 8px;
|
padding: 11px;
|
border: 1px dashed #c6daf5;
|
border-radius: $radius-md;
|
color: $primary;
|
font-size: 14px;
|
}
|
|
.sheet-handle {
|
width: 36px;
|
height: 4px;
|
margin: 10px auto 4px;
|
background: #d8dde6;
|
border-radius: 2px;
|
}
|
|
.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-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;
|
}
|
|
.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: 10px;
|
padding: 12px 16px 16px;
|
}
|
|
.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 {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
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 {
|
display: block;
|
font-size: 13px;
|
font-weight: 500;
|
color: $text-secondary;
|
margin-bottom: 8px;
|
|
&.required::before {
|
content: "*";
|
color: #f56c6c;
|
margin-right: 3px;
|
}
|
}
|
|
.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 {
|
display: grid;
|
grid-template-columns: 1fr 1fr;
|
gap: 10px;
|
}
|
|
.type-chip {
|
text-align: center;
|
padding: 10px 6px;
|
font-size: 13px;
|
color: $text-secondary;
|
background: #f7f9fc;
|
border: 1px solid #eef1f6;
|
border-radius: 8px;
|
|
&.active {
|
color: $primary;
|
background: $primary-light;
|
border-color: $primary;
|
font-weight: 500;
|
}
|
}
|
|
.editor-footer {
|
display: flex;
|
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: 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: linear-gradient(135deg, #4d8dff 0%, #2979ff 100%);
|
box-shadow: 0 4px 12px rgba(41, 121, 255, 0.35);
|
}
|
|
&--confirm:active {
|
opacity: 0.9;
|
}
|
}
|
|
.picker-head {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
padding-bottom: 14px;
|
border-bottom: 1px solid #f5f7fa;
|
margin-bottom: 8px;
|
}
|
|
.picker-title {
|
font-size: 16px;
|
font-weight: 600;
|
color: $text;
|
}
|
|
.picker-cancel {
|
font-size: 15px;
|
color: $text-muted;
|
min-width: 48px;
|
}
|
|
.picker-confirm {
|
font-size: 15px;
|
color: $primary;
|
font-weight: 600;
|
min-width: 48px;
|
text-align: right;
|
}
|
|
.user-scroll {
|
max-height: 52vh;
|
}
|
|
.user-item {
|
display: flex;
|
align-items: center;
|
gap: 12px;
|
padding: 14px 4px;
|
border-bottom: 1px solid #f5f7fa;
|
border-radius: 10px;
|
margin-bottom: 4px;
|
transition: background 0.2s;
|
|
&.selected {
|
background: #f5f9ff;
|
}
|
}
|
|
.user-avatar {
|
width: 40px;
|
height: 40px;
|
border-radius: 12px;
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
color: #fff;
|
font-size: 16px;
|
font-weight: 600;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
flex-shrink: 0;
|
}
|
|
.user-name {
|
flex: 1;
|
font-size: 15px;
|
color: $text;
|
}
|
|
.user-check {
|
width: 22px;
|
height: 22px;
|
border-radius: 50%;
|
border: 2px solid #dcdfe6;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
flex-shrink: 0;
|
|
&.checked {
|
background: $primary;
|
border-color: $primary;
|
}
|
}
|
</style>
|