feat: 办事大厅前端源码首次入库(事项列表/详情/申报/管理端)
build / build (push) Successful in 1m55s

This commit is contained in:
chenxing
2026-09-15 11:00:57 +08:00
parent 7a2ea27f1e
commit 22dfb70377
328 changed files with 26089 additions and 0 deletions
+348
View File
@@ -0,0 +1,348 @@
<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 { SvcDeptVo, SvcFormVo, SvcItemAttachmentVo, SvcItemSaveRequest } from "@/types/svc";
import config from "@/assets/config/host.config";
import { useSite } from "@/utils/site";
import RichTextEditor from "@/components/RichTextEditor.vue";
const route = useRoute();
const router = useRouter();
const { currentSiteCode, loadSites } = useSite();
const form = ref<SvcItemSaveRequest & Record<string, any>>({
siteCode: "",
itemCode: "",
title: "",
handleMode: 1,
onlineFlag: 1,
sortNo: 0,
});
const objects = ref<{ dictKey: string; dictName: string }[]>([]);
const categories = ref<{ dictKey: string; dictName: string }[]>([]);
const depts = ref<SvcDeptVo[]>([]);
const forms = ref<SvcFormVo[]>([]);
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 ? "编辑事项" : "新建事项"));
/** 部门树扁平化(缩进展示,承袭 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()) || [];
forms.value = ((await svcFormApi.list(siteCode).send()) as SvcFormVo[]) || [];
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 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) } });
}
/** 流程图:flow-center 深链整页跳转(事项库场景用科室视图) */
function openFlowDiagram() {
window.open(`${config.flowHost}/#/diagram?flowGuid=${form.value.flowGuid}&mode=dept`, "_blank");
}
onMounted(loadAll);
</script>
<template>
<div class="page">
<Card :bordered="false" dis-hover>
<template #title>{{ pageTitle }}</template>
<Form :label-width="130">
<Divider orientation="left">基本信息</Divider>
<Row :gutter="16">
<Col span="12">
<FormItem label="事项编码" required>
<Input v-model="form.itemCode" placeholder="字母/数字/下划线/中划线,2~64 位" />
</FormItem>
</Col>
<Col span="12">
<FormItem label="事项名称" required>
<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" />
</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>
<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 v-model="form.formCode" clearable placeholder="该事项发起时填写的自定义表单">
<Option v-for="f in forms" :key="f.code" :value="f.code">{{ f.name }}</Option>
</Select>
</FormItem>
</Col>
<Col span="24">
<FormItem label="我要办理外链">
<Input v-model="form.applyUrl" placeholder="http(s):// 完整地址;填写后优先于自动生成地址" />
<div class="tip">优先级:本外链 &gt; 办事大厅自动生成 &gt; 不显示按钮</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>
</template>
<style scoped>
.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>