This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
<script setup lang="ts">
|
||||
/** 在线办理发起页(CMS 指南页"我要办理"跳入:/#/apply?siteCode=&itemCode=)。 */
|
||||
import { computed, onMounted, reactive, ref } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { Message } from "view-ui-plus";
|
||||
import { applyApi } from "@/api/module/svc";
|
||||
import { useSite } from "@/utils/site";
|
||||
import DynamicForm from "@/components/DynamicForm.vue";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { setSite } = useSite();
|
||||
|
||||
const loading = ref(false);
|
||||
const submitting = ref(false);
|
||||
const item = ref<Record<string, any> | null>(null);
|
||||
const loadError = ref("");
|
||||
const formData = reactive<Record<string, any>>({});
|
||||
|
||||
const schemaJson = computed(() => (item.value?.schemaJson as string) || "");
|
||||
|
||||
async function load() {
|
||||
const siteCode = String(route.query.siteCode || "");
|
||||
const itemCode = String(route.query.itemCode || "");
|
||||
if (!siteCode || !itemCode) {
|
||||
Message.error("缺少 siteCode/itemCode 参数");
|
||||
return;
|
||||
}
|
||||
setSite(siteCode);
|
||||
loading.value = true;
|
||||
try {
|
||||
item.value = (await applyApi.item(siteCode, itemCode).send()) as any;
|
||||
loadError.value = "";
|
||||
} catch (e: any) {
|
||||
// 区分加载失败与业务性不可办理(走查 §七.8):网络/服务异常不当成"事项不可办理"
|
||||
loadError.value = String(e?.message || e || "网络或服务异常");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
if (!item.value) return;
|
||||
submitting.value = true;
|
||||
try {
|
||||
const apply: any = await applyApi
|
||||
.submit({
|
||||
siteCode: String(route.query.siteCode),
|
||||
itemCode: String(route.query.itemCode),
|
||||
formData: Object.keys(formData).length ? JSON.stringify(formData) : undefined,
|
||||
})
|
||||
.send();
|
||||
if (apply.flowStatus === 0) {
|
||||
Message.warning("流程中心暂不可用,已保存为待提交,可稍后在「我的办件」中重试");
|
||||
} else {
|
||||
Message.success("已提交,进入审批流程");
|
||||
}
|
||||
router.replace(`/apply-detail?id=${apply.id}`);
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(load);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<Card :bordered="false" dis-hover v-if="item">
|
||||
<template #title>在线办理 · {{ item.title }}</template>
|
||||
<div class="summary" v-if="item.summary || item.deptName || item.handleTimeLimit">
|
||||
<div v-if="item.summary">{{ item.summary }}</div>
|
||||
<div class="meta">
|
||||
<span v-if="item.deptName">办理部门:{{ item.deptName }}</span>
|
||||
<span v-if="item.handleTimeLimit">办理时限:{{ item.handleTimeLimit }}</span>
|
||||
<span v-if="item.contactPhone">咨询电话:{{ item.contactPhone }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<Divider v-if="schemaJson" orientation="left">申请表单</Divider>
|
||||
<DynamicForm v-if="schemaJson" :schema-json="schemaJson" v-model:form-data="formData" />
|
||||
<div v-else class="no-form">该事项无需填写表单,直接提交申请材料请联系办理部门。</div>
|
||||
<div class="actions">
|
||||
<Button type="primary" :loading="submitting" @click="submit">提交申请</Button>
|
||||
<Button @click="router.push('/my')">我的办件</Button>
|
||||
</div>
|
||||
</Card>
|
||||
<Card v-else :bordered="false" dis-hover>
|
||||
<div style="text-align: center; padding: 40px; color: #999">
|
||||
<template v-if="loading">加载中…</template>
|
||||
<template v-else-if="loadError">
|
||||
事项加载失败:{{ loadError }}(网络或服务异常,请稍后重试)
|
||||
</template>
|
||||
<template v-else>事项不可办理(未发布或未开通在线办理)</template>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.summary {
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
.meta {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
color: #888;
|
||||
font-size: 13px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.no-form {
|
||||
color: #999;
|
||||
padding: 12px 0;
|
||||
}
|
||||
.actions {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,177 @@
|
||||
<script setup lang="ts">
|
||||
/** 办件详情:快照回显 + 镜像状态 + 结果意见;撤回/前往流程中心直调 flow-center(同主域 CAS)。 */
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { Message, Modal } from "view-ui-plus";
|
||||
import { applyApi } from "@/api/module/svc";
|
||||
import config from "@/assets/config/host.config";
|
||||
import type { ApplyVo } from "@/types/svc";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const apply = ref<ApplyVo | null>(null);
|
||||
const withdrawing = ref(false);
|
||||
|
||||
const STATUS: Record<number, { text: string; color: string }> = {
|
||||
0: { text: "待提交", color: "default" },
|
||||
10: { text: "审批中", color: "primary" },
|
||||
20: { text: "已完成", color: "success" },
|
||||
30: { text: "已拒绝", color: "error" },
|
||||
40: { text: "已撤回", color: "warning" },
|
||||
};
|
||||
|
||||
const snapshot = computed<Record<string, any>>(() => {
|
||||
try {
|
||||
return JSON.parse(apply.value?.itemSnapshotJson || "{}");
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
});
|
||||
|
||||
const formData = computed<Record<string, any>>(() => {
|
||||
try {
|
||||
return JSON.parse(apply.value?.formDataJson || "{}");
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
});
|
||||
|
||||
const result = computed<Record<string, any>>(() => {
|
||||
try {
|
||||
return JSON.parse(apply.value?.flowResultJson || "{}");
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
});
|
||||
|
||||
async function load() {
|
||||
const id = Number(route.query.id || 0);
|
||||
if (id) {
|
||||
apply.value = (await applyApi.detail(id).send()) as any;
|
||||
}
|
||||
}
|
||||
|
||||
/** 前往流程中心:flowHost 完整地址(不再依赖部署层反代 /flow-center/,BP-R3-3)
|
||||
* 路由段须与 flow-center 前端一致(routes.ts path="/my" 我的办件;/#/my-instances 会 no-match 空白页) */
|
||||
const flowCenterUrl = computed(() =>
|
||||
config.flowHost ? `${config.flowHost}/#/my` : ""
|
||||
);
|
||||
|
||||
/** 查看流程图:flow-center 示意图深链(办件 instanceId + 科室视图,整页跳转不受 CORS 约束) */
|
||||
function openFlowDiagram() {
|
||||
const instanceId = apply.value?.flowInstanceId;
|
||||
if (!instanceId || !config.flowHost) {
|
||||
Message.warning("流程中心地址未配置");
|
||||
return;
|
||||
}
|
||||
window.open(`${config.flowHost}/#/diagram?instanceId=${instanceId}&mode=dept`, "_blank");
|
||||
}
|
||||
|
||||
/** 撤回:直调 flow-center 用户面(BP-R2-1;同主域 CAS 免二次登录,本地 dev 走 flowHost CORS) */
|
||||
async function withdraw() {
|
||||
const instanceId = apply.value?.flowInstanceId;
|
||||
if (!instanceId || !config.flowHost) {
|
||||
Message.warning("流程中心地址未配置,无法撤回");
|
||||
return;
|
||||
}
|
||||
withdrawing.value = true;
|
||||
try {
|
||||
const resp = await fetch(`${config.flowHost}/api/flow/instance/${instanceId}/withdraw`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
});
|
||||
const res = await resp.json().catch(() => null);
|
||||
if (resp.ok && res && res.code === 0) {
|
||||
Message.success("已撤回,可重新发起或编辑后重提");
|
||||
load();
|
||||
} else {
|
||||
Message.error(res?.msg || "撤回失败(已有任务被办理时不可撤回)");
|
||||
}
|
||||
} catch (e: any) {
|
||||
Message.error("流程中心不可达:" + (e.message || e));
|
||||
} finally {
|
||||
withdrawing.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 一键重提(30/40 态;同 bizId R4 重启,表单数据沿用原填报) */
|
||||
function resubmit() {
|
||||
if (!apply.value) return;
|
||||
Modal.confirm({
|
||||
title: "重新提交确认",
|
||||
content: `将以原填报内容重新提交「${apply.value.itemTitle}」,审批轨迹跨轮连续保留。确认继续?`,
|
||||
onOk: async () => {
|
||||
await applyApi.resubmit(apply.value!.id).send();
|
||||
Message.success("已重新提交,进入审批流程");
|
||||
load();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(load);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<Card :bordered="false" dis-hover v-if="apply">
|
||||
<template #title>
|
||||
办件详情 #{{ apply.id }}
|
||||
<Tag :color="STATUS[apply.flowStatus]?.color || 'default'">
|
||||
{{ STATUS[apply.flowStatus]?.text || apply.flowStatus }}
|
||||
</Tag>
|
||||
</template>
|
||||
<Descriptions :column="2" border>
|
||||
<DescriptionsItem label="事项">{{ apply.itemTitle }}</DescriptionsItem>
|
||||
<DescriptionsItem label="事项编码">{{ apply.itemCode }}</DescriptionsItem>
|
||||
<DescriptionsItem label="提交人">{{ apply.submitterName }}</DescriptionsItem>
|
||||
<DescriptionsItem label="提交时间">{{ apply.submitTime || "—" }}</DescriptionsItem>
|
||||
<DescriptionsItem label="办理部门">{{ snapshot.deptName || "—" }}</DescriptionsItem>
|
||||
<DescriptionsItem label="办理时限">{{ snapshot.handleTimeLimit || "—" }}</DescriptionsItem>
|
||||
</Descriptions>
|
||||
|
||||
<template v-if="apply.formDataJson">
|
||||
<Divider orientation="left">申请内容</Divider>
|
||||
<div class="kv" v-for="(v, k) in formData" :key="String(k)">
|
||||
<span class="k">{{ k }}</span>
|
||||
<span class="v">{{ Array.isArray(v) ? v.map((f: any) => f.fileName || f).join("、") : v }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="result.opinion || result.result">
|
||||
<Divider orientation="left">审批结果</Divider>
|
||||
<p>结果:{{ result.result || "—" }}</p>
|
||||
<p v-if="result.opinion">意见:{{ result.opinion }}</p>
|
||||
</template>
|
||||
|
||||
<div class="actions">
|
||||
<Button @click="router.push('/my')">返回我的办件</Button>
|
||||
<Button v-if="apply.flowInstanceId" @click="openFlowDiagram">查看流程图</Button>
|
||||
<Button v-if="apply.flowStatus === 10 && apply.flowInstanceId" :loading="withdrawing"
|
||||
@click="withdraw">撤回</Button>
|
||||
<Button v-if="apply.flowStatus === 30 || apply.flowStatus === 40" @click="resubmit">重新提交</Button>
|
||||
<Button v-if="flowCenterUrl && apply.flowInstanceId" type="primary" target="_blank"
|
||||
:href="flowCenterUrl">前往流程中心查看进度</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.kv {
|
||||
display: flex;
|
||||
padding: 6px 0;
|
||||
border-bottom: 1px dashed #eee;
|
||||
}
|
||||
.k {
|
||||
width: 220px;
|
||||
color: #888;
|
||||
}
|
||||
.v {
|
||||
flex: 1;
|
||||
}
|
||||
.actions {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,100 @@
|
||||
<script setup lang="ts">
|
||||
/** 办件记录(管理面,modId 26):本校在途与历史办件分页。 */
|
||||
import { onMounted, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { applyApi } from "@/api/module/svc";
|
||||
import type { ApplyVo } from "@/types/svc";
|
||||
import { useSite } from "@/utils/site";
|
||||
|
||||
const router = useRouter();
|
||||
const { currentSiteCode, sites, loadSites, setSite } = useSite();
|
||||
const loading = ref(false);
|
||||
const list = ref<ApplyVo[]>([]);
|
||||
const total = ref(0);
|
||||
const page = ref(1);
|
||||
const flowStatus = ref<number | undefined>(undefined);
|
||||
const keyword = ref("");
|
||||
|
||||
const STATUS: Record<number, { text: string; color: string }> = {
|
||||
0: { text: "待提交", color: "default" },
|
||||
10: { text: "审批中", color: "primary" },
|
||||
20: { text: "已完成", color: "success" },
|
||||
30: { text: "已拒绝", color: "error" },
|
||||
40: { text: "已撤回", color: "warning" },
|
||||
};
|
||||
|
||||
const cols = [
|
||||
{ title: "办件号", key: "id", width: 90 },
|
||||
{ title: "事项", key: "itemTitle", minWidth: 180 },
|
||||
{ title: "编码", key: "itemCode", width: 130 },
|
||||
{ title: "提交人", key: "submitterName", width: 100 },
|
||||
{ title: "提交时间", key: "submitTime", width: 160 },
|
||||
{ title: "状态", slot: "status", width: 100 },
|
||||
{ title: "操作", slot: "action", width: 100 },
|
||||
];
|
||||
|
||||
async function load() {
|
||||
if (!currentSiteCode.value) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
const res: any = await applyApi
|
||||
.adminPage({
|
||||
siteCode: currentSiteCode.value,
|
||||
flowStatus: flowStatus.value,
|
||||
keyword: keyword.value,
|
||||
page: page.value,
|
||||
pageSize: 10,
|
||||
})
|
||||
.send();
|
||||
total.value = res.total || 0;
|
||||
list.value = res.list || [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadSites();
|
||||
load();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<Card :bordered="false" dis-hover>
|
||||
<div class="bar">
|
||||
<Select :model-value="currentSiteCode" style="width: 240px" @on-change="(c: string) => { setSite(c); page = 1; load(); }">
|
||||
<Option v-for="s in sites" :key="s.siteCode" :value="s.siteCode">{{ s.name }}({{ s.siteCode }})</Option>
|
||||
</Select>
|
||||
<Select v-model="flowStatus" clearable placeholder="状态" style="width: 140px" @on-change="() => { page = 1; load(); }">
|
||||
<Option v-for="(v, k) in STATUS" :key="k" :value="Number(k)">{{ v.text }}</Option>
|
||||
</Select>
|
||||
<Input v-model="keyword" search enter-button placeholder="事项/提交人" style="width: 220px" @on-search="load" />
|
||||
</div>
|
||||
<Table :loading="loading" :columns="cols" :data="list">
|
||||
<template #status="{ row }">
|
||||
<Tag :color="STATUS[row.flowStatus]?.color || 'default'">
|
||||
{{ STATUS[row.flowStatus]?.text || row.flowStatus }}
|
||||
</Tag>
|
||||
</template>
|
||||
<template #action="{ row }">
|
||||
<Button size="small" type="text" @click="router.push(`/apply-detail?id=${row.id}`)">详情</Button>
|
||||
</template>
|
||||
</Table>
|
||||
<Page class="pager" :total="total" :current="page" :page-size="10" show-total
|
||||
@on-change="(p: number) => { page = p; load(); }" />
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.bar {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.pager {
|
||||
margin-top: 12px;
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,133 @@
|
||||
<script setup lang="ts">
|
||||
/** 我的办件(modId 27):镜像状态列表;撤回直调流程中心(同主域 CAS),审批轨迹后续接。 */
|
||||
import { onMounted, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { Message, Modal } from "view-ui-plus";
|
||||
import { applyApi } from "@/api/module/svc";
|
||||
import config from "@/assets/config/host.config";
|
||||
import type { ApplyVo } from "@/types/svc";
|
||||
|
||||
const router = useRouter();
|
||||
const loading = ref(false);
|
||||
const list = ref<ApplyVo[]>([]);
|
||||
const total = ref(0);
|
||||
const page = ref(1);
|
||||
const flowStatus = ref<number | undefined>(undefined);
|
||||
const withdrawingId = ref<number>(0);
|
||||
|
||||
const STATUS: Record<number, { text: string; color: string }> = {
|
||||
0: { text: "待提交", color: "default" },
|
||||
10: { text: "审批中", color: "primary" },
|
||||
20: { text: "已完成", color: "success" },
|
||||
30: { text: "已拒绝", color: "error" },
|
||||
40: { text: "已撤回", color: "warning" },
|
||||
};
|
||||
|
||||
const cols = [
|
||||
{ title: "办件号", key: "id", width: 90 },
|
||||
{ title: "事项", key: "itemTitle", minWidth: 200 },
|
||||
{ title: "提交人", key: "submitterName", width: 110 },
|
||||
{ title: "提交时间", key: "submitTime", width: 160 },
|
||||
{ title: "状态", slot: "status", width: 100 },
|
||||
{ title: "操作", slot: "action", width: 220 },
|
||||
];
|
||||
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res: any = await applyApi.my({ flowStatus: flowStatus.value, page: page.value, pageSize: 10 }).send();
|
||||
total.value = res.total || 0;
|
||||
list.value = res.list || [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function retry(row: ApplyVo) {
|
||||
await applyApi.retry(row.id).send();
|
||||
Message.success("已重新提交流程中心");
|
||||
load();
|
||||
}
|
||||
|
||||
/** 一键重提(30/40 态;同 bizId R4 重启,表单数据沿用原填报) */
|
||||
async function resubmit(row: ApplyVo) {
|
||||
Modal.confirm({
|
||||
title: "重新提交确认",
|
||||
content: `将以原填报内容重新提交「${row.itemTitle}」,审批轨迹跨轮连续保留。确认继续?`,
|
||||
onOk: async () => {
|
||||
await applyApi.resubmit(row.id).send();
|
||||
Message.success("已重新提交,进入审批流程");
|
||||
load();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** 撤回:直调 flow-center 用户面(BP-R2-1;同主域 CAS 免二次登录,本地 dev 走 flowHost CORS) */
|
||||
async function withdraw(row: ApplyVo) {
|
||||
if (!row.flowInstanceId || !config.flowHost) {
|
||||
Message.warning("流程中心地址未配置,无法撤回");
|
||||
return;
|
||||
}
|
||||
withdrawingId.value = row.id;
|
||||
try {
|
||||
const resp = await fetch(`${config.flowHost}/api/flow/instance/${row.flowInstanceId}/withdraw`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
});
|
||||
const res = await resp.json().catch(() => null);
|
||||
if (resp.ok && res && res.code === 0) {
|
||||
Message.success("已撤回,可重新发起或编辑后重提");
|
||||
load();
|
||||
} else {
|
||||
Message.error(res?.msg || "撤回失败(已有任务被办理时不可撤回)");
|
||||
}
|
||||
} catch (e: any) {
|
||||
Message.error("流程中心不可达:" + (e.message || e));
|
||||
} finally {
|
||||
withdrawingId.value = 0;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(load);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<Card :bordered="false" dis-hover>
|
||||
<div class="bar">
|
||||
<Select v-model="flowStatus" clearable placeholder="状态" style="width: 140px" @on-change="() => { page = 1; load(); }">
|
||||
<Option v-for="(v, k) in STATUS" :key="k" :value="Number(k)">{{ v.text }}</Option>
|
||||
</Select>
|
||||
</div>
|
||||
<Table :loading="loading" :columns="cols" :data="list">
|
||||
<template #status="{ row }">
|
||||
<Tag :color="STATUS[row.flowStatus]?.color || 'default'">
|
||||
{{ STATUS[row.flowStatus]?.text || row.flowStatus }}
|
||||
</Tag>
|
||||
</template>
|
||||
<template #action="{ row }">
|
||||
<Button size="small" type="text" @click="router.push(`/apply-detail?id=${row.id}`)">详情</Button>
|
||||
<Button v-if="row.flowStatus === 0" size="small" type="text" @click="retry(row)">重试提交</Button>
|
||||
<Button v-if="row.flowStatus === 10 && row.flowInstanceId" size="small" type="text"
|
||||
:loading="withdrawingId === row.id" @click="withdraw(row)">撤回</Button>
|
||||
<Button v-if="row.flowStatus === 30 || row.flowStatus === 40" size="small" type="text"
|
||||
@click="resubmit(row)">重新提交</Button>
|
||||
</template>
|
||||
</Table>
|
||||
<Page class="pager" :total="total" :current="page" :page-size="10" show-total
|
||||
@on-change="(p: number) => { page = p; load(); }" />
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.bar {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.pager {
|
||||
margin-top: 12px;
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,131 @@
|
||||
<script setup lang="ts">
|
||||
/** 服务字典管理(modId 24):object/category 双 Tab,引用守护。 */
|
||||
import { onMounted, ref } from "vue";
|
||||
import { Message, Modal } from "view-ui-plus";
|
||||
import { svcDictApi } from "@/api/module/svc";
|
||||
import type { SvcDictVo } from "@/types/svc";
|
||||
import { useSite } from "@/utils/site";
|
||||
|
||||
const { currentSiteCode, sites, loadSites, setSite } = useSite();
|
||||
const tab = ref<"object" | "category">("object");
|
||||
const loading = ref(false);
|
||||
const list = ref<SvcDictVo[]>([]);
|
||||
const modal = ref(false);
|
||||
const saving = ref(false);
|
||||
const form = ref<any>({});
|
||||
|
||||
const cols = [
|
||||
{ title: "键", key: "dictKey", width: 160 },
|
||||
{ title: "名称", key: "dictName", minWidth: 160 },
|
||||
{ title: "排序", key: "sortNo", width: 80 },
|
||||
{ title: "被引用", key: "referenced", width: 90 },
|
||||
{ title: "状态", slot: "status", width: 90 },
|
||||
{ title: "操作", slot: "action", width: 140 },
|
||||
];
|
||||
|
||||
async function load() {
|
||||
if (!currentSiteCode.value) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
list.value = (await svcDictApi.list(currentSiteCode.value, tab.value).send()) || [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function open(row?: SvcDictVo) {
|
||||
form.value = row
|
||||
? { ...row }
|
||||
: { siteCode: currentSiteCode.value, dictType: tab.value, dictKey: "", dictName: "", sortNo: 0, status: 1 };
|
||||
modal.value = true;
|
||||
}
|
||||
|
||||
async function save() {
|
||||
saving.value = true;
|
||||
try {
|
||||
await svcDictApi.save(form.value).send();
|
||||
Message.success("已保存");
|
||||
modal.value = false;
|
||||
load();
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function remove(row: SvcDictVo) {
|
||||
Modal.confirm({
|
||||
title: "删除确认",
|
||||
content: `确定删除字典「${row.dictName}」吗?被引用的删除会被拒绝。`,
|
||||
onOk: async () => {
|
||||
await svcDictApi.delete(currentSiteCode.value, row.id).send();
|
||||
Message.success("已删除");
|
||||
load();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadSites();
|
||||
load();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<Card :bordered="false" dis-hover>
|
||||
<div class="bar">
|
||||
<Select :model-value="currentSiteCode" style="width: 240px" @on-change="(c: string) => { setSite(c); load(); }">
|
||||
<Option v-for="s in sites" :key="s.siteCode" :value="s.siteCode">{{ s.name }}({{ s.siteCode }})</Option>
|
||||
</Select>
|
||||
<Button type="primary" @click="open()">新建{{ tab === "object" ? "服务对象" : "事项类别" }}</Button>
|
||||
</div>
|
||||
<Tabs v-model="tab" @on-click="() => load()">
|
||||
<TabPane label="服务对象" name="object" />
|
||||
<TabPane label="事项类别" name="category" />
|
||||
</Tabs>
|
||||
<Table :loading="loading" :columns="cols" :data="list">
|
||||
<template #status="{ row }">
|
||||
<Tag :color="row.status === 1 ? 'success' : 'default'">{{ row.status === 1 ? "启用" : "停用" }}</Tag>
|
||||
</template>
|
||||
<template #action="{ row }">
|
||||
<Button size="small" type="text" @click="open(row)">编辑</Button>
|
||||
<Button size="small" type="text" style="color: #ed4014" @click="remove(row)">删除</Button>
|
||||
</template>
|
||||
</Table>
|
||||
</Card>
|
||||
|
||||
<Modal v-model="modal" :title="form.id ? '编辑字典' : '新建字典'">
|
||||
<Form :label-width="90">
|
||||
<FormItem label="字典键" required>
|
||||
<Input v-model="form.dictKey" :disabled="!!form.id && (form.referenced || 0) > 0" placeholder="字母/数字/下划线" />
|
||||
<div v-if="(form.referenced || 0) > 0" class="tip">已被 {{ form.referenced }} 个事项引用,仅可改名称</div>
|
||||
</FormItem>
|
||||
<FormItem label="名称" required>
|
||||
<Input v-model="form.dictName" :maxlength="128" />
|
||||
</FormItem>
|
||||
<FormItem label="排序">
|
||||
<InputNumber v-model="form.sortNo" style="width: 100%" />
|
||||
</FormItem>
|
||||
<FormItem label="状态">
|
||||
<Switch v-model="form.status" :true-value="1" :false-value="0" />
|
||||
</FormItem>
|
||||
</Form>
|
||||
<template #footer>
|
||||
<Button @click="modal = false">取消</Button>
|
||||
<Button type="primary" :loading="saving" @click="save">保存</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.bar {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.tip {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,170 @@
|
||||
<script setup lang="ts">
|
||||
/** 表单管理(modId 25):列表/JSON 编辑(发布版保存=自动升版本)/发布/删除/版本轨迹。 */
|
||||
import { onMounted, ref } from "vue";
|
||||
import { Message, Modal } from "view-ui-plus";
|
||||
import { svcFormApi } from "@/api/module/svc";
|
||||
import type { SvcFormVo } from "@/types/svc";
|
||||
import { useSite } from "@/utils/site";
|
||||
|
||||
const { currentSiteCode, sites, loadSites, setSite } = useSite();
|
||||
const loading = ref(false);
|
||||
const list = ref<SvcFormVo[]>([]);
|
||||
const modal = ref(false);
|
||||
const versionsModal = ref(false);
|
||||
const saving = ref(false);
|
||||
const form = ref<any>({ schemaJson: '{\n "groups": [\n { "title": "基本信息", "fields": [\n { "key": "reason", "label": "申请事由", "type": "textarea", "required": true }\n ] }\n ]\n}' });
|
||||
const versionRows = ref<SvcFormVo[]>([]);
|
||||
|
||||
const statusName = (s: number) => (s === 1 ? "已发布" : s === 2 ? "停用" : "草稿");
|
||||
const statusColor = (s: number) => (s === 1 ? "success" : "default");
|
||||
|
||||
async function load() {
|
||||
if (!currentSiteCode.value) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
list.value = (await svcFormApi.list(currentSiteCode.value).send()) || [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function open(row?: SvcFormVo) {
|
||||
if (row) {
|
||||
form.value = { id: row.id, code: row.code, name: row.name, schemaJson: row.schemaJson };
|
||||
} else {
|
||||
form.value = { code: "", name: "", schemaJson: form.value.schemaJson };
|
||||
}
|
||||
modal.value = true;
|
||||
}
|
||||
|
||||
async function save() {
|
||||
// 客户端先过一遍 JSON 合法性(服务端还有契约静态校验)
|
||||
try {
|
||||
JSON.parse(form.value.schemaJson);
|
||||
} catch {
|
||||
Message.error("表单结构不是合法 JSON");
|
||||
return;
|
||||
}
|
||||
saving.value = true;
|
||||
try {
|
||||
await svcFormApi.save(currentSiteCode.value, form.value).send();
|
||||
Message.success("已保存(已发布版本会自动生成新草稿版本)");
|
||||
modal.value = false;
|
||||
load();
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function publish(row: SvcFormVo) {
|
||||
await svcFormApi.publish(currentSiteCode.value, row.id).send();
|
||||
Message.success("已发布");
|
||||
load();
|
||||
}
|
||||
|
||||
function remove(row: SvcFormVo) {
|
||||
Modal.confirm({
|
||||
title: "删除确认",
|
||||
content: `确定删除表单「${row.name}」的全部版本吗?被事项绑定的删除会被拒绝。`,
|
||||
onOk: async () => {
|
||||
await svcFormApi.delete(currentSiteCode.value, row.code).send();
|
||||
Message.success("已删除");
|
||||
load();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function showVersions(row: SvcFormVo) {
|
||||
versionRows.value = (await svcFormApi.versions(currentSiteCode.value, row.code).send()) || [];
|
||||
versionsModal.value = true;
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadSites();
|
||||
load();
|
||||
});
|
||||
|
||||
const cols = [
|
||||
{ title: "编码", key: "code", width: 160 },
|
||||
{ title: "名称", key: "name", minWidth: 140 },
|
||||
{ title: "最新版本", key: "version", width: 90 },
|
||||
{ title: "状态", slot: "status", width: 90 },
|
||||
{ title: "被绑定", slot: "referenced", width: 90 },
|
||||
{ title: "发布时间", key: "publishTime", width: 160 },
|
||||
{ title: "操作", slot: "action", width: 220 },
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<Card :bordered="false" dis-hover>
|
||||
<div class="bar">
|
||||
<Select :model-value="currentSiteCode" style="width: 240px" @on-change="(c: string) => { setSite(c); load(); }">
|
||||
<Option v-for="s in sites" :key="s.siteCode" :value="s.siteCode">{{ s.name }}({{ s.siteCode }})</Option>
|
||||
</Select>
|
||||
<Button type="primary" @click="open()">新建表单</Button>
|
||||
</div>
|
||||
<Table :loading="loading" :columns="cols" :data="list">
|
||||
<template #status="{ row }">
|
||||
<Tag :color="statusColor(row.status)">{{ statusName(row.status) }}</Tag>
|
||||
</template>
|
||||
<template #referenced="{ row }">
|
||||
<Tag v-if="row.referenced" color="blue">已绑定事项</Tag>
|
||||
<span v-else>—</span>
|
||||
</template>
|
||||
<template #action="{ row }">
|
||||
<Button size="small" type="text" @click="open(row)">编辑</Button>
|
||||
<Button v-if="row.status === 0" size="small" type="text" @click="publish(row)">发布</Button>
|
||||
<Button size="small" type="text" @click="showVersions(row)">版本</Button>
|
||||
<Button size="small" type="text" style="color: #ed4014" @click="remove(row)">删除</Button>
|
||||
</template>
|
||||
</Table>
|
||||
</Card>
|
||||
|
||||
<Modal v-model="modal" width="720" :title="form.id ? `编辑表单(已发布版本将自动升版)` : '新建表单'">
|
||||
<Form :label-width="90">
|
||||
<FormItem label="表单编码" required>
|
||||
<Input v-model="form.code" :disabled="!!form.id" placeholder="字母/数字/下划线/中划线" />
|
||||
</FormItem>
|
||||
<FormItem label="表单名称" required>
|
||||
<Input v-model="form.name" :maxlength="128" />
|
||||
</FormItem>
|
||||
<FormItem label="表单结构" required>
|
||||
<Input v-model="form.schemaJson" type="textarea" :rows="14" />
|
||||
<div class="tip">
|
||||
契约:{"groups":[{title, fields:[{key,label,type,required,pattern,min,max,maxLength,options,accept,maxSize,maxFiles,…}]}]}
|
||||
;一期控件:text/textarea/number/date/select/radio/upload
|
||||
</div>
|
||||
</FormItem>
|
||||
</Form>
|
||||
<template #footer>
|
||||
<Button @click="modal = false">取消</Button>
|
||||
<Button type="primary" :loading="saving" @click="save">保存</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
|
||||
<Modal v-model="versionsModal" title="版本轨迹">
|
||||
<Table size="small" :columns="[
|
||||
{ title: '版本', key: 'version', width: 80 },
|
||||
{ title: '状态', key: 'status', width: 80 },
|
||||
{ title: '发布时间', key: 'publishTime' },
|
||||
]" :data="versionRows" />
|
||||
<template #footer>
|
||||
<Button @click="versionsModal = false">关闭</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.bar {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.tip {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
line-height: 1.6;
|
||||
}
|
||||
</style>
|
||||
@@ -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">优先级:本外链 > 办事大厅自动生成 > 不显示按钮</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>
|
||||
@@ -0,0 +1,168 @@
|
||||
<script setup lang="ts">
|
||||
/** 办事事项列表(modId 23):筛选/发布/下架/删除/同步部门名。 */
|
||||
import { h, onMounted, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { Message, Modal } from "view-ui-plus";
|
||||
import { svcItemApi } from "@/api/module/svc";
|
||||
import type { SvcItemPageItem } from "@/types/svc";
|
||||
import { useSite } from "@/utils/site";
|
||||
|
||||
const router = useRouter();
|
||||
const { currentSiteCode, sites, loadSites, setSite } = useSite();
|
||||
const loading = ref(false);
|
||||
const list = ref<SvcItemPageItem[]>([]);
|
||||
const total = ref(0);
|
||||
const page = ref(1);
|
||||
const pageSize = ref(10);
|
||||
const status = ref<number | undefined>(undefined);
|
||||
const keyword = ref("");
|
||||
|
||||
const statusName = (s: number) => (s === 1 ? "已发布" : s === 2 ? "已下架" : "草稿");
|
||||
const statusColor = (s: number) => (s === 1 ? "success" : s === 2 ? "warning" : "default");
|
||||
const handleModeName = (m: number) => (m === 1 ? "线上" : m === 2 ? "线下" : "线上线下");
|
||||
|
||||
const columns = [
|
||||
{ title: "编码", key: "itemCode", width: 140 },
|
||||
{ title: "名称", key: "title", minWidth: 200 },
|
||||
{ title: "类别", key: "categoryName", width: 110 },
|
||||
{ title: "部门", key: "deptName", width: 130 },
|
||||
{
|
||||
title: "办理方式",
|
||||
width: 90,
|
||||
render: (h: any, p: any) => h("span", handleModeName(p.row.handleMode)),
|
||||
},
|
||||
{ title: "在线办理", slot: "online", width: 110 },
|
||||
{ title: "状态", slot: "status", width: 90 },
|
||||
{ title: "浏览量", key: "viewCount", width: 80 },
|
||||
{ title: "操作", slot: "action", width: 200 },
|
||||
];
|
||||
|
||||
async function load() {
|
||||
if (!currentSiteCode.value) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
const res: any = await svcItemApi
|
||||
.page({
|
||||
siteCode: currentSiteCode.value,
|
||||
status: status.value,
|
||||
keyword: keyword.value,
|
||||
page: page.value,
|
||||
pageSize: pageSize.value,
|
||||
})
|
||||
.send();
|
||||
total.value = res.total || 0;
|
||||
list.value = res.list || [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function onSiteChange(code: string) {
|
||||
setSite(code);
|
||||
page.value = 1;
|
||||
load();
|
||||
}
|
||||
|
||||
function edit(row?: SvcItemPageItem) {
|
||||
router.push({ path: "/svc/item/edit", query: { id: row?.id ? String(row.id) : undefined } });
|
||||
}
|
||||
|
||||
async function publish(row: SvcItemPageItem) {
|
||||
await svcItemApi.publish(currentSiteCode.value, row.id).send();
|
||||
Message.success("已发布");
|
||||
load();
|
||||
}
|
||||
|
||||
async function offline(row: SvcItemPageItem) {
|
||||
await svcItemApi.offline(currentSiteCode.value, row.id).send();
|
||||
Message.success("已下架");
|
||||
load();
|
||||
}
|
||||
|
||||
function remove(row: SvcItemPageItem) {
|
||||
Modal.confirm({
|
||||
title: "删除确认",
|
||||
content: `确定删除事项「${row.title}」吗?删除后不可恢复。`,
|
||||
onOk: async () => {
|
||||
await svcItemApi.delete(currentSiteCode.value, row.id).send();
|
||||
Message.success("已删除");
|
||||
load();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function syncDept() {
|
||||
const res: any = await svcItemApi.syncDeptName(currentSiteCode.value).send();
|
||||
Message.success(`已同步 ${res.updated} 条部门名快照`);
|
||||
load();
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadSites();
|
||||
load();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<Card :bordered="false" dis-hover>
|
||||
<div class="bar">
|
||||
<Select :model-value="currentSiteCode" style="width: 240px" @on-change="onSiteChange">
|
||||
<Option v-for="s in sites" :key="s.siteCode" :value="s.siteCode">
|
||||
{{ s.name }}({{ s.siteCode }})
|
||||
</Option>
|
||||
</Select>
|
||||
<Select v-model="status" clearable placeholder="状态" style="width: 120px" @on-change="load">
|
||||
<Option :value="0">草稿</Option>
|
||||
<Option :value="1">已发布</Option>
|
||||
<Option :value="2">已下架</Option>
|
||||
</Select>
|
||||
<Input
|
||||
v-model="keyword"
|
||||
search
|
||||
enter-button
|
||||
placeholder="名称/编码"
|
||||
style="width: 240px"
|
||||
@on-search="load"
|
||||
/>
|
||||
<Button type="primary" @click="edit()">新建事项</Button>
|
||||
<Button @click="syncDept">同步部门名</Button>
|
||||
</div>
|
||||
<Table :loading="loading" :columns="columns" :data="list">
|
||||
<template #status="{ row }">
|
||||
<Tag :color="statusColor(row.status)">{{ statusName(row.status) }}</Tag>
|
||||
</template>
|
||||
<template #online="{ row }">
|
||||
<Tag v-if="row.onlineFlag === 1" color="green">可在线办理</Tag>
|
||||
<span v-else>—</span>
|
||||
</template>
|
||||
<template #action="{ row }">
|
||||
<Button size="small" type="text" @click="edit(row)">编辑</Button>
|
||||
<Button v-if="row.status !== 1" size="small" type="text" @click="publish(row)">发布</Button>
|
||||
<Button v-if="row.status === 1" size="small" type="text" @click="offline(row)">下架</Button>
|
||||
<Button size="small" type="text" style="color: #ed4014" @click="remove(row)">删除</Button>
|
||||
</template>
|
||||
</Table>
|
||||
<Page
|
||||
class="pager"
|
||||
:total="total"
|
||||
:current="page"
|
||||
:page-size="pageSize"
|
||||
show-total
|
||||
@on-change="(p: number) => { page = p; load(); }"
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.bar {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.pager {
|
||||
margin-top: 12px;
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user