490 lines
23 KiB
Vue
490 lines
23 KiB
Vue
<script setup lang="ts">
|
||
/** 事项编辑(22 字段 + 表单/流程绑定;迁移自 CMS ServiceItemEdit,siteId→siteCode)。 */
|
||
import { computed, onMounted, ref } from "vue";
|
||
import { useRoute, useRouter } from "vue-router";
|
||
import { Message } from "view-ui-plus";
|
||
import {
|
||
fileApi,
|
||
svcDictApi,
|
||
svcFlowApi,
|
||
svcFormApi,
|
||
svcItemApi,
|
||
} from "@/api/module/svc";
|
||
import type { FcFormCatalogItem, SvcDeptVo, SvcItemAttachmentVo, SvcItemSaveRequest } from "@/types/svc";
|
||
import { useSite } from "@/utils/site";
|
||
import RichTextEditor from "@/components/RichTextEditor.vue";
|
||
|
||
const route = useRoute();
|
||
const router = useRouter();
|
||
const { currentSiteCode, loadSites } = useSite();
|
||
const formRef = ref<any>(null);
|
||
|
||
// 必填校验(bsch应用页规范 §1:校验失败提示统一"请填写红色星号内容!")
|
||
const saveRules = {
|
||
itemCode: [{ required: true, message: "请填写红色星号内容!", trigger: "blur" }],
|
||
title: [{ required: true, message: "请填写红色星号内容!", trigger: "blur" }],
|
||
};
|
||
|
||
const form = ref<SvcItemSaveRequest>({
|
||
siteCode: "",
|
||
itemCode: "",
|
||
title: "",
|
||
handleMode: 1,
|
||
onlineFlag: 1,
|
||
sortNo: 0,
|
||
coopDepts: "",
|
||
handlerName: "",
|
||
handleTimeDesc: "",
|
||
runCount: undefined,
|
||
expressFlag: undefined,
|
||
feeDesc: "",
|
||
feeBasis: "",
|
||
});
|
||
const objects = ref<{ dictKey: string; dictName: string }[]>([]);
|
||
const categories = ref<{ dictKey: string; dictName: string }[]>([]);
|
||
const depts = ref<SvcDeptVo[]>([]);
|
||
/** 表单中心目录(v3/TASK_1:办理表单只能从这里选;本地表单定义已停写) */
|
||
const fcForms = ref<FcFormCatalogItem[]>([]);
|
||
const fcFormsDisabled = ref(false);
|
||
const fcFormsReason = ref("");
|
||
const flows = ref<{ flowGuid: string; name: string; version: number }[]>([]);
|
||
const flowDisabled = ref(false);
|
||
const flowDisabledReason = ref("");
|
||
const attachments = ref<SvcItemAttachmentVo[]>([]);
|
||
const uploadingCover = ref(false);
|
||
|
||
const editing = computed(() => !!route.query.id);
|
||
const pageTitle = computed(() => (editing.value ? "编辑事项" : "新建事项"));
|
||
|
||
/**
|
||
* 选中即成对存 uuid + 中心当时的发布版(v3 §4:只保存服务器验证过的坐标,不接受手填版本)。
|
||
* 清空=两列一起清——后端要求成对,留一个没人认领的版本号没有意义。
|
||
*/
|
||
function onFcFormChange(uuid?: string) {
|
||
const hit = uuid ? fcForms.value.find((f) => f.formUuid === uuid) : undefined;
|
||
form.value.fcFormUuid = hit ? hit.formUuid : undefined;
|
||
form.value.fcFormVersion = hit ? hit.version : undefined;
|
||
}
|
||
|
||
/** 绑定回显:存的是当时核验过的版本,中心又发新版时页面要点出来(不自动改绑) */
|
||
const fcBinding = computed(() => {
|
||
if (!form.value.fcFormUuid) return "";
|
||
const newest = fcForms.value.find((f) => f.formUuid === form.value.fcFormUuid)?.version;
|
||
const base = `已绑定 ${form.value.fcFormUuid} v${form.value.fcFormVersion}`;
|
||
return newest && newest !== form.value.fcFormVersion ? `${base}(中心最新已发布 v${newest},需换版请重新选择)` : base;
|
||
});
|
||
|
||
/** 部门树扁平化(缩进展示,承袭 CMS flattenDepts) */
|
||
const deptOptions = computed(() => {
|
||
const out: { id: number; label: string }[] = [];
|
||
const walk = (nodes: SvcDeptVo[], depth: number) => {
|
||
for (const n of nodes || []) {
|
||
out.push({ id: n.id, label: `${"\u00A0\u00A0\u00A0\u00A0".repeat(depth)}${n.name}` });
|
||
walk(n.children || [], depth + 1);
|
||
}
|
||
};
|
||
walk(depts.value, 0);
|
||
return out;
|
||
});
|
||
|
||
async function loadAll() {
|
||
await loadSites();
|
||
const siteCode = currentSiteCode.value;
|
||
if (!siteCode) {
|
||
Message.warning("无可管理的站点,请先在 UserBase 配置应用模块并开通站点");
|
||
return;
|
||
}
|
||
form.value.siteCode = siteCode;
|
||
objects.value = ((await svcDictApi.list(siteCode, "object").send()) as any[]) || [];
|
||
categories.value = ((await svcDictApi.list(siteCode, "category").send()) as any[]) || [];
|
||
depts.value = (await svcDictApi.deptTree(siteCode).send()) || [];
|
||
// 表单中心目录(同源代理):授权没配/中心不可达时降级为"选不了",不阻塞事项其余字段的编辑
|
||
try {
|
||
const page = await svcFormApi.centerForms(siteCode, { page: 1, pageSize: 100 }).send();
|
||
fcForms.value = page?.items || [];
|
||
fcFormsDisabled.value = false;
|
||
fcFormsReason.value = "";
|
||
} catch (e: any) {
|
||
fcForms.value = [];
|
||
fcFormsDisabled.value = true;
|
||
const msg = String(e?.message || e || "");
|
||
fcFormsReason.value = msg.includes("不可达") || msg.includes("返回空")
|
||
? msg
|
||
: `表单中心目录获取失败:${msg}`;
|
||
}
|
||
try {
|
||
flows.value = ((await svcFlowApi.list(siteCode).send()) as any[]) || [];
|
||
flowDisabled.value = false;
|
||
flowDisabledReason.value = "";
|
||
} catch (e: any) {
|
||
// BP-R4-2:仅真正"流程中心未接入"(svchall.flow.enabled=false)才提示未接入;
|
||
// 站点/鉴权/网络类错误按原始信息提示,避免排障方向被误导
|
||
flowDisabled.value = true;
|
||
const msg = String(e?.message || e || "");
|
||
flowDisabledReason.value = msg.includes("流程中心未接入")
|
||
? "流程中心未接入,暂只能配置线下事项"
|
||
: `流程列表获取失败:${msg}`;
|
||
}
|
||
if (route.query.id) {
|
||
const detail: any = await svcItemApi.get(siteCode, Number(route.query.id)).send();
|
||
form.value = { ...form.value, ...detail };
|
||
attachments.value = (await svcItemApi.attList(siteCode, Number(route.query.id)).send()) || [];
|
||
}
|
||
}
|
||
|
||
async function uploadCover(file: File) {
|
||
uploadingCover.value = true;
|
||
try {
|
||
const res: any = await fileApi.upload(file, "image", currentSiteCode.value).send();
|
||
form.value.coverImage = res.fileUrl;
|
||
} finally {
|
||
uploadingCover.value = false;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
async function uploadGuide(file: File) {
|
||
const res: any = await fileApi.upload(file, "file", currentSiteCode.value).send();
|
||
attachments.value.push({ id: res.id, fileName: res.fileName, fileUrl: res.fileUrl, fileSize: res.fileSize });
|
||
return false;
|
||
}
|
||
|
||
async function save(publishAfter: boolean) {
|
||
const valid = await new Promise<boolean>(resolve => formRef.value?.validate(resolve));
|
||
if (!valid) {
|
||
Message.warning("请填写红色星号内容!");
|
||
return;
|
||
}
|
||
const payload: SvcItemSaveRequest = {
|
||
...form.value,
|
||
id: route.query.id ? Number(route.query.id) : undefined,
|
||
attachmentIds: attachments.value.map(a => a.id),
|
||
};
|
||
const id: number = await svcItemApi.save(payload).send();
|
||
if (publishAfter) {
|
||
await svcItemApi.publish(currentSiteCode.value, id).send();
|
||
Message.success("已保存并发布");
|
||
} else {
|
||
Message.success("已保存");
|
||
}
|
||
router.replace({ path: "/svc/item/edit", query: { id: String(id) } });
|
||
}
|
||
|
||
/** 流程图:走本应用后端 /api/svc/flow/diagram.png(后端换免签 token 取图)。
|
||
* 原先是 window.open 到 flow-center 前端 /#/diagram?...——跨应用页面 + 需要流程中心的登录会话;
|
||
* 现在同源出图,浏览器只认 <img src>,也不再把 flow-center 地址带进前端配置。 */
|
||
function openFlowDiagram() {
|
||
if (!currentSiteCode.value || !form.value.flowGuid) {
|
||
Message.warning("请先选择流程并确认当前站点");
|
||
return;
|
||
}
|
||
window.open(svcFlowApi.diagramUrl(currentSiteCode.value, form.value.flowGuid, "dept"), "_blank");
|
||
}
|
||
|
||
onMounted(loadAll);
|
||
</script>
|
||
|
||
<template>
|
||
<div class="page-wrapper">
|
||
<div class="main-container">
|
||
<Card :bordered="false" dis-hover>
|
||
<template #title>{{ pageTitle }}</template>
|
||
<Form ref="formRef" :model="form" :rules="saveRules" :label-width="130">
|
||
<Divider orientation="left">基本信息</Divider>
|
||
<Row :gutter="16">
|
||
<Col span="12">
|
||
<FormItem label="事项编码" prop="itemCode">
|
||
<Input v-model="form.itemCode" placeholder="字母/数字/下划线/中划线,2~64 位" />
|
||
</FormItem>
|
||
</Col>
|
||
<Col span="12">
|
||
<FormItem label="事项名称" prop="title">
|
||
<Input v-model="form.title" :maxlength="200" />
|
||
</FormItem>
|
||
</Col>
|
||
<Col span="24">
|
||
<FormItem label="一句话简介">
|
||
<Input v-model="form.summary" type="textarea" :rows="2" :maxlength="500" />
|
||
</FormItem>
|
||
</Col>
|
||
<Col span="8">
|
||
<FormItem label="服务对象">
|
||
<Select v-model="form.objectKey" clearable>
|
||
<Option v-for="d in objects" :key="d.dictKey" :value="d.dictKey">{{ d.dictName }}</Option>
|
||
</Select>
|
||
</FormItem>
|
||
</Col>
|
||
<Col span="8">
|
||
<FormItem label="事项类别">
|
||
<Select v-model="form.categoryKey" clearable>
|
||
<Option v-for="d in categories" :key="d.dictKey" :value="d.dictKey">{{ d.dictName }}</Option>
|
||
</Select>
|
||
</FormItem>
|
||
</Col>
|
||
<Col span="8">
|
||
<FormItem label="服务部门">
|
||
<Select v-model="form.deptGroupId" clearable>
|
||
<Option v-for="d in deptOptions" :key="d.id" :value="d.id">{{ d.label }}</Option>
|
||
</Select>
|
||
</FormItem>
|
||
</Col>
|
||
</Row>
|
||
|
||
<Divider orientation="left">办理信息</Divider>
|
||
<Row :gutter="16">
|
||
<Col span="8">
|
||
<FormItem label="办理方式">
|
||
<RadioGroup v-model="form.handleMode">
|
||
<Radio :label="1">线上</Radio>
|
||
<Radio :label="2">线下</Radio>
|
||
<Radio :label="3">线上线下</Radio>
|
||
</RadioGroup>
|
||
</FormItem>
|
||
</Col>
|
||
<Col span="8">
|
||
<FormItem label="可在线办理">
|
||
<Switch v-model="form.onlineFlag" :true-value="1" :false-value="0" />
|
||
</FormItem>
|
||
</Col>
|
||
<Col span="8">
|
||
<FormItem label="办理地点">
|
||
<Input v-model="form.handlePlace" :maxlength="200" />
|
||
</FormItem>
|
||
</Col>
|
||
<Col span="8">
|
||
<FormItem label="办理时限">
|
||
<Input v-model="form.handleTimeLimit" :maxlength="64" />
|
||
</FormItem>
|
||
</Col>
|
||
<Col span="8">
|
||
<FormItem label="承诺时限">
|
||
<Input v-model="form.promiseTimeLimit" :maxlength="64" />
|
||
<div class="tip">建议填纯数字(天),详情页会用它推算预计完成日期</div>
|
||
</FormItem>
|
||
</Col>
|
||
<Col span="8">
|
||
<FormItem label="咨询电话">
|
||
<Input v-model="form.contactPhone" :maxlength="64" />
|
||
</FormItem>
|
||
</Col>
|
||
<Col span="8">
|
||
<FormItem label="监督投诉电话">
|
||
<Input v-model="form.supervisePhone" :maxlength="64" />
|
||
</FormItem>
|
||
</Col>
|
||
</Row>
|
||
|
||
<div class="section-title">
|
||
<span>服务承诺</span><span class="new-tag">新增</span>
|
||
</div>
|
||
<Row :gutter="16">
|
||
<Col span="12">
|
||
<FormItem label="协办部门">
|
||
<Input v-model="form.coopDepts" :maxlength="500" placeholder="多个部门用顿号分隔" />
|
||
</FormItem>
|
||
</Col>
|
||
<Col span="12">
|
||
<FormItem label="经办人员">
|
||
<Input v-model="form.handlerName" :maxlength="64" />
|
||
</FormItem>
|
||
</Col>
|
||
<Col span="12">
|
||
<FormItem label="办理时间(时段)">
|
||
<Input v-model="form.handleTimeDesc" :maxlength="200" placeholder="工作日 08:00–11:45" />
|
||
<div class="tip">区别于办理时限,指窗口办公时段</div>
|
||
</FormItem>
|
||
</Col>
|
||
<Col span="12">
|
||
<FormItem label="跑现场次数">
|
||
<InputNumber v-model="form.runCount" :min="0" style="width: 100%" />
|
||
<div class="tip">0 = 零跑动</div>
|
||
</FormItem>
|
||
</Col>
|
||
<Col span="12">
|
||
<FormItem label="是否快递送达">
|
||
<Select v-model="form.expressFlag" clearable>
|
||
<Option :value="0">否</Option>
|
||
<Option :value="1">是(到付)</Option>
|
||
<Option :value="2">是(学校承担)</Option>
|
||
</Select>
|
||
</FormItem>
|
||
</Col>
|
||
<Col span="12">
|
||
<FormItem label="收费标准">
|
||
<Input v-model="form.feeDesc" :maxlength="200" />
|
||
</FormItem>
|
||
</Col>
|
||
<Col span="12">
|
||
<FormItem label="收费依据">
|
||
<Input v-model="form.feeBasis" :maxlength="200" />
|
||
</FormItem>
|
||
</Col>
|
||
</Row>
|
||
|
||
<Divider orientation="left">在线办理绑定</Divider>
|
||
<Row :gutter="16">
|
||
<Col span="12">
|
||
<FormItem label="办理流程">
|
||
<div class="flow-select">
|
||
<Select v-model="form.flowGuid" clearable :disabled="flowDisabled" placeholder="流程中心流程">
|
||
<Option v-for="f in flows" :key="f.flowGuid" :value="f.flowGuid">
|
||
{{ f.name }}(v{{ f.version }})
|
||
</Option>
|
||
</Select>
|
||
<Button v-if="form.flowGuid" size="small" @click="openFlowDiagram">流程图</Button>
|
||
</div>
|
||
<div v-if="flowDisabled" class="tip">{{ flowDisabledReason || "流程中心未接入,暂只能配置线下事项" }}</div>
|
||
<div v-else class="tip">可在线办理的事项须绑定流程:此处选择,或留空回退所用表单的绑定流程</div>
|
||
</FormItem>
|
||
</Col>
|
||
<Col span="12">
|
||
<FormItem label="办理表单">
|
||
<Select :value="form.fcFormUuid" clearable placeholder="从表单中心选择已发布表单"
|
||
:disabled="fcFormsDisabled" @on-change="onFcFormChange">
|
||
<Option v-for="f in fcForms" :key="f.formUuid" :value="f.formUuid">
|
||
{{ f.name }}({{ f.appCode }}/{{ f.code }} · v{{ f.version }})
|
||
</Option>
|
||
</Select>
|
||
<div v-if="fcFormsDisabled" class="tip" style="color: #ed4014">
|
||
{{ fcFormsReason || "表单中心目录不可达" }}
|
||
</div>
|
||
<div v-else-if="fcBinding" class="tip">{{ fcBinding }}</div>
|
||
<div v-else class="tip">
|
||
目录只列中心已发布、且已授权给本应用的表单;保存时服务端会再向中心核验一次坐标
|
||
</div>
|
||
<div v-if="form.formCode" class="tip" style="color: #ed4014">
|
||
历史本地表单「{{ form.formCode }}」只读保留(本地表单定义已停用,改绑请选表单中心的表)
|
||
</div>
|
||
</FormItem>
|
||
</Col>
|
||
<Col span="24">
|
||
<FormItem label="我要办理外链">
|
||
<Input v-model="form.applyUrl" placeholder="http(s):// 完整地址;填写后优先于自动生成地址" />
|
||
<div class="tip">优先级:本外链 > 办事大厅自动生成 > 不显示按钮</div>
|
||
</FormItem>
|
||
</Col>
|
||
</Row>
|
||
|
||
<Divider orientation="left">关联栏目与资产</Divider>
|
||
<Row :gutter="16">
|
||
<Col span="8">
|
||
<FormItem label="常见问题栏目">
|
||
<InputNumber v-model="form.faqCategoryId" :min="1" style="width: 100%" placeholder="CMS 栏目 id" />
|
||
</FormItem>
|
||
</Col>
|
||
<Col span="8">
|
||
<FormItem label="公告信息栏目">
|
||
<InputNumber v-model="form.noticeCategoryId" :min="1" style="width: 100%" placeholder="CMS 栏目 id" />
|
||
</FormItem>
|
||
</Col>
|
||
<Col span="8">
|
||
<FormItem label="文件规定栏目">
|
||
<InputNumber v-model="form.ruleCategoryId" :min="1" style="width: 100%" placeholder="CMS 栏目 id" />
|
||
</FormItem>
|
||
</Col>
|
||
<Col span="12">
|
||
<FormItem label="事项封面">
|
||
<div class="cover">
|
||
<img v-if="form.coverImage" :src="form.coverImage" class="cover-img" />
|
||
<Upload :before-upload="uploadCover" :show-upload-list="false" accept="image/*">
|
||
<Button :loading="uploadingCover" icon="ios-image-outline">上传封面</Button>
|
||
</Upload>
|
||
<Button v-if="form.coverImage" size="small" type="text" @click="form.coverImage = ''">移除</Button>
|
||
</div>
|
||
</FormItem>
|
||
</Col>
|
||
<Col span="6">
|
||
<FormItem label="排序值">
|
||
<InputNumber v-model="form.sortNo" style="width: 100%" />
|
||
</FormItem>
|
||
</Col>
|
||
</Row>
|
||
|
||
<Divider orientation="left">指南内容</Divider>
|
||
<FormItem label="办理流程">
|
||
<RichTextEditor v-model="form.processHtml" placeholder="办理流程说明" />
|
||
</FormItem>
|
||
<FormItem label="所需材料">
|
||
<RichTextEditor v-model="form.materialHtml" placeholder="所需材料说明" />
|
||
</FormItem>
|
||
<FormItem label="注意事项">
|
||
<RichTextEditor v-model="form.noticeHtml" placeholder="注意事项说明" />
|
||
</FormItem>
|
||
<FormItem label="指南附件">
|
||
<Upload :before-upload="uploadGuide" :show-upload-list="false">
|
||
<Button icon="ios-cloud-upload-outline">上传附件</Button>
|
||
</Upload>
|
||
<div class="att-list">
|
||
<div v-for="a in attachments" :key="a.id" class="att-item">
|
||
<a :href="a.fileUrl" target="_blank" rel="noopener">{{ a.fileName }}</a>
|
||
<Button size="small" type="text" style="color: #ed4014"
|
||
@click="attachments = attachments.filter(x => x.id !== a.id)">移除</Button>
|
||
</div>
|
||
</div>
|
||
</FormItem>
|
||
|
||
<div class="actions">
|
||
<Button type="primary" :loading="false" @click="save(false)">保存</Button>
|
||
<Button type="success" @click="save(true)">保存并发布</Button>
|
||
<Button @click="router.back()">返回</Button>
|
||
</div>
|
||
</Form>
|
||
</Card>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.section-title {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin: 24px 0 16px;
|
||
padding-bottom: 8px;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
color: #17233d;
|
||
border-bottom: 1px solid #e8eaec;
|
||
}
|
||
.new-tag {
|
||
padding: 0 8px;
|
||
font-size: 12px;
|
||
font-weight: 400;
|
||
line-height: 18px;
|
||
color: #808695;
|
||
border: 1px solid #dcdee2;
|
||
border-radius: 9px;
|
||
}
|
||
.flow-select {
|
||
display: flex;
|
||
gap: 8px;
|
||
align-items: center;
|
||
}
|
||
.tip {
|
||
font-size: 12px;
|
||
color: #999;
|
||
}
|
||
.cover {
|
||
display: flex;
|
||
gap: 8px;
|
||
align-items: center;
|
||
}
|
||
.cover-img {
|
||
height: 48px;
|
||
border-radius: 4px;
|
||
}
|
||
.att-list {
|
||
margin-top: 8px;
|
||
}
|
||
.att-item {
|
||
display: flex;
|
||
gap: 8px;
|
||
align-items: center;
|
||
}
|
||
.actions {
|
||
margin-top: 16px;
|
||
display: flex;
|
||
gap: 8px;
|
||
}
|
||
</style>
|