refactor(admin): 六页对齐 bsch/ysch 应用页规范(09-18 升级补入库,与 flow-center 同批次)
build / build (push) Successful in 1m58s
build / build (push) Successful in 1m58s
- types/index.ts 补 OperatorEnum(此前 import 悬空,接入 TablePage* 即构建失败)
- TablePageAlovaNew 对齐 flow-center 版:页长记忆 localStorage.customPageCount
+ 重载清空勾选 + watch(tableHeight) 跟随页面侧 ResizeObserver 重算
- ServiceItemList/ApplyManage:三段式骨架 + search-wrap(下拉+关键字 200px
即输即搜)+ TablePageAlovaNew + Utils.operatingHandle 图标操作列(编辑/发布
icon-fasong/下架 icon-stop/删除/详情 icon-browse)+ MyConfirm + 增删后智能翻页
- ServiceDictManage:tabs-wrapper + card Tabs 双容器 v-show + 定高 + 弹窗校验
统一文案「请填写红色星号内容!」
- ServiceFormManage:无分页定高 + 前端过滤 + 按状态条件图标
- ServiceItemEdit:page-wrapper 根容器 + 必填红星
- 状态 Tag 色常量迁 api 层(APPLY_STATUS_COLOR/ITEM_STATUS_COLOR)
- dataApi 返回 alova Method 不调 .send();分页 formatResponse 适配 {total,list}
- vite build 通过(27s)
This commit is contained in:
@@ -29,6 +29,26 @@ const APPLY = "/api/apply";
|
||||
|
||||
const FILE = "/api/file";
|
||||
|
||||
/** 办件流程状态(文案与 Tag 色和用户面 hall 同口径) */
|
||||
export const APPLY_STATUS: Record<number, string> = {
|
||||
0: "待提交",
|
||||
10: "审批中",
|
||||
20: "已完成",
|
||||
30: "已拒绝",
|
||||
40: "已撤回",
|
||||
};
|
||||
export const APPLY_STATUS_COLOR: Record<number, string> = {
|
||||
0: "default",
|
||||
10: "primary",
|
||||
20: "success",
|
||||
30: "error",
|
||||
40: "warning",
|
||||
};
|
||||
|
||||
/** 事项发布状态(0 草稿 / 1 已发布 / 2 已下架) */
|
||||
export const ITEM_STATUS: Record<number, string> = { 0: "草稿", 1: "已发布", 2: "已下架" };
|
||||
export const ITEM_STATUS_COLOR: Record<number, string> = { 0: "default", 1: "success", 2: "warning" };
|
||||
|
||||
/** GET 请求 */
|
||||
function get<T = any>(url: string, params?: Record<string, any>): Method<T> {
|
||||
return new Method<T>("GET", alovaInstance, url, { params });
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, useSlots } from 'vue'
|
||||
import { computed, ref, useSlots, watch } from 'vue'
|
||||
import Utils from '@/utils'
|
||||
import { OperatorEnum } from '@/types'
|
||||
|
||||
@@ -45,6 +45,10 @@ const emit = defineEmits<{
|
||||
const slots = useSlots()
|
||||
|
||||
const tableH = ref(props.tableHeight ?? Utils.useTableHeight())
|
||||
// 页面侧挂载后可能重算高度(嵌入面板初始视口先小后大),跟随 props 更新
|
||||
watch(() => props.tableHeight, v => {
|
||||
if (v) tableH.value = v
|
||||
})
|
||||
const tableSlotNames = computed(() => Object.keys(slots).filter(name => !['default', 'loading'].includes(name)))
|
||||
|
||||
const selectList = ref<number[]>([])
|
||||
@@ -68,18 +72,30 @@ const {
|
||||
total: res => props.formatResponse(res).total,
|
||||
data: res => props.formatResponse(res).list,
|
||||
initialPage: 1,
|
||||
initialPageSize: 10,
|
||||
// 页长记忆(bsch 应用页规范 §3:customPageCount localStorage)
|
||||
initialPageSize: parseInt(localStorage.getItem('customPageCount') || '10') || 10,
|
||||
immediate: props.autoLoad,
|
||||
}
|
||||
)
|
||||
|
||||
// 数据重载后清空勾选,避免批量操作用到旧选中行
|
||||
watch(resp, () => {
|
||||
if (selectList.value.length) {
|
||||
selectList.value = []
|
||||
emit('selection-change', selectList.value)
|
||||
}
|
||||
})
|
||||
|
||||
const list = computed(() => {
|
||||
if (!resp.value) return []
|
||||
return props.dataFilter ? props.dataFilter(resp.value) : resp.value
|
||||
})
|
||||
|
||||
const onPageChange = (p: number) => (page.value = p)
|
||||
const onSizeChange = (s: number) => (pageSize.value = s)
|
||||
const onSizeChange = (s: number) => {
|
||||
pageSize.value = s
|
||||
localStorage.setItem('customPageCount', String(s))
|
||||
}
|
||||
|
||||
function refreshData() {
|
||||
page.value = 1
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
/** 列表增删改查后的智能翻页语义(TablePageAlovaNew.reloadData 参数,与 flow-center/UserBase 同源) */
|
||||
export enum OperatorEnum {
|
||||
add,
|
||||
del,
|
||||
edit,
|
||||
sel,
|
||||
}
|
||||
@@ -6,21 +6,34 @@
|
||||
* svc-hall-public(BP-R4-3),管理端不该跨应用跳转;管理员查办件要的是"快照 + 填报内容 +
|
||||
* 审批轨迹 + 流程图",一个只读抽屉就够,也不必给管理员看"撤回/重提"这类提交人动作。
|
||||
* 数据面复用用户面同口径接口(/api/apply/{id} 与 /{id}/progress,管理员在守卫白名单内)。
|
||||
* 规范骨架(bsch应用页规范 §1):搜索区 → 表格 + 分页(纯查询页,无操作按钮区)。
|
||||
*/
|
||||
import { onMounted, ref } from "vue";
|
||||
import { nextTick, onMounted, onUnmounted, ref } from "vue";
|
||||
import { Message } from "view-ui-plus";
|
||||
import { applyApi } from "@/api/module/svc";
|
||||
import TablePageAlovaNew from "@/components/TablePageAlovaNew.vue";
|
||||
import { OperatorEnum } from "@/types";
|
||||
import { applyApi, APPLY_STATUS, APPLY_STATUS_COLOR } from "@/api/module/svc";
|
||||
import type { ApplyVo, FlowProgressVo } from "@/types/svc";
|
||||
import { useSite } from "@/utils/site";
|
||||
import { buildFormDataGroups, FormDataGroup } from "@/utils/formDataGroups";
|
||||
import Utils from "@/utils";
|
||||
|
||||
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 tableRef = ref<InstanceType<typeof TablePageAlovaNew> | null>(null);
|
||||
const tableHeight = ref(Utils.useTableHeight({ isSearch: true, isPage: true }));
|
||||
// 首挂载时视口/样式可能尚未定型(body 高偏小甚至 0,且其变化不触发 window resize):
|
||||
// ResizeObserver 观察 body,任何高度来源(样式注入/视口调整)变化即重算,卸载时断开
|
||||
let bodyObserver: ResizeObserver | null = null;
|
||||
const recalcTableHeight = () => {
|
||||
tableHeight.value = Utils.useTableHeight({ isSearch: true, isPage: true });
|
||||
};
|
||||
onMounted(() => {
|
||||
recalcTableHeight();
|
||||
bodyObserver = new ResizeObserver(recalcTableHeight);
|
||||
bodyObserver.observe(document.body);
|
||||
});
|
||||
onUnmounted(() => bodyObserver?.disconnect());
|
||||
const dataParams = ref<{ flowStatus?: number; keyword: string }>({ flowStatus: undefined, keyword: "" });
|
||||
|
||||
const detailVisible = ref(false);
|
||||
const detailLoading = ref(false);
|
||||
@@ -29,14 +42,6 @@ const progress = ref<FlowProgressVo | null>(null);
|
||||
const diagramVisible = ref(false);
|
||||
const diagramUrl = 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 SNAPSHOT_LABEL: Record<string, string> = {
|
||||
itemTitle: "事项名称",
|
||||
summary: "事项说明",
|
||||
@@ -49,33 +54,28 @@ const SNAPSHOT_LABEL: Record<string, string> = {
|
||||
const contentGroups = ref<FormDataGroup[]>([]);
|
||||
|
||||
const cols = [
|
||||
{ title: "序号", type: "index", width: 70, align: "center" },
|
||||
{ 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 },
|
||||
{ title: "状态", width: 100, align: "center", render: (h: any, p: any) =>
|
||||
h("Tag", { color: APPLY_STATUS_COLOR[p.row.flowStatus] || "default" }, () => APPLY_STATUS[p.row.flowStatus] || p.row.flowStatus) },
|
||||
{ title: "操作", width: 100, align: "center", fixed: "right", render: (h: any, p: any) =>
|
||||
Utils.operatingHandle(h, "详情", "icon-browse", () => openDetail(p.row)) },
|
||||
];
|
||||
|
||||
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;
|
||||
}
|
||||
// dataApi 返回 Method(alova usePagination 负责发送与解包),不能调 .send()
|
||||
const tableApi = (p: { pageIndex: number; pageSize: number }) => {
|
||||
const params: any = { siteCode: currentSiteCode.value, page: p.pageIndex, pageSize: p.pageSize };
|
||||
if (dataParams.value.flowStatus !== undefined) params.flowStatus = dataParams.value.flowStatus;
|
||||
if (dataParams.value.keyword) params.keyword = dataParams.value.keyword.trim();
|
||||
return applyApi.adminPage(params);
|
||||
};
|
||||
|
||||
function onSearch() {
|
||||
tableRef.value?.refreshData();
|
||||
}
|
||||
|
||||
function parse(json?: string): Record<string, any> {
|
||||
@@ -126,38 +126,33 @@ function openDiagram() {
|
||||
diagramVisible.value = true;
|
||||
}
|
||||
|
||||
// 站点列表就绪后再拉首屏(siteCode 为定位键,避免空站点空跑一页)
|
||||
onMounted(async () => {
|
||||
await loadSites();
|
||||
load();
|
||||
tableRef.value?.reloadData(OperatorEnum.sel);
|
||||
});
|
||||
</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(); }">
|
||||
<div class="page-wrapper">
|
||||
<div class="main-container">
|
||||
<!-- 搜索区:站点/状态下拉靠左,关键字即输即搜不设查询按钮 -->
|
||||
<div class="search-wrap fix">
|
||||
<Select :model-value="currentSiteCode" class="search-wrap_sel" placeholder="站点"
|
||||
@on-change="(c: string) => { setSite(c); onSearch(); }">
|
||||
<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 v-model="dataParams.flowStatus" class="search-wrap_sel ml-12" clearable placeholder="状态" @on-change="onSearch">
|
||||
<Option v-for="(v, k) in APPLY_STATUS" :key="k" :value="Number(k)" :label="v" />
|
||||
</Select>
|
||||
<Input v-model="keyword" search enter-button placeholder="事项/提交人" style="width: 220px" @on-search="load" />
|
||||
<span class="sh-muted" style="color: #999; font-size: 13px">用户面办理入口在独立前端 /svc-hall/public/</span>
|
||||
<Input class="fr" v-model="dataParams.keyword" placeholder="事项/提交人" search suffix="ios-search"
|
||||
style="width: 200px" @on-search="onSearch" @on-clear="onSearch" />
|
||||
</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="openDetail(row)">详情</Button>
|
||||
</template>
|
||||
</Table>
|
||||
<Page class="pager" :total="total" :current="page" :page-size="10" show-total
|
||||
@on-change="(p: number) => { page = p; load(); }" />
|
||||
</Card>
|
||||
|
||||
<TablePageAlovaNew ref="tableRef" :dataApi="tableApi" :columns="cols" :tableHeight="tableHeight"
|
||||
:autoLoad="false"
|
||||
:formatResponse="(res: any) => ({ total: res.total, list: res.list })" />
|
||||
</div>
|
||||
|
||||
<Drawer v-model="detailVisible" title="办件详情(只读)" width="620">
|
||||
<Spin v-if="detailLoading" fix></Spin>
|
||||
@@ -166,8 +161,8 @@ onMounted(async () => {
|
||||
<DescriptionsItem label="办件号">{{ detail.id }}</DescriptionsItem>
|
||||
<DescriptionsItem label="事项">{{ detail.itemTitle }}({{ detail.itemCode }})</DescriptionsItem>
|
||||
<DescriptionsItem label="状态">
|
||||
<Tag :color="STATUS[detail.flowStatus]?.color || 'default'">
|
||||
{{ STATUS[detail.flowStatus]?.text || detail.flowStatus }}
|
||||
<Tag :color="APPLY_STATUS_COLOR[detail.flowStatus] || 'default'">
|
||||
{{ APPLY_STATUS[detail.flowStatus] || detail.flowStatus }}
|
||||
</Tag>
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="提交人">{{ detail.submitterName }}</DescriptionsItem>
|
||||
@@ -225,16 +220,6 @@ onMounted(async () => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.bar {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
.pager {
|
||||
margin-top: 12px;
|
||||
text-align: right;
|
||||
}
|
||||
.kv {
|
||||
display: flex;
|
||||
padding: 5px 0;
|
||||
|
||||
@@ -1,38 +1,81 @@
|
||||
<script setup lang="ts">
|
||||
/** 服务字典管理(modId 24):object/category 双 Tab,引用守护。 */
|
||||
import { onMounted, ref } from "vue";
|
||||
import { Message, Modal } from "view-ui-plus";
|
||||
/**
|
||||
* 服务字典管理(modId 24):object/category 双 Tab,引用守护。
|
||||
* 规范骨架(bsch应用页规范 §1):tabs-wrapper + Tabs type=card,Tab 切换只 v-show 内容区,列表状态各自独立。
|
||||
*/
|
||||
import { nextTick, onMounted, onUnmounted, ref } from "vue";
|
||||
import { Message } from "view-ui-plus";
|
||||
import MyConfirm from "@/components/MyConfirm";
|
||||
import { svcDictApi } from "@/api/module/svc";
|
||||
import type { SvcDictVo } from "@/types/svc";
|
||||
import { useSite } from "@/utils/site";
|
||||
import Utils from "@/utils";
|
||||
|
||||
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 modal = ref(false);
|
||||
const formRef = ref<any>(null);
|
||||
const form = ref<any>({});
|
||||
|
||||
// tabs 页:表格高度在常规骨架上再扣 Tab 条(无分页)
|
||||
const tableHeight = ref(Utils.useTableHeight({ isTabs: true, isOperat: true }));
|
||||
// 首挂载时视口/样式可能尚未定型(body 高偏小甚至 0,且其变化不触发 window resize):
|
||||
// ResizeObserver 观察 body,任何高度来源(样式注入/视口调整)变化即重算,卸载时断开
|
||||
let bodyObserver: ResizeObserver | null = null;
|
||||
const recalcTableHeight = () => {
|
||||
tableHeight.value = Utils.useTableHeight({ isTabs: true, isOperat: true });
|
||||
};
|
||||
onMounted(() => {
|
||||
recalcTableHeight();
|
||||
bodyObserver = new ResizeObserver(recalcTableHeight);
|
||||
bodyObserver.observe(document.body);
|
||||
});
|
||||
onUnmounted(() => bodyObserver?.disconnect());
|
||||
|
||||
// 两个 Tab 各自的列表状态(切换只 v-show,互不重置)
|
||||
const objectRows = ref<SvcDictVo[]>([]);
|
||||
const objectLoading = ref(false);
|
||||
const categoryRows = ref<SvcDictVo[]>([]);
|
||||
const categoryLoading = ref(false);
|
||||
|
||||
const cols = [
|
||||
{ title: "序号", type: "index", width: 70, align: "center" },
|
||||
{ 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 },
|
||||
{ title: "排序", key: "sortNo", width: 80, align: "center" },
|
||||
{ title: "被引用", key: "referenced", width: 90, align: "center" },
|
||||
{ title: "状态", width: 90, align: "center", render: (h: any, p: any) =>
|
||||
h("Tag", { color: p.row.status === 1 ? "green" : "gray" }, () => (p.row.status === 1 ? "启用" : "停用")) },
|
||||
{ title: "操作", width: 140, align: "center", fixed: "right", render: (h: any, p: any) => [
|
||||
Utils.operatingHandle(h, "编辑", "icon-edit", () => open(p.row)),
|
||||
Utils.operatingHandle(h, "删除", "icon-ashbin", () => remove(p.row)),
|
||||
] },
|
||||
];
|
||||
|
||||
async function load() {
|
||||
if (!currentSiteCode.value) return;
|
||||
loading.value = true;
|
||||
const loadingRef = tab.value === "object" ? objectLoading : categoryLoading;
|
||||
const rowsRef = tab.value === "object" ? objectRows : categoryRows;
|
||||
loadingRef.value = true;
|
||||
try {
|
||||
list.value = (await svcDictApi.list(currentSiteCode.value, tab.value).send()) || [];
|
||||
rowsRef.value = (await svcDictApi.list(currentSiteCode.value, tab.value).send()) || [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
loadingRef.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 切 Tab 拉当前页数据(字典量小,直接刷新保持实时)
|
||||
function switchTab(name: string) {
|
||||
tab.value = name as any;
|
||||
load();
|
||||
}
|
||||
|
||||
const saveRules = {
|
||||
dictKey: [{ required: true, message: "请填写红色星号内容!", trigger: "blur" }],
|
||||
dictName: [{ required: true, message: "请填写红色星号内容!", trigger: "blur" }],
|
||||
};
|
||||
|
||||
function open(row?: SvcDictVo) {
|
||||
form.value = row
|
||||
? { ...row }
|
||||
@@ -40,23 +83,27 @@ function open(row?: SvcDictVo) {
|
||||
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 save() {
|
||||
formRef.value?.validate(async (valid: boolean) => {
|
||||
if (!valid) return;
|
||||
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 () => {
|
||||
MyConfirm({
|
||||
title: "删除",
|
||||
msg: `确定删除字典「${row.dictName}」吗?`,
|
||||
info: "被引用的删除会被拒绝",
|
||||
async onOk() {
|
||||
await svcDictApi.delete(currentSiteCode.value, row.id).send();
|
||||
Message.success("已删除");
|
||||
load();
|
||||
@@ -71,36 +118,47 @@ onMounted(async () => {
|
||||
</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(); }">
|
||||
<div class="tabs-wrapper">
|
||||
<Tabs type="card" :value="tab" @on-click="switchTab">
|
||||
<TabPane label="服务对象" name="object" />
|
||||
<TabPane label="事项类别" name="category" />
|
||||
</Tabs>
|
||||
|
||||
<div class="main-container" v-show="tab === 'object'">
|
||||
<div class="fix mb-12">
|
||||
<Button class="fl" type="primary" size="large" @click="open()">新建服务对象</Button>
|
||||
</div>
|
||||
<div class="search-wrap fix">
|
||||
<Select :model-value="currentSiteCode" class="search-wrap_sel" placeholder="站点"
|
||||
@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>
|
||||
<Table :columns="cols" :data="objectRows" :loading="objectLoading" class="link-table table-bottom-box"
|
||||
:height="tableHeight" no-data-text="暂无数据" />
|
||||
</div>
|
||||
|
||||
<Modal v-model="modal" :title="form.id ? '编辑字典' : '新建字典'">
|
||||
<Form :label-width="90">
|
||||
<FormItem label="字典键" required>
|
||||
<div class="main-container" v-show="tab === 'category'">
|
||||
<div class="fix mb-12">
|
||||
<Button class="fl" type="primary" size="large" @click="open()">新建事项类别</Button>
|
||||
</div>
|
||||
<div class="search-wrap fix">
|
||||
<Select :model-value="currentSiteCode" class="search-wrap_sel" placeholder="站点"
|
||||
@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>
|
||||
</div>
|
||||
<Table :columns="cols" :data="categoryRows" :loading="categoryLoading" class="link-table table-bottom-box"
|
||||
:height="tableHeight" no-data-text="暂无数据" />
|
||||
</div>
|
||||
|
||||
<Modal v-model="modal" :title="form.id ? '编辑字典' : '新建字典'" :mask-closable="false">
|
||||
<Form ref="formRef" :model="form" :rules="saveRules" :label-width="90">
|
||||
<FormItem label="字典键" prop="dictKey">
|
||||
<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>
|
||||
<FormItem label="名称" prop="dictName">
|
||||
<Input v-model="form.dictName" :maxlength="128" />
|
||||
</FormItem>
|
||||
<FormItem label="排序">
|
||||
@@ -112,18 +170,13 @@ onMounted(async () => {
|
||||
</Form>
|
||||
<template #footer>
|
||||
<Button @click="modal = false">取消</Button>
|
||||
<Button type="primary" :loading="saving" @click="save">保存</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;
|
||||
|
||||
@@ -1,16 +1,37 @@
|
||||
<script setup lang="ts">
|
||||
/** 表单管理(modId 25):列表/发布/删除/版本轨迹/绑定明细。编辑走独立全屏设计器(/svc/form/designer)。 */
|
||||
import { onMounted, ref } from "vue";
|
||||
/**
|
||||
* 表单管理(modId 25):列表/发布/删除/版本轨迹/绑定明细。编辑走独立全屏设计器(/svc/form/designer)。
|
||||
* 规范骨架(bsch应用页规范 §1):操作按钮区 → 搜索区 → 表格(接口全量返回,无分页,前端关键字过滤)。
|
||||
*/
|
||||
import { computed, nextTick, onMounted, onUnmounted, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { Message, Modal } from "view-ui-plus";
|
||||
import MyConfirm from "@/components/MyConfirm";
|
||||
import { svcFormApi } from "@/api/module/svc";
|
||||
import type { SvcFormItemBrief, SvcFormVo } from "@/types/svc";
|
||||
import { useSite } from "@/utils/site";
|
||||
import Utils from "@/utils";
|
||||
|
||||
const router = useRouter();
|
||||
const { currentSiteCode, sites, loadSites, setSite } = useSite();
|
||||
const loading = ref(false);
|
||||
const list = ref<SvcFormVo[]>([]);
|
||||
const keyword = ref("");
|
||||
const rows = ref<SvcFormVo[]>([]);
|
||||
// 操作按钮区 + 搜索区、无分页
|
||||
const tableHeight = ref(Utils.useTableHeight({ isSearch: true, isOperat: true }));
|
||||
// 首挂载时视口/样式可能尚未定型(body 高偏小甚至 0,且其变化不触发 window resize):
|
||||
// ResizeObserver 观察 body,任何高度来源(样式注入/视口调整)变化即重算,卸载时断开
|
||||
let bodyObserver: ResizeObserver | null = null;
|
||||
const recalcTableHeight = () => {
|
||||
tableHeight.value = Utils.useTableHeight({ isSearch: true, isOperat: true });
|
||||
};
|
||||
onMounted(() => {
|
||||
recalcTableHeight();
|
||||
bodyObserver = new ResizeObserver(recalcTableHeight);
|
||||
bodyObserver.observe(document.body);
|
||||
});
|
||||
onUnmounted(() => bodyObserver?.disconnect());
|
||||
|
||||
const versionsModal = ref(false);
|
||||
const versionRows = ref<SvcFormVo[]>([]);
|
||||
const refsModal = ref(false);
|
||||
@@ -18,14 +39,21 @@ const refRows = ref<SvcFormItemBrief[]>([]);
|
||||
const refFormName = ref("");
|
||||
|
||||
const statusName = (s: number) => (s === 1 ? "已发布" : s === 2 ? "停用" : "草稿");
|
||||
const statusColor = (s: number) => (s === 1 ? "success" : "default");
|
||||
const itemStatusName = (s: number) => (s === 1 ? "已发布" : s === 2 ? "已下线" : "草稿");
|
||||
|
||||
// 关键字前端过滤(list 接口全量返回,无服务端 keyword)
|
||||
const filteredRows = computed(() => {
|
||||
const kw = keyword.value.trim().toLowerCase();
|
||||
if (!kw) return rows.value;
|
||||
return rows.value.filter((r: any) =>
|
||||
String(r.name || "").toLowerCase().includes(kw) || String(r.code || "").toLowerCase().includes(kw));
|
||||
});
|
||||
|
||||
async function load() {
|
||||
if (!currentSiteCode.value) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
list.value = (await svcFormApi.list(currentSiteCode.value).send()) || [];
|
||||
rows.value = (await svcFormApi.list(currentSiteCode.value).send()) || [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@@ -56,10 +84,11 @@ async function publish(row: SvcFormVo) {
|
||||
}
|
||||
|
||||
function remove(row: SvcFormVo) {
|
||||
Modal.confirm({
|
||||
title: "删除确认",
|
||||
content: `确定删除表单「${row.name}」的全部版本吗?被事项绑定的删除会被拒绝。`,
|
||||
onOk: async () => {
|
||||
MyConfirm({
|
||||
title: "删除",
|
||||
msg: `确定删除表单「${row.name}」的全部版本吗?`,
|
||||
info: "被事项绑定的删除会被拒绝",
|
||||
async onOk() {
|
||||
await svcFormApi.delete(currentSiteCode.value, row.code).send();
|
||||
Message.success("已删除");
|
||||
load();
|
||||
@@ -85,49 +114,56 @@ function editRefItem(item: SvcFormItemBrief) {
|
||||
router.push(`/svc/item/edit?id=${item.id}`);
|
||||
}
|
||||
|
||||
const cols = [
|
||||
{ title: "序号", type: "index", width: 70, align: "center" },
|
||||
{ title: "编码", key: "code", width: 160 },
|
||||
{ title: "名称", key: "name", minWidth: 140 },
|
||||
{ title: "最新版本", key: "version", width: 90, align: "center" },
|
||||
{ title: "状态", width: 90, align: "center", render: (h: any, p: any) =>
|
||||
h("Tag", { color: p.row.status === 1 ? "success" : "default" }, () => statusName(p.row.status)) },
|
||||
{ title: "被绑定", width: 110, align: "center", render: (h: any, p: any) =>
|
||||
p.row.referenced
|
||||
? h("a", { style: "cursor:pointer", onClick: () => showRefs(p.row) }, `已绑定 ${(p.row.refItems || []).length} 个事项`)
|
||||
: h("span", { style: "color:#999" }, "未绑定") },
|
||||
{ title: "发布时间", key: "publishTime", width: 160 },
|
||||
{ title: "操作", width: 220, align: "center", fixed: "right", render: (h: any, p: any) => {
|
||||
const btns = [Utils.operatingHandle(h, "编辑", "icon-edit", () => editForm(p.row))];
|
||||
if (p.row.status === 0) btns.push(Utils.operatingHandle(h, "发布", "icon-fasong", () => publish(p.row)));
|
||||
if (!p.row.referenced) btns.push(Utils.operatingHandle(h, "去绑定", "icon-peizhi", () => router.push("/svc/item")));
|
||||
btns.push(Utils.operatingHandle(h, "版本", "icon-chakan", () => showVersions(p.row)));
|
||||
btns.push(Utils.operatingHandle(h, "删除", "icon-ashbin", () => remove(p.row)));
|
||||
return btns;
|
||||
} },
|
||||
];
|
||||
|
||||
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: 110 },
|
||||
{ title: "发布时间", key: "publishTime", width: 160 },
|
||||
{ title: "操作", slot: "action", width: 260 },
|
||||
];
|
||||
</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(); }">
|
||||
<div class="page-wrapper">
|
||||
<div class="main-container">
|
||||
<!-- 操作按钮区:主操作靠左,说明文字靠右 -->
|
||||
<div class="fix mb-12">
|
||||
<Button class="fl" type="primary" size="large" @click="createForm">新建表单</Button>
|
||||
<span class="fr bar-tip">拖拽式设计器编辑表单结构,保存为草稿后发布即可在事项发起页使用</span>
|
||||
</div>
|
||||
|
||||
<!-- 搜索区:站点下拉靠左,关键字前端过滤即输即搜 -->
|
||||
<div class="search-wrap fix">
|
||||
<Select :model-value="currentSiteCode" class="search-wrap_sel" placeholder="站点"
|
||||
@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="createForm">新建表单</Button>
|
||||
<div class="bar-tip">拖拽式设计器编辑表单结构,保存为草稿后发布即可在事项发起页使用</div>
|
||||
<Input class="fr" v-model="keyword" placeholder="名称/编码" search suffix="ios-search"
|
||||
style="width: 200px" />
|
||||
</div>
|
||||
<Table :loading="loading" :columns="cols" :data="list">
|
||||
<template #status="{ row }">
|
||||
<Tag :color="statusColor(row.status)">{{ statusName(row.status) }}</Tag>
|
||||
</template>
|
||||
<template #referenced="{ row }">
|
||||
<a v-if="row.referenced" @click="showRefs(row)">已绑定 {{ (row.refItems || []).length }} 个事项</a>
|
||||
<span v-else style="color: #999">未绑定</span>
|
||||
</template>
|
||||
<template #action="{ row }">
|
||||
<Button size="small" type="text" @click="editForm(row)">编辑</Button>
|
||||
<Button v-if="row.status === 0" size="small" type="text" @click="publish(row)">发布</Button>
|
||||
<Button v-if="!row.referenced" size="small" type="text" @click="router.push('/svc/item')">去绑定</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>
|
||||
|
||||
<Table :columns="cols" :data="filteredRows" :loading="loading" class="link-table table-bottom-box"
|
||||
:height="tableHeight" no-data-text="暂无数据" />
|
||||
</div>
|
||||
|
||||
<Modal v-model="refsModal" width="720" :title="`「${refFormName}」被以下事项使用`">
|
||||
<Table size="small" :columns="[
|
||||
@@ -167,19 +203,14 @@ const cols = [
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.bar {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
.bar-tip {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
line-height: 40px;
|
||||
}
|
||||
.tip {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
line-height: 1.6;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
+195
-181
@@ -17,6 +17,13 @@ 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 & Record<string, any>>({
|
||||
siteCode: "",
|
||||
@@ -109,6 +116,11 @@ async function uploadGuide(file: File) {
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -139,195 +151,197 @@ 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>
|
||||
<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" />
|
||||
</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="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 }})
|
||||
<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 }}{{ f.status !== 1 ? `(${f.status === 2 ? "已停用" : "草稿"})` : "" }}
|
||||
</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 }}{{ f.status !== 1 ? `(${f.status === 2 ? "已停用" : "草稿"})` : "" }}
|
||||
</Option>
|
||||
</Select>
|
||||
<div v-if="selectedFormDraft" class="tip" style="color: #ed4014">
|
||||
该表单尚未发布,发布后才能在发起页实际填写;请到「表单管理」发布。
|
||||
</div>
|
||||
<div v-else class="tip">发起页按表单编码取最新已发布版本渲染</div>
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col span="24">
|
||||
<FormItem label="我要办理外链">
|
||||
<Input v-model="form.applyUrl" placeholder="http(s):// 完整地址;填写后优先于自动生成地址" />
|
||||
<div class="tip">优先级:本外链 > 办事大厅自动生成 > 不显示按钮</div>
|
||||
</FormItem>
|
||||
</Col>
|
||||
</Row>
|
||||
<div v-if="selectedFormDraft" class="tip" style="color: #ed4014">
|
||||
该表单尚未发布,发布后才能在发起页实际填写;请到「表单管理」发布。
|
||||
</div>
|
||||
<div v-else class="tip">发起页按表单编码取最新已发布版本渲染</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>
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
</FormItem>
|
||||
</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 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>
|
||||
|
||||
|
||||
@@ -1,92 +1,101 @@
|
||||
<script setup lang="ts">
|
||||
/** 办事事项列表(modId 23):筛选/发布/下架/删除/同步部门名。 */
|
||||
import { h, onMounted, ref } from "vue";
|
||||
/**
|
||||
* 办事事项列表(modId 23):筛选/发布/下架/删除/同步部门名。
|
||||
* 规范骨架(bsch应用页规范 §1):操作按钮区 → 搜索区 → 表格 + 分页,分页走 TablePageAlovaNew。
|
||||
*/
|
||||
import { nextTick, onMounted, onUnmounted, 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 { Message } from "view-ui-plus";
|
||||
import MyConfirm from "@/components/MyConfirm";
|
||||
import TablePageAlovaNew from "@/components/TablePageAlovaNew.vue";
|
||||
import { OperatorEnum } from "@/types";
|
||||
import { svcItemApi, ITEM_STATUS, ITEM_STATUS_COLOR } from "@/api/module/svc";
|
||||
import { useSite } from "@/utils/site";
|
||||
import Utils from "@/utils";
|
||||
|
||||
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 tableRef = ref<InstanceType<typeof TablePageAlovaNew> | null>(null);
|
||||
const tableHeight = ref(Utils.useTableHeight({ isSearch: true, isOperat: true, isPage: true }));
|
||||
// 首挂载时视口/样式可能尚未定型(body 高偏小甚至 0,且其变化不触发 window resize):
|
||||
// ResizeObserver 观察 body,任何高度来源(样式注入/视口调整)变化即重算,卸载时断开
|
||||
let bodyObserver: ResizeObserver | null = null;
|
||||
const recalcTableHeight = () => {
|
||||
tableHeight.value = Utils.useTableHeight({ isSearch: true, isOperat: true, isPage: true });
|
||||
};
|
||||
onMounted(() => {
|
||||
recalcTableHeight();
|
||||
bodyObserver = new ResizeObserver(recalcTableHeight);
|
||||
bodyObserver.observe(document.body);
|
||||
});
|
||||
onUnmounted(() => bodyObserver?.disconnect());
|
||||
|
||||
const dataParams = ref<{ status?: number; keyword: string }>({ status: undefined, keyword: "" });
|
||||
|
||||
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: "序号", type: "index", width: 70, align: "center" },
|
||||
{ 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 },
|
||||
{ title: "办理方式", width: 90, align: "center", render: (h: any, p: any) => h("span", handleModeName(p.row.handleMode)) },
|
||||
{ title: "在线办理", width: 110, align: "center", render: (h: any, p: any) =>
|
||||
p.row.onlineFlag === 1
|
||||
? h("Tag", { color: "green" }, () => "可在线办理")
|
||||
: h("span", "-") },
|
||||
{ title: "状态", width: 90, align: "center", render: (h: any, p: any) =>
|
||||
h("Tag", { color: ITEM_STATUS_COLOR[p.row.status] || "default" }, () => ITEM_STATUS[p.row.status] || p.row.status) },
|
||||
{ title: "浏览量", key: "viewCount", width: 80, align: "center" },
|
||||
{ title: "操作", width: 180, align: "center", fixed: "right", render: (h: any, p: any) => {
|
||||
const btns = [Utils.operatingHandle(h, "编辑", "icon-edit", () => edit(p.row))];
|
||||
btns.push(p.row.status === 1
|
||||
? Utils.operatingHandle(h, "下架", "icon-stop", () => offline(p.row))
|
||||
: Utils.operatingHandle(h, "发布", "icon-fasong", () => publish(p.row)));
|
||||
btns.push(Utils.operatingHandle(h, "删除", "icon-ashbin", () => remove(p.row)));
|
||||
return btns;
|
||||
} },
|
||||
];
|
||||
|
||||
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;
|
||||
}
|
||||
// dataApi 返回 Method(alova usePagination 负责发送与解包),不能调 .send()
|
||||
const tableApi = (p: { pageIndex: number; pageSize: number }) => {
|
||||
const params: any = { siteCode: currentSiteCode.value, page: p.pageIndex, pageSize: p.pageSize };
|
||||
if (dataParams.value.status !== undefined) params.status = dataParams.value.status;
|
||||
if (dataParams.value.keyword) params.keyword = dataParams.value.keyword.trim();
|
||||
return svcItemApi.page(params);
|
||||
};
|
||||
|
||||
// 站点/筛选变化回到第 1 页
|
||||
function onSearch() {
|
||||
tableRef.value?.refreshData();
|
||||
}
|
||||
|
||||
function onSiteChange(code: string) {
|
||||
setSite(code);
|
||||
page.value = 1;
|
||||
load();
|
||||
}
|
||||
|
||||
function edit(row?: SvcItemPageItem) {
|
||||
function edit(row?: any) {
|
||||
router.push({ path: "/svc/item/edit", query: { id: row?.id ? String(row.id) : undefined } });
|
||||
}
|
||||
|
||||
async function publish(row: SvcItemPageItem) {
|
||||
async function publish(row: any) {
|
||||
await svcItemApi.publish(currentSiteCode.value, row.id).send();
|
||||
Message.success("已发布");
|
||||
load();
|
||||
tableRef.value?.reloadData(OperatorEnum.edit);
|
||||
}
|
||||
|
||||
async function offline(row: SvcItemPageItem) {
|
||||
async function offline(row: any) {
|
||||
await svcItemApi.offline(currentSiteCode.value, row.id).send();
|
||||
Message.success("已下架");
|
||||
load();
|
||||
tableRef.value?.reloadData(OperatorEnum.edit);
|
||||
}
|
||||
|
||||
function remove(row: SvcItemPageItem) {
|
||||
Modal.confirm({
|
||||
title: "删除确认",
|
||||
content: `确定删除事项「${row.title}」吗?删除后不可恢复。`,
|
||||
onOk: async () => {
|
||||
function remove(row: any) {
|
||||
MyConfirm({
|
||||
title: "删除",
|
||||
msg: `确定删除事项「${row.title}」吗?`,
|
||||
info: "删除后不可恢复",
|
||||
async onOk() {
|
||||
await svcItemApi.delete(currentSiteCode.value, row.id).send();
|
||||
Message.success("已删除");
|
||||
load();
|
||||
tableRef.value?.reloadData(OperatorEnum.del);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -94,75 +103,41 @@ function remove(row: SvcItemPageItem) {
|
||||
async function syncDept() {
|
||||
const res: any = await svcItemApi.syncDeptName(currentSiteCode.value).send();
|
||||
Message.success(`已同步 ${res.updated} 条部门名快照`);
|
||||
load();
|
||||
tableRef.value?.refreshData();
|
||||
}
|
||||
|
||||
// 站点列表就绪后再拉首屏(siteCode 为定位键,避免空站点空跑一页)
|
||||
onMounted(async () => {
|
||||
await loadSites();
|
||||
load();
|
||||
tableRef.value?.reloadData(OperatorEnum.sel);
|
||||
});
|
||||
</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 class="page-wrapper">
|
||||
<div class="main-container">
|
||||
<!-- 操作按钮区:主操作靠左、其余靠右(bsch应用页规范 §1) -->
|
||||
<div class="fix mb-12">
|
||||
<Button class="fl" type="primary" size="large" @click="edit()">新建事项</Button>
|
||||
<Button class="fr" size="large" @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 class="search-wrap fix">
|
||||
<Select :model-value="currentSiteCode" class="search-wrap_sel" placeholder="站点"
|
||||
@on-change="(c: string) => { setSite(c); onSearch(); }">
|
||||
<Option v-for="s in sites" :key="s.siteCode" :value="s.siteCode">{{ s.name }}({{ s.siteCode }})</Option>
|
||||
</Select>
|
||||
<Select v-model="dataParams.status" class="search-wrap_sel ml-12" clearable placeholder="状态" @on-change="onSearch">
|
||||
<Option v-for="(name, k) in ITEM_STATUS" :key="k" :value="Number(k)" :label="name" />
|
||||
</Select>
|
||||
<Input class="fr" v-model="dataParams.keyword" placeholder="名称/编码" search suffix="ios-search"
|
||||
style="width: 200px" @on-search="onSearch" @on-clear="onSearch" />
|
||||
</div>
|
||||
|
||||
<TablePageAlovaNew ref="tableRef" :dataApi="tableApi" :columns="columns" :tableHeight="tableHeight"
|
||||
:autoLoad="false"
|
||||
:formatResponse="(res: any) => ({ total: res.total, list: res.list })" />
|
||||
</div>
|
||||
</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