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
+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>