refactor(admin): 六页对齐 bsch/ysch 应用页规范(09-18 升级补入库,与 flow-center 同批次)
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:
chenxing
2026-09-20 02:50:36 +08:00
parent c2023755ac
commit 18ddb17374
8 changed files with 580 additions and 479 deletions
+20
View File
@@ -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 });
+19 -3
View File
@@ -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
+7
View File
@@ -0,0 +1,7 @@
/** 列表增删改查后的智能翻页语义(TablePageAlovaNew.reloadData 参数,与 flow-center/UserBase 同源) */
export enum OperatorEnum {
add,
del,
edit,
sel,
}
+55 -70
View File
@@ -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;
+109 -56
View File
@@ -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;
+80 -49
View File
@@ -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,15 +203,10 @@ 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;
+195 -181
View File
@@ -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">优先级:本外链 &gt; 办事大厅自动生成 &gt; 不显示按钮</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">优先级:本外链 &gt; 办事大厅自动生成 &gt; 不显示按钮</div>
</FormItem>
</Col>
</Row>
<Divider orientation="left">关联栏目与资产</Divider>
<Row :gutter="16">
<Col span="8">
<FormItem label="常见问题栏目">
<InputNumber v-model="form.faqCategoryId" :min="1" style="width: 100%" placeholder="CMS 栏目 id" />
</FormItem>
</Col>
<Col span="8">
<FormItem label="公告信息栏目">
<InputNumber v-model="form.noticeCategoryId" :min="1" style="width: 100%" placeholder="CMS 栏目 id" />
</FormItem>
</Col>
<Col span="8">
<FormItem label="文件规定栏目">
<InputNumber v-model="form.ruleCategoryId" :min="1" style="width: 100%" placeholder="CMS 栏目 id" />
</FormItem>
</Col>
<Col span="12">
<FormItem label="事项封面">
<div class="cover">
<img v-if="form.coverImage" :src="form.coverImage" class="cover-img" />
<Upload :before-upload="uploadCover" :show-upload-list="false" accept="image/*">
<Button :loading="uploadingCover" icon="ios-image-outline">上传封面</Button>
</Upload>
<Button v-if="form.coverImage" size="small" type="text" @click="form.coverImage = ''">移除</Button>
</div>
</FormItem>
</Col>
<Col span="6">
<FormItem label="排序值">
<InputNumber v-model="form.sortNo" style="width: 100%" />
</FormItem>
</Col>
</Row>
<Divider orientation="left">关联栏目与资产</Divider>
<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>
+94 -119
View File
@@ -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>