diff --git a/README.md b/README.md
index 1dd32c2..62c6e53 100644
--- a/README.md
+++ b/README.md
@@ -28,7 +28,7 @@ node server.js
{
"projectId": "approval_of_design",
"port": 8080,
- "host": "0.0.0.0",
+ "host": "127.0.0.1",
"label": "飞书审批配置服务"
}
```
diff --git a/deploy/feishu-approval.service b/deploy/feishu-approval.service
index 9886a16..fd41052 100644
--- a/deploy/feishu-approval.service
+++ b/deploy/feishu-approval.service
@@ -8,7 +8,7 @@ User=root
WorkingDirectory=/opt/feishu-approval
Environment=NODE_ENV=production
Environment=PORT=8080
-Environment=HOST=0.0.0.0
+Environment=HOST=127.0.0.1
ExecStart=/usr/bin/node server.js
Restart=on-failure
RestartSec=5
diff --git a/project.config.json b/project.config.json
index cf24eab..62bbf69 100644
--- a/project.config.json
+++ b/project.config.json
@@ -1,6 +1,6 @@
{
"projectId": "approval_of_design",
"port": 8080,
- "host": "0.0.0.0",
+ "host": "127.0.0.1",
"label": "飞书审批配置服务"
}
diff --git a/飞书审批配置文件_V1.0.html b/飞书审批配置文件_V1.0.html
index 60317da..8fecc6d 100644
--- a/飞书审批配置文件_V1.0.html
+++ b/飞书审批配置文件_V1.0.html
@@ -98,6 +98,16 @@ a{color:#3370ff;text-decoration:none}
.widget-item{display:flex;align-items:center;gap:10px;padding:9px 10px;border-radius:var(--radius-sm);cursor:pointer;transition:all .15s;margin-bottom:2px;font-size:13px;color:var(--text)}
.widget-item:hover{background:var(--primary-light);color:var(--primary)}
.widget-item .icon{width:20px;height:20px;display:flex;align-items:center;justify-content:center;font-size:14px}
+.widget-item.is-limited{position:relative;color:var(--text-caption);cursor:not-allowed}
+.widget-item.is-limited:hover{background:var(--bg);color:var(--text-caption)}
+.widget-item.is-limited::after{content:attr(data-limited-tip);position:absolute;left:10px;top:calc(100% + 4px);z-index:20;width:max-content;max-width:210px;padding:6px 8px;border-radius:var(--radius-sm);background:#1f2937;color:#fff;font-size:12px;line-height:1.4;box-shadow:var(--shadow);opacity:0;pointer-events:none;transform:translateY(-2px);transition:opacity .15s,transform .15s}
+.widget-item.is-limited:hover::after{opacity:1;transform:translateY(0)}
+.tip-dialog-mask{position:fixed;inset:0;z-index:1000;display:flex;align-items:center;justify-content:center;background:rgba(15,23,42,.35)}
+.tip-dialog-mask.hidden{display:none}
+.tip-dialog{width:320px;max-width:calc(100vw - 32px);padding:20px;border-radius:var(--radius);background:var(--surface);box-shadow:var(--shadow-lg)}
+.tip-dialog-title{font-size:16px;font-weight:600;color:var(--text);margin-bottom:12px}
+.tip-dialog-content{font-size:14px;line-height:1.6;color:var(--text-secondary);margin-bottom:20px}
+.tip-dialog-actions{display:flex;justify-content:flex-end}
.form-design-right{width:360px;background:var(--surface);border-left:1px solid var(--border);overflow-y:auto;flex-shrink:0}
@@ -493,7 +503,7 @@ document.documentElement.setAttribute('data-app-mode', window.APP_MODE);
-
@@ -703,6 +713,7 @@ let startCcRecipients = [];
let endCcRecipients = [{ id: 'end_cc_1', approverType: 'self', approverName: '', approverLevel: 1, roleName: '', userGroupName: '', formContactField: '', formDeptField: '' }];
let startFormPermissions = {};
let endFormPermissions = {};
+const CLOUD_DOC_LIMIT_MESSAGE = '表单中只能放置一个文档类型的字段';
const submitterTypeLabels = { all: '全员', dept: '指定部门', member: '指定成员' };
const approverTypeLabels = { superior: '上级', dept_head: '部门负责人', role: '角色', user_group: '用户组', member: '指定成员', self_select: '提交人自选', self: '提交人本人', form_contact: '表单内联系人', form_dept: '表单内部门', node_approver: '节点审批人', node_cc: '节点抄送人', multi_level_superior: '连续多级上级', multi_level_dept_head: '连续多级部门负责人' };
@@ -861,6 +872,71 @@ function getSelectedDetailParent() {
return null;
}
+function hasFormFieldType(type) {
+ for (const f of formFields) {
+ if (f.type === type) return true;
+ if (f.detailChildren?.some(c => c.type === type)) return true;
+ }
+ return false;
+}
+
+function canAddFormFieldType(type, showMessage) {
+ if (type === 'cloud_doc' && hasFormFieldType('cloud_doc')) {
+ if (showMessage) showTipDialog(CLOUD_DOC_LIMIT_MESSAGE);
+ return false;
+ }
+ return true;
+}
+
+function showTipDialog(message) {
+ let mask = document.getElementById('tip-dialog-mask');
+ if (!mask) {
+ mask = document.createElement('div');
+ mask.id = 'tip-dialog-mask';
+ mask.className = 'tip-dialog-mask hidden';
+ mask.setAttribute('role', 'dialog');
+ mask.setAttribute('aria-modal', 'true');
+ mask.onclick = e => {
+ if (e.target === mask) closeTipDialog();
+ };
+ mask.innerHTML = `
+
`;
+ document.body.appendChild(mask);
+ }
+ const content = document.getElementById('tip-dialog-content');
+ if (content) content.textContent = message;
+ mask.classList.remove('hidden');
+ mask.querySelector('button')?.focus();
+}
+
+function closeTipDialog() {
+ document.getElementById('tip-dialog-mask')?.classList.add('hidden');
+}
+
+function syncCloudDocWidgetState() {
+ const el = document.getElementById('widget-cloud-doc');
+ if (!el) return;
+ const limited = hasFormFieldType('cloud_doc');
+ el.classList.toggle('is-limited', limited);
+ el.draggable = !limited;
+ if (limited) {
+ el.setAttribute('title', CLOUD_DOC_LIMIT_MESSAGE);
+ el.setAttribute('data-limited-tip', CLOUD_DOC_LIMIT_MESSAGE);
+ } else {
+ el.removeAttribute('title');
+ el.removeAttribute('data-limited-tip');
+ }
+}
+
+function handleCloudDocWidgetClick() {
+ if (!canAddFormFieldType('cloud_doc', true)) return;
+ addFormField('cloud_doc');
+}
+
function syncHeaderName() {
const name = document.getElementById('approval-name')?.value || '审批配置';
const header = document.getElementById('header-approval-name');
@@ -1328,7 +1404,7 @@ function renderOtherOptionsSection(field) {
}
let printExtra = '';
if (field.type === 'attachment' && field.printable) {
- printExtra = '
打印附件中的图片
';
+ printExtra = `
`;
}
return `
@@ -1425,6 +1501,7 @@ function createFormFieldData(type) {
if (type === 'single_choice') field.linkage = { enabled: false, targetId: '', mappings: {} };
if (type === 'bitable') { field.bitableTitle = ''; field.tableName = ''; field.refFields = []; }
if (type === 'image') { field.allowImage = true; field.allowVideo = true; field.mobileOnlyCapture = false; }
+ if (type === 'attachment') field.printAttachmentImages = false;
if (type === 'department') { field.deptSelectionMode = 'single'; field.deptDisplayMode = 'leaf_only'; }
if (type === 'contact' || type === 'member') { field.contactSelectionMode = 'single'; field.allowSelectSelf = true; }
if (type === 'related_approval') { field.scopeConfig = ''; field.onlyApproved = false; }
@@ -1480,6 +1557,7 @@ function normalizeFormField(field) {
if (field.allowVideo === undefined) field.allowVideo = true;
if (field.mobileOnlyCapture === undefined) field.mobileOnlyCapture = false;
}
+ if (field.type === 'attachment' && field.printAttachmentImages === undefined) field.printAttachmentImages = false;
if (field.type === 'department') {
if (!field.deptSelectionMode) field.deptSelectionMode = 'single';
if (!field.deptDisplayMode) field.deptDisplayMode = 'leaf_only';
@@ -1507,6 +1585,7 @@ function normalizeFormField(field) {
}
function addFormField(type, index) {
+ if (!canAddFormFieldType(type, true)) return;
if (type === 'detail') {
const field = createFormFieldData(type);
if (!field) return;
@@ -1538,6 +1617,7 @@ function addFormField(type, index) {
function addFormFieldToDetail(detailId, type) {
if (type === 'detail' || !fieldMeta[type]) return;
+ if (!canAddFormFieldType(type, true)) return;
const detail = formFields.find(f => f.id === detailId && f.type === 'detail');
if (!detail) return;
if (!detail.detailChildren) detail.detailChildren = [];
@@ -1551,6 +1631,11 @@ function addFormFieldToDetail(detailId, type) {
}
function dragWidget(ev, type) {
+ if (!canAddFormFieldType(type, true)) {
+ ev.preventDefault();
+ ev.stopPropagation();
+ return;
+ }
ev.dataTransfer.setData('widgetType', type);
}
function allowDrop(ev) { ev.preventDefault(); }
@@ -1626,6 +1711,7 @@ function commitFieldName(id, key) {
function renderFormFields() {
const list = document.getElementById('field-list');
if (!list) return;
+ syncCloudDocWidgetState();
if (formFields.length === 0) {
list.innerHTML = FIELD_EMPTY_TIP_HTML;
return;
@@ -2331,6 +2417,7 @@ function updateField(id, key, val) {
const f = findFormField(id);
if (!f) return;
f[key] = val;
+ if (key === 'printable' && f.type === 'attachment' && !val) f.printAttachmentImages = false;
const liveTextKeys = ['title', 'startTitle', 'endTitle', 'durationTitle', 'placeholder', 'defaultValue', 'unit', 'minValue', 'maxValue', 'formula', 'bitableTitle', 'tableName', 'scopeConfig', 'archiveLocation', 'customDateStart', 'customDateEnd'];
if (liveTextKeys.includes(key)) {
if (['title', 'startTitle', 'endTitle', 'durationTitle'].includes(key)) {
@@ -2346,7 +2433,7 @@ function updateField(id, key, val) {
autoSave();
return;
}
- const configOnlyKeys = ['allowSelectSelf', 'deptDisplayMode', 'deptSelectionMode', 'contactSelectionMode', 'onlyApproved', 'autoLocate', 'showDetailAddress', 'fillDetailAddress', 'locationDisplayMode', 'validateBranchRequired', 'phoneType', 'allowImage', 'allowVideo', 'mobileOnlyCapture', 'printable', 'required', 'allowEditDuration', 'dateFormat', 'dateRangeOption'];
+ const configOnlyKeys = ['allowSelectSelf', 'deptDisplayMode', 'deptSelectionMode', 'contactSelectionMode', 'onlyApproved', 'autoLocate', 'showDetailAddress', 'fillDetailAddress', 'locationDisplayMode', 'validateBranchRequired', 'phoneType', 'allowImage', 'allowVideo', 'mobileOnlyCapture', 'printable', 'printAttachmentImages', 'required', 'allowEditDuration', 'dateFormat', 'dateRangeOption'];
if (configOnlyKeys.includes(key)) {
if (key === 'deptSelectionMode' || key === 'contactSelectionMode') renderFlowConfig();
renderFormConfig();
@@ -3276,6 +3363,21 @@ function renderEmptyHandlerPanel(node) {
`;
}
+function renderMultiPersonModeSection(node, title, oneLabel, allLabel, extraOptions) {
+ ensureNodeApprovers(node);
+ if (node.approvers.length <= 1) return '';
+ const extraHtml = (extraOptions || []).map(opt =>
+ `
`
+ ).join('');
+ return `
${title}
+
+
`;
+}
+
function renderNodeCcPanel(node) {
ensureNodeApprovers(node);
if (!node.ccRecipients) node.ccRecipients = [];
@@ -3833,12 +3935,7 @@ function renderApproverConfig(node) {
${renderNodePersonsPanel(node, '审批人', '+ 添加审批人')}
${renderEmptyApproverPanel(node)}
${renderSameAsSubmitterPanel(node)}
-
多人审批方式
-
-
+ ${renderMultiPersonModeSection(node, '多人审批时采用的审批方式', '或签(一名审批人同意即可)', '会签(须所有审批人同意)')}
${renderNodeCcPanel(node)}
`;
@@ -3873,6 +3970,7 @@ function renderHandlerConfig(node) {
body = `