refactor: 全部表格页改用 Vben Vxe Table(useVbenVxeGrid) + Vben TableAction 行操作
11 个 t-table 页面统一重构:system/{log,login-log,admin,role}、
recruitment/{info,crawler,push}、house/{data,listing,community,presale}、dashboard/workbench。
- 本地小表 proxyConfig.enabled=false + setGridOptions 写数据(workbench/crawler/push)
- listing 多选批量标记改 checkboxConfig + getCheckboxRecords
- 行操作走 ActionItem.auth 权限码、删除 danger 标红、确认弹窗保留 DialogPlugin
- 筛选区保留 TDesign 组件;typecheck 与 lint 全绿
This commit is contained in:
parent
0807bb8514
commit
8b86b73424
@ -1,6 +1,8 @@
|
||||
# admin.xpcool.com 变更记录
|
||||
> 倒序:最新在上。格式:YYYY-MM-DD | 类型 | 摘要
|
||||
|
||||
2026-08-27 | CHG | 全量表格重构:views 下全部 11 个 t-table 页面统一改为 Vben Vxe Table(useVbenVxeGrid,#/adapter/vxe-table)+ Vben TableAction 行操作——system/{log(范式),login-log,admin,role}、recruitment/{info,crawler,push}、house/{data,listing,community,presale}、dashboard/workbench(两处本地小表 proxyConfig.enabled=false + gridApi.setGridOptions 写数据)。要点:gridOptions 需注解 VxeTableGridOptions(否则 type:'seq' 推断 string 报错);单元格自定义用 columns[].slots.default + h(TDesign组件);远程分页走 proxyConfig.ajax.query 返回 {items,total};筛选区保留手写 TDesign 组件不动;admin/role/listing/community 行操作 ActionItem 带 auth 权限码、删除 danger 标红;listing 多选批量标记改 vxe checkboxConfig + gridApi.grid.getCheckboxRecords();confirm 弹窗交互(删除/重置密码)保留 DialogPlugin 不变;typecheck 通过
|
||||
|
||||
2026-08-27 | FIX | 恢复丢失的前端代码:recovery 分支经 rebase/改名(dev↔main)折腾后工作树回退,未提交改动全丢;从悬空提交 f02a514(recovery 工作树快照,含 HEAD 缺失的全部文件)整体恢复 apps/web-tdesign/src——含 styles/theme.css 全局主题、dashboard/workbench 工作台页、system 五页卡片化美化、操作日志增强版(VxeTable)、house 预售/楼盘地图、recruitment 页面;另补 log 页卡片美化(wb-page-head+t-card)。已提交保护
|
||||
2026-08-26 | CFG | 安装 TDesign MCP(~/.workbuddy/mcp.json 新增 tdesign-mcp-server: `npx -y tdesign-mcp-server@latest`,v0.2.4 启动验证通过);约定:后续后台界面开发优先用 TDesign MCP 查官方文档(get-component-docs 等)
|
||||
2026-08-26 | FIX+CHG | 修复前端编译错误(access.ts layoutMap 重复声明致 vite Transform PARSE_ERROR);菜单管理树形改用 tdesign 官方方案:`t-enhanced-table`(t-table 不支持 tree)+ tree{childrenKey/treeNodeColumnIndex/defaultExpandAll/indent} + 异步数据后 `ref.expandAll()`(defaultExpandAll 仅首次渲染生效)——替代先前扁平化+缩进的自造方案
|
||||
|
||||
@ -1,14 +1,16 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { LoginLogItem, LogItem, MenuNode } from '#/api/system';
|
||||
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
import { computed, h, onMounted, reactive, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { useUserStore } from '@vben/stores';
|
||||
|
||||
import { MessagePlugin } from 'tdesign-vue-next';
|
||||
import { MessagePlugin, Tag } from 'tdesign-vue-next';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
getAdminList,
|
||||
getLoginLogList,
|
||||
@ -103,20 +105,54 @@ const quickNavs = [
|
||||
},
|
||||
];
|
||||
|
||||
const opColumns = [
|
||||
{ colKey: 'adminId', title: '操作人ID', width: 90 },
|
||||
{ colKey: 'permission', title: '权限码', ellipsis: true },
|
||||
{ colKey: 'path', title: '接口', ellipsis: true },
|
||||
{ colKey: 'ip', title: 'IP', width: 130 },
|
||||
{ colKey: 'createdAt', title: '时间', width: 170 },
|
||||
];
|
||||
// 最新操作日志(本地数据小表:关闭 proxyConfig,数据由 load() 写入)
|
||||
const opGridOptions: VxeTableGridOptions = {
|
||||
columns: [
|
||||
{ type: 'seq', width: 50, title: '#' },
|
||||
{ field: 'adminId', title: '操作人ID', width: 90 },
|
||||
{ field: 'permission', title: '权限码', showOverflow: 'tooltip' },
|
||||
{ field: 'path', title: '接口', showOverflow: 'tooltip' },
|
||||
{ field: 'ip', title: 'IP', width: 130 },
|
||||
{ field: 'createdAt', title: '时间', width: 170 },
|
||||
],
|
||||
pagerConfig: { enabled: false }, // 仅展示最新 5 条,不分页
|
||||
proxyConfig: { enabled: false }, // 本地数据模式
|
||||
};
|
||||
|
||||
const loginColumns = [
|
||||
{ colKey: 'username', title: '账号', width: 120 },
|
||||
{ colKey: 'ip', title: 'IP', width: 130 },
|
||||
{ colKey: 'statusText', title: '结果', width: 90 },
|
||||
{ colKey: 'createdAt', title: '时间', width: 170 },
|
||||
];
|
||||
const [OpGrid, opGridApi] = useVbenVxeGrid({ gridOptions: opGridOptions });
|
||||
|
||||
// 最新登录日志(本地数据小表)
|
||||
const loginGridOptions: VxeTableGridOptions = {
|
||||
columns: [
|
||||
{ type: 'seq', width: 50, title: '#' },
|
||||
{ field: 'username', title: '账号', width: 120 },
|
||||
{ field: 'ip', title: 'IP', width: 130 },
|
||||
{
|
||||
field: 'statusText',
|
||||
title: '结果',
|
||||
width: 90,
|
||||
slots: {
|
||||
default: ({ row }: { row: LoginLogItem }) =>
|
||||
h(
|
||||
Tag,
|
||||
{
|
||||
theme: row.status === 1 ? 'success' : 'danger',
|
||||
variant: 'light',
|
||||
size: 'small',
|
||||
},
|
||||
row.status === 1 ? '成功' : '失败',
|
||||
),
|
||||
},
|
||||
},
|
||||
{ field: 'createdAt', title: '时间', width: 170 },
|
||||
],
|
||||
pagerConfig: { enabled: false },
|
||||
proxyConfig: { enabled: false },
|
||||
};
|
||||
|
||||
const [LoginGrid, loginGridApi] = useVbenVxeGrid({
|
||||
gridOptions: loginGridOptions,
|
||||
});
|
||||
|
||||
function formatToday() {
|
||||
const d = new Date();
|
||||
@ -155,9 +191,13 @@ async function load() {
|
||||
if (opRes.status === 'fulfilled') {
|
||||
stats.opLogCount = opRes.value.total ?? 0;
|
||||
opLogs.value = opRes.value.list ?? [];
|
||||
// 本地数据模式:直接写入表格
|
||||
opGridApi.setGridOptions({ data: opLogs.value });
|
||||
}
|
||||
if (loginRes.status === 'fulfilled')
|
||||
if (loginRes.status === 'fulfilled') {
|
||||
loginLogs.value = loginRes.value.list ?? [];
|
||||
loginGridApi.setGridOptions({ data: loginLogs.value });
|
||||
}
|
||||
} catch {
|
||||
MessagePlugin.warning('部分数据加载失败');
|
||||
} finally {
|
||||
@ -224,33 +264,10 @@ onMounted(load);
|
||||
<section class="wb-grid">
|
||||
<div class="wb-col">
|
||||
<t-card title="最新操作日志" :bordered="false" class="wb-card">
|
||||
<t-table
|
||||
row-key="id"
|
||||
:data="opLogs"
|
||||
:columns="opColumns"
|
||||
:loading="loading"
|
||||
size="small"
|
||||
:pagination="false"
|
||||
/>
|
||||
<OpGrid />
|
||||
</t-card>
|
||||
<t-card title="最新登录日志" :bordered="false" class="wb-card">
|
||||
<t-table
|
||||
row-key="id"
|
||||
:data="loginLogs"
|
||||
:columns="loginColumns"
|
||||
:loading="loading"
|
||||
size="small"
|
||||
:pagination="false"
|
||||
>
|
||||
<template #statusText="{ row }">
|
||||
<t-tag
|
||||
:theme="row.status === 1 ? 'success' : 'danger'"
|
||||
variant="light"
|
||||
>
|
||||
{{ row.status === 1 ? '成功' : '失败' }}
|
||||
</t-tag>
|
||||
</template>
|
||||
</t-table>
|
||||
<LoginGrid />
|
||||
</t-card>
|
||||
</div>
|
||||
|
||||
@ -315,11 +332,7 @@ onMounted(load);
|
||||
width: 360px;
|
||||
height: 360px;
|
||||
pointer-events: none;
|
||||
background: radial-gradient(
|
||||
circle,
|
||||
rgb(255 255 255 / 35%),
|
||||
transparent 65%
|
||||
);
|
||||
background: radial-gradient(circle, rgb(255 255 255 / 35%), transparent 65%);
|
||||
border-radius: 50%;
|
||||
filter: blur(8px);
|
||||
animation: wb-float 9s ease-in-out infinite;
|
||||
|
||||
@ -1,13 +1,15 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { CommunityItem } from '#/api/house';
|
||||
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
import { h, reactive, ref } from 'vue';
|
||||
|
||||
import { useAccess } from '@vben/access';
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { Page, VbenTableAction } from '@vben/common-ui';
|
||||
|
||||
import { DialogPlugin, MessagePlugin } from 'tdesign-vue-next';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
createCommunity,
|
||||
deleteCommunity,
|
||||
@ -18,18 +20,24 @@ import {
|
||||
// 小区管理:列表 + 新增/编辑弹窗 + 删除(软删除)
|
||||
const { hasAccessByCodes } = useAccess();
|
||||
|
||||
const loading = ref(false);
|
||||
const list = ref<CommunityItem[]>([]);
|
||||
const dialogOpen = ref(false);
|
||||
const editing = ref<null | CommunityItem>(null);
|
||||
const editing = ref<CommunityItem | null>(null);
|
||||
|
||||
const regionOptions = [
|
||||
'云岩区', '南明区', '观山湖区', '花溪区', '乌当区', '白云区',
|
||||
'清镇市', '修文县', '开阳县', '息烽县',
|
||||
'云岩区',
|
||||
'南明区',
|
||||
'观山湖区',
|
||||
'花溪区',
|
||||
'乌当区',
|
||||
'白云区',
|
||||
'清镇市',
|
||||
'修文县',
|
||||
'开阳县',
|
||||
'息烽县',
|
||||
].map((r) => ({ label: r, value: r }));
|
||||
|
||||
const query = reactive({ page: 1, size: 10, keyword: '', region: '' });
|
||||
const pagination = reactive({ current: 1, pageSize: 10, total: 0 });
|
||||
// 筛选条件(分页由 grid 自动管理)
|
||||
const query = reactive({ keyword: '', region: '' });
|
||||
|
||||
const form = reactive({
|
||||
name: '',
|
||||
@ -47,23 +55,22 @@ const form = reactive({
|
||||
developer: '',
|
||||
});
|
||||
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const data = await getCommunityList(query);
|
||||
list.value = data.list ?? [];
|
||||
pagination.total = data.total ?? 0;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
editing.value = null;
|
||||
Object.assign(form, {
|
||||
name: '', region: '', businessDistrict: '', address: '', lng: 0, lat: 0,
|
||||
buildYear: 0, households: 0, plotRatio: 0, greenRate: 0,
|
||||
propertyCompany: '', propertyFee: 0, developer: '',
|
||||
name: '',
|
||||
region: '',
|
||||
businessDistrict: '',
|
||||
address: '',
|
||||
lng: 0,
|
||||
lat: 0,
|
||||
buildYear: 0,
|
||||
households: 0,
|
||||
plotRatio: 0,
|
||||
greenRate: 0,
|
||||
propertyCompany: '',
|
||||
propertyFee: 0,
|
||||
developer: '',
|
||||
});
|
||||
dialogOpen.value = true;
|
||||
}
|
||||
@ -75,48 +82,98 @@ function openEdit(row: CommunityItem) {
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
if (editing.value) {
|
||||
await updateCommunity(editing.value.id, form);
|
||||
} else {
|
||||
await createCommunity(form);
|
||||
}
|
||||
// 编辑走更新、新增走创建
|
||||
editing.value
|
||||
? await updateCommunity(editing.value.id, form)
|
||||
: await createCommunity(form);
|
||||
MessagePlugin.success('保存成功');
|
||||
dialogOpen.value = false;
|
||||
load();
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 删除小区(保留原确认弹窗行为) */
|
||||
function onDelete(row: CommunityItem) {
|
||||
const d = DialogPlugin.confirm({
|
||||
header: `确认删除「${row.name}」?`,
|
||||
onConfirm: async () => {
|
||||
await deleteCommunity(row.id);
|
||||
MessagePlugin.success('删除成功');
|
||||
load();
|
||||
gridApi.query();
|
||||
d.destroy();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{ colKey: 'id', title: 'ID', width: 70 },
|
||||
{ colKey: 'name', title: '小区名称', width: 180 },
|
||||
{ colKey: 'region', title: '区县', width: 100 },
|
||||
{ colKey: 'businessDistrict', title: '板块', width: 120 },
|
||||
{ colKey: 'address', title: '地址' },
|
||||
{ colKey: 'buildYear', title: '建成年份', width: 90 },
|
||||
{ colKey: 'propertyCompany', title: '物业', width: 140 },
|
||||
{ colKey: 'op', title: '操作', width: 160 },
|
||||
];
|
||||
const gridOptions: VxeTableGridOptions = {
|
||||
columns: [
|
||||
{ type: 'seq', width: 50, title: '#' },
|
||||
{ field: 'id', title: 'ID', width: 70 },
|
||||
{ field: 'name', title: '小区名称', width: 180 },
|
||||
{ field: 'region', title: '区县', width: 100 },
|
||||
{ field: 'businessDistrict', title: '板块', width: 120 },
|
||||
{ field: 'address', title: '地址', showOverflow: 'tooltip' },
|
||||
{ field: 'buildYear', title: '建成年份', width: 90 },
|
||||
{ field: 'propertyCompany', title: '物业', width: 140 },
|
||||
{
|
||||
field: 'op',
|
||||
title: '操作',
|
||||
width: 160,
|
||||
fixed: 'right',
|
||||
slots: {
|
||||
default: ({ row }: { row: CommunityItem }) =>
|
||||
h(VbenTableAction, {
|
||||
actions: [
|
||||
// 编辑:按权限码显隐
|
||||
{
|
||||
text: '编辑',
|
||||
onClick: () => openEdit(row),
|
||||
auth: ['house:community:update'],
|
||||
},
|
||||
// 删除(危险操作标红)
|
||||
{
|
||||
text: '删除',
|
||||
danger: true,
|
||||
onClick: () => onDelete(row),
|
||||
auth: ['house:community:delete'],
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
},
|
||||
],
|
||||
pagerConfig: { pageSize: 10 },
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({
|
||||
page,
|
||||
}: {
|
||||
page: { currentPage: number; pageSize: number };
|
||||
}) => {
|
||||
const data = await getCommunityList({
|
||||
page: page.currentPage,
|
||||
size: page.pageSize,
|
||||
keyword: query.keyword || undefined,
|
||||
region: query.region || undefined,
|
||||
});
|
||||
return { items: data.list ?? [], total: data.total ?? 0 };
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
function onPageChange(p: { current: number; pageSize: number }) {
|
||||
query.page = p.current;
|
||||
query.size = p.pageSize;
|
||||
pagination.current = p.current;
|
||||
pagination.pageSize = p.pageSize;
|
||||
load();
|
||||
const [Grid, gridApi] = useVbenVxeGrid({ gridOptions });
|
||||
|
||||
/** 触发查询(保留分页条件,因 grid 自动管理分页) */
|
||||
function search() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
onMounted(load);
|
||||
/** 重置筛选并回到第一页重新查询 */
|
||||
function reset() {
|
||||
query.keyword = '';
|
||||
query.region = '';
|
||||
gridApi.reload();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -128,7 +185,7 @@ onMounted(load);
|
||||
clearable
|
||||
placeholder="小区名称"
|
||||
style="width: 200px"
|
||||
@enter="load"
|
||||
@enter="search"
|
||||
/>
|
||||
<t-select
|
||||
v-model="query.region"
|
||||
@ -137,7 +194,8 @@ onMounted(load);
|
||||
placeholder="区县"
|
||||
style="width: 140px"
|
||||
/>
|
||||
<t-button theme="primary" @click="load">查询</t-button>
|
||||
<t-button theme="primary" @click="search">查询</t-button>
|
||||
<t-button variant="outline" @click="reset">重置</t-button>
|
||||
</div>
|
||||
<t-button
|
||||
v-if="hasAccessByCodes(['house:community:create'])"
|
||||
@ -148,35 +206,8 @@ onMounted(load);
|
||||
</t-button>
|
||||
</div>
|
||||
|
||||
<t-table
|
||||
row-key="id"
|
||||
:data="list"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
@page-change="onPageChange"
|
||||
>
|
||||
<template #op="{ row }">
|
||||
<t-button
|
||||
v-if="hasAccessByCodes(['house:community:update'])"
|
||||
size="small"
|
||||
theme="primary"
|
||||
variant="text"
|
||||
@click="openEdit(row)"
|
||||
>
|
||||
编辑
|
||||
</t-button>
|
||||
<t-button
|
||||
v-if="hasAccessByCodes(['house:community:delete'])"
|
||||
size="small"
|
||||
theme="danger"
|
||||
variant="text"
|
||||
@click="onDelete(row)"
|
||||
>
|
||||
删除
|
||||
</t-button>
|
||||
</template>
|
||||
</t-table>
|
||||
<!-- 小区列表(Vben Vxe Table + TableAction) -->
|
||||
<Grid />
|
||||
|
||||
<t-dialog
|
||||
v-model:visible="dialogOpen"
|
||||
@ -190,7 +221,11 @@ onMounted(load);
|
||||
<t-input v-model="form.name" placeholder="如:中天未来方舟" />
|
||||
</t-form-item>
|
||||
<t-form-item label="区县">
|
||||
<t-select v-model="form.region" clearable :options="regionOptions" />
|
||||
<t-select
|
||||
v-model="form.region"
|
||||
clearable
|
||||
:options="regionOptions"
|
||||
/>
|
||||
</t-form-item>
|
||||
<t-form-item label="板块">
|
||||
<t-input v-model="form.businessDistrict" placeholder="如:会展城" />
|
||||
@ -208,10 +243,18 @@ onMounted(load);
|
||||
<t-input v-model="form.address" />
|
||||
</t-form-item>
|
||||
<t-form-item label="经度">
|
||||
<t-input-number v-model="form.lng" theme="normal" :decimal-places="6" />
|
||||
<t-input-number
|
||||
v-model="form.lng"
|
||||
theme="normal"
|
||||
:decimal-places="6"
|
||||
/>
|
||||
</t-form-item>
|
||||
<t-form-item label="纬度">
|
||||
<t-input-number v-model="form.lat" theme="normal" :decimal-places="6" />
|
||||
<t-input-number
|
||||
v-model="form.lat"
|
||||
theme="normal"
|
||||
:decimal-places="6"
|
||||
/>
|
||||
</t-form-item>
|
||||
</div>
|
||||
</t-form>
|
||||
|
||||
@ -1,16 +1,14 @@
|
||||
<script lang="ts" setup>
|
||||
import type { TransactionItem } from '#/api/house';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
import { reactive } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getTransactionList } from '#/api/house';
|
||||
|
||||
// 数据明细:成交记录列表(价格快照/配套/学区划片在后续阶段接入)
|
||||
const loading = ref(false);
|
||||
const list = ref<TransactionItem[]>([]);
|
||||
|
||||
// 数据明细:成交记录列表(Vben Vxe Table,价格快照/配套/学区划片在后续阶段接入)
|
||||
const regionOptions = [
|
||||
'云岩区',
|
||||
'南明区',
|
||||
@ -24,49 +22,55 @@ const regionOptions = [
|
||||
'息烽县',
|
||||
].map((r) => ({ label: r, value: r }));
|
||||
|
||||
const query = reactive({ page: 1, size: 10, keyword: '', region: '' });
|
||||
const pagination = reactive({ current: 1, pageSize: 10, total: 0 });
|
||||
// 筛选条件(分页由 grid 自动管理)
|
||||
const query = reactive({ keyword: '', region: '' });
|
||||
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const data = await getTransactionList(query);
|
||||
list.value = data.list ?? [];
|
||||
pagination.total = data.total ?? 0;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
const gridOptions: VxeTableGridOptions = {
|
||||
columns: [
|
||||
{ type: 'seq', width: 50, title: '#' },
|
||||
{ field: 'id', title: 'ID', width: 70 },
|
||||
{ field: 'communityName', title: '小区', width: 180 },
|
||||
{ field: 'layout', title: '户型', width: 100 },
|
||||
{ field: 'area', title: '面积㎡', width: 90 },
|
||||
{ field: 'dealPrice', title: '成交价(万)', width: 110 },
|
||||
{ field: 'dealUnitPrice', title: '成交单价', width: 110 },
|
||||
{ field: 'listDays', title: '挂牌天数', width: 90 },
|
||||
{ field: 'dealDate', title: '成交日期', width: 120 },
|
||||
{ field: 'source', title: '来源', width: 90 },
|
||||
],
|
||||
pagerConfig: { pageSize: 10 },
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({
|
||||
page,
|
||||
}: {
|
||||
page: { currentPage: number; pageSize: number };
|
||||
}) => {
|
||||
const data = await getTransactionList({
|
||||
page: page.currentPage,
|
||||
size: page.pageSize,
|
||||
keyword: query.keyword,
|
||||
region: query.region,
|
||||
});
|
||||
return { items: data.list ?? [], total: data.total ?? 0 };
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({ gridOptions });
|
||||
|
||||
/** 触发查询(保留分页条件,因 grid 自动管理分页) */
|
||||
function search() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 重置筛选并回到第一页重新查询 */
|
||||
function reset() {
|
||||
query.keyword = '';
|
||||
query.region = '';
|
||||
query.page = 1;
|
||||
pagination.current = 1;
|
||||
load();
|
||||
gridApi.reload();
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{ colKey: 'id', title: 'ID', width: 70 },
|
||||
{ colKey: 'communityName', title: '小区', width: 180 },
|
||||
{ colKey: 'layout', title: '户型', width: 100 },
|
||||
{ colKey: 'area', title: '面积㎡', width: 90 },
|
||||
{ colKey: 'dealPrice', title: '成交价(万)', width: 110 },
|
||||
{ colKey: 'dealUnitPrice', title: '成交单价', width: 110 },
|
||||
{ colKey: 'listDays', title: '挂牌天数', width: 90 },
|
||||
{ colKey: 'dealDate', title: '成交日期', width: 120 },
|
||||
{ colKey: 'source', title: '来源', width: 90 },
|
||||
];
|
||||
|
||||
function onPageChange(p: { current: number; pageSize: number }) {
|
||||
query.page = p.current;
|
||||
query.size = p.pageSize;
|
||||
pagination.current = p.current;
|
||||
pagination.pageSize = p.pageSize;
|
||||
load();
|
||||
}
|
||||
|
||||
onMounted(load);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -77,7 +81,7 @@ onMounted(load);
|
||||
clearable
|
||||
placeholder="小区名/户型"
|
||||
style="width: 200px"
|
||||
@enter="load"
|
||||
@enter="search"
|
||||
/>
|
||||
<t-select
|
||||
v-model="query.region"
|
||||
@ -86,17 +90,11 @@ onMounted(load);
|
||||
placeholder="区县"
|
||||
style="width: 140px"
|
||||
/>
|
||||
<t-button theme="primary" @click="load">查询</t-button>
|
||||
<t-button theme="primary" @click="search">查询</t-button>
|
||||
<t-button variant="outline" @click="reset">重置</t-button>
|
||||
</div>
|
||||
|
||||
<t-table
|
||||
row-key="id"
|
||||
:data="list"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
@page-change="onPageChange"
|
||||
/>
|
||||
<!-- 成交记录列表(Vben Vxe Table) -->
|
||||
<Grid />
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
@ -1,13 +1,15 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { ListingItem } from '#/api/house';
|
||||
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
import { h, reactive, ref } from 'vue';
|
||||
|
||||
import { useAccess } from '@vben/access';
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { Page, VbenTableAction } from '@vben/common-ui';
|
||||
|
||||
import { DialogPlugin, MessagePlugin } from 'tdesign-vue-next';
|
||||
import { DialogPlugin, MessagePlugin, Tag } from 'tdesign-vue-next';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
batchMarkListing,
|
||||
deleteListing,
|
||||
@ -18,20 +20,24 @@ import {
|
||||
// 房源管理:多维筛选 + 多选批量标记(笋盘/低可信/状态)+ 编辑/删除
|
||||
const { hasAccessByCodes } = useAccess();
|
||||
|
||||
const loading = ref(false);
|
||||
const list = ref<ListingItem[]>([]);
|
||||
const selectedKeys = ref<number[]>([]);
|
||||
const dialogOpen = ref(false);
|
||||
const editing = ref<null | ListingItem>(null);
|
||||
const editing = ref<ListingItem | null>(null);
|
||||
|
||||
const regionOptions = [
|
||||
'云岩区', '南明区', '观山湖区', '花溪区', '乌当区', '白云区',
|
||||
'清镇市', '修文县', '开阳县', '息烽县',
|
||||
'云岩区',
|
||||
'南明区',
|
||||
'观山湖区',
|
||||
'花溪区',
|
||||
'乌当区',
|
||||
'白云区',
|
||||
'清镇市',
|
||||
'修文县',
|
||||
'开阳县',
|
||||
'息烽县',
|
||||
].map((r) => ({ label: r, value: r }));
|
||||
|
||||
// 筛选条件(分页由 grid 自动管理)
|
||||
const query = reactive({
|
||||
page: 1,
|
||||
size: 10,
|
||||
keyword: '',
|
||||
region: '',
|
||||
layout: '',
|
||||
@ -42,42 +48,23 @@ const query = reactive({
|
||||
isBargain: 0,
|
||||
confidence: 0,
|
||||
});
|
||||
const pagination = reactive({ current: 1, pageSize: 10, total: 0 });
|
||||
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const data = await getListingList(query);
|
||||
list.value = data.list ?? [];
|
||||
pagination.total = data.total ?? 0;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function reset() {
|
||||
Object.assign(query, {
|
||||
keyword: '', region: '', layout: '', source: '',
|
||||
priceMin: undefined, priceMax: undefined, status: 0, isBargain: 0, confidence: 0,
|
||||
});
|
||||
query.page = 1;
|
||||
pagination.current = 1;
|
||||
load();
|
||||
}
|
||||
|
||||
function onSelectChange(keys: Array<string | number>) {
|
||||
selectedKeys.value = keys.map(Number);
|
||||
/** 收集当前勾选的房源 ID */
|
||||
function getSelectedIds(): number[] {
|
||||
const records = (gridApi.grid?.getCheckboxRecords() ?? []) as ListingItem[];
|
||||
return records.map((r) => Number(r.id));
|
||||
}
|
||||
|
||||
/** 批量标记(笋盘/低可信/状态) */
|
||||
async function onBatchMark(field: string, value: number) {
|
||||
if (selectedKeys.value.length === 0) {
|
||||
const ids = getSelectedIds();
|
||||
if (ids.length === 0) {
|
||||
MessagePlugin.warning('请先勾选房源');
|
||||
return;
|
||||
}
|
||||
await batchMarkListing({ ids: selectedKeys.value, field, value });
|
||||
await batchMarkListing({ ids, field, value });
|
||||
MessagePlugin.success('标记成功');
|
||||
load();
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
function openEdit(row: ListingItem) {
|
||||
@ -98,45 +85,174 @@ async function submit() {
|
||||
}
|
||||
MessagePlugin.success('保存成功');
|
||||
dialogOpen.value = false;
|
||||
load();
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 删除房源(保留原确认弹窗行为) */
|
||||
function onDelete(row: ListingItem) {
|
||||
const d = DialogPlugin.confirm({
|
||||
header: '确认删除该房源?',
|
||||
onConfirm: async () => {
|
||||
await deleteListing(row.id);
|
||||
MessagePlugin.success('删除成功');
|
||||
load();
|
||||
gridApi.query();
|
||||
d.destroy();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{ colKey: 'row-select', type: 'multiple', width: 46 },
|
||||
{ colKey: 'id', title: 'ID', width: 70 },
|
||||
{ colKey: 'communityName', title: '小区', width: 160 },
|
||||
{ colKey: 'layout', title: '户型', width: 100 },
|
||||
{ colKey: 'area', title: '面积㎡', width: 90 },
|
||||
{ colKey: 'totalPrice', title: '总价(万)', width: 100 },
|
||||
{ colKey: 'unitPrice', title: '单价', width: 100 },
|
||||
{ colKey: 'source', title: '来源', width: 90 },
|
||||
{ colKey: 'onMarketDays', title: '挂牌天数', width: 90 },
|
||||
{ colKey: 'statusText', title: '状态', width: 80 },
|
||||
{ colKey: 'flagText', title: '标记', width: 140 },
|
||||
{ colKey: 'op', title: '操作', width: 140 },
|
||||
];
|
||||
|
||||
function onPageChange(p: { current: number; pageSize: number }) {
|
||||
query.page = p.current;
|
||||
query.size = p.pageSize;
|
||||
pagination.current = p.current;
|
||||
pagination.pageSize = p.pageSize;
|
||||
load();
|
||||
/** 状态码 -> TDesign 标签主题色(1 在售 / 2 下架 / 3 成交) */
|
||||
function statusTheme(status: number) {
|
||||
if (status === 1) return 'success';
|
||||
if (status === 2) return 'default';
|
||||
return 'primary';
|
||||
}
|
||||
|
||||
onMounted(load);
|
||||
/** 状态码 -> 文案 */
|
||||
function statusText(status: number) {
|
||||
if (status === 1) return '在售';
|
||||
if (status === 2) return '下架';
|
||||
return '成交';
|
||||
}
|
||||
|
||||
const gridOptions: VxeTableGridOptions = {
|
||||
columns: [
|
||||
{ type: 'checkbox', width: 46 }, // 多选列(配合批量标记)
|
||||
{ field: 'id', title: 'ID', width: 70 },
|
||||
{ field: 'communityName', title: '小区', width: 160 },
|
||||
{ field: 'layout', title: '户型', width: 100 },
|
||||
{ field: 'area', title: '面积㎡', width: 90 },
|
||||
{ field: 'totalPrice', title: '总价(万)', width: 100 },
|
||||
{ field: 'unitPrice', title: '单价', width: 100 },
|
||||
{ field: 'source', title: '来源', width: 90 },
|
||||
{ field: 'onMarketDays', title: '挂牌天数', width: 90 },
|
||||
{
|
||||
field: 'statusText',
|
||||
title: '状态',
|
||||
width: 80,
|
||||
slots: {
|
||||
default: ({ row }: { row: ListingItem }) =>
|
||||
h(
|
||||
Tag,
|
||||
{
|
||||
theme: statusTheme(row.status),
|
||||
variant: 'light',
|
||||
size: 'small',
|
||||
},
|
||||
statusText(row.status),
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'flagText',
|
||||
title: '标记',
|
||||
width: 140,
|
||||
slots: {
|
||||
default: ({ row }: { row: ListingItem }) => {
|
||||
const tags: ReturnType<typeof h>[] = [];
|
||||
if (row.isBargain === 1) {
|
||||
tags.push(
|
||||
h(
|
||||
Tag,
|
||||
{ theme: 'warning', variant: 'light', size: 'small' },
|
||||
'笋盘',
|
||||
),
|
||||
);
|
||||
}
|
||||
if (row.confidence === 1) {
|
||||
tags.push(
|
||||
h(
|
||||
Tag,
|
||||
{ theme: 'danger', variant: 'light', size: 'small' },
|
||||
'低可信',
|
||||
),
|
||||
);
|
||||
}
|
||||
return h(
|
||||
'span',
|
||||
{ class: 'flex items-center justify-center gap-1' },
|
||||
{ default: () => tags },
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'op',
|
||||
title: '操作',
|
||||
width: 140,
|
||||
fixed: 'right',
|
||||
slots: {
|
||||
default: ({ row }: { row: ListingItem }) =>
|
||||
h(VbenTableAction, {
|
||||
actions: [
|
||||
// 编辑:按权限码显隐
|
||||
{
|
||||
text: '编辑',
|
||||
onClick: () => openEdit(row),
|
||||
auth: ['house:listing:update'],
|
||||
},
|
||||
// 删除(危险操作标红)
|
||||
{
|
||||
text: '删除',
|
||||
danger: true,
|
||||
onClick: () => onDelete(row),
|
||||
auth: ['house:listing:delete'],
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
},
|
||||
],
|
||||
checkboxConfig: { highlight: true }, // 勾选行高亮,便于批量标记定位
|
||||
pagerConfig: { pageSize: 10 },
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({
|
||||
page,
|
||||
}: {
|
||||
page: { currentPage: number; pageSize: number };
|
||||
}) => {
|
||||
const data = await getListingList({
|
||||
page: page.currentPage,
|
||||
size: page.pageSize,
|
||||
keyword: query.keyword || undefined,
|
||||
region: query.region || undefined,
|
||||
layout: query.layout || undefined,
|
||||
source: query.source || undefined,
|
||||
priceMin: query.priceMin || undefined,
|
||||
priceMax: query.priceMax || undefined,
|
||||
status: query.status || undefined,
|
||||
isBargain: query.isBargain || undefined,
|
||||
confidence: query.confidence || undefined,
|
||||
});
|
||||
return { items: data.list ?? [], total: data.total ?? 0 };
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({ gridOptions });
|
||||
|
||||
/** 触发查询(保留分页条件,因 grid 自动管理分页) */
|
||||
function search() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 重置筛选并回到第一页重新查询 */
|
||||
function reset() {
|
||||
Object.assign(query, {
|
||||
keyword: '',
|
||||
region: '',
|
||||
layout: '',
|
||||
source: '',
|
||||
priceMin: undefined,
|
||||
priceMax: undefined,
|
||||
status: 0,
|
||||
isBargain: 0,
|
||||
confidence: 0,
|
||||
});
|
||||
gridApi.reload();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -147,7 +263,7 @@ onMounted(load);
|
||||
clearable
|
||||
placeholder="房号/户型"
|
||||
style="width: 160px"
|
||||
@enter="load"
|
||||
@enter="search"
|
||||
/>
|
||||
<t-select
|
||||
v-model="query.region"
|
||||
@ -184,7 +300,7 @@ onMounted(load);
|
||||
]"
|
||||
style="width: 110px"
|
||||
/>
|
||||
<t-button theme="primary" @click="load">查询</t-button>
|
||||
<t-button theme="primary" @click="search">查询</t-button>
|
||||
<t-button variant="outline" @click="reset">重置</t-button>
|
||||
<span class="ml-auto flex items-center gap-2">
|
||||
<t-button
|
||||
@ -204,52 +320,8 @@ onMounted(load);
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<t-table
|
||||
row-key="id"
|
||||
:data="list"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
:selected-row-keys="selectedKeys"
|
||||
@page-change="onPageChange"
|
||||
@select-change="onSelectChange"
|
||||
>
|
||||
<template #statusText="{ row }">
|
||||
<t-tag
|
||||
:theme="row.status === 1 ? 'success' : row.status === 2 ? 'default' : 'primary'"
|
||||
variant="light"
|
||||
size="small"
|
||||
>
|
||||
{{ row.status === 1 ? '在售' : row.status === 2 ? '下架' : '成交' }}
|
||||
</t-tag>
|
||||
</template>
|
||||
<template #flagText="{ row }">
|
||||
<span class="flex items-center gap-1">
|
||||
<t-tag v-if="row.isBargain === 1" theme="warning" variant="light" size="small">笋盘</t-tag>
|
||||
<t-tag v-if="row.confidence === 1" theme="danger" variant="light" size="small">低可信</t-tag>
|
||||
</span>
|
||||
</template>
|
||||
<template #op="{ row }">
|
||||
<t-button
|
||||
v-if="hasAccessByCodes(['house:listing:update'])"
|
||||
size="small"
|
||||
theme="primary"
|
||||
variant="text"
|
||||
@click="openEdit(row)"
|
||||
>
|
||||
编辑
|
||||
</t-button>
|
||||
<t-button
|
||||
v-if="hasAccessByCodes(['house:listing:delete'])"
|
||||
size="small"
|
||||
theme="danger"
|
||||
variant="text"
|
||||
@click="onDelete(row)"
|
||||
>
|
||||
删除
|
||||
</t-button>
|
||||
</template>
|
||||
</t-table>
|
||||
<!-- 房源列表(Vben Vxe Table + TableAction) -->
|
||||
<Grid />
|
||||
|
||||
<t-dialog
|
||||
v-model:visible="dialogOpen"
|
||||
|
||||
@ -1,16 +1,14 @@
|
||||
<script lang="ts" setup>
|
||||
import type { PresaleItem } from '#/api/house';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
import { reactive } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getPresaleList } from '#/api/house';
|
||||
|
||||
// 新房预售:预售许可证列表(数据源:贵阳市住建局预售公示)
|
||||
const loading = ref(false);
|
||||
const list = ref<PresaleItem[]>([]);
|
||||
|
||||
// 新房预售:预售许可证列表(Vben Vxe Table,数据源:贵阳市住建局预售公示)
|
||||
const regionOptions = [
|
||||
'云岩区',
|
||||
'南明区',
|
||||
@ -30,57 +28,63 @@ const purposeOptions = [
|
||||
{ label: '商业', value: '商业' },
|
||||
];
|
||||
|
||||
const query = reactive({
|
||||
page: 1,
|
||||
size: 10,
|
||||
keyword: '',
|
||||
region: '',
|
||||
purpose: '',
|
||||
});
|
||||
const pagination = reactive({ current: 1, pageSize: 10, total: 0 });
|
||||
// 筛选条件(分页由 grid 自动管理)
|
||||
const query = reactive({ keyword: '', region: '', purpose: '' });
|
||||
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const data = await getPresaleList(query);
|
||||
list.value = data.list ?? [];
|
||||
pagination.total = data.total ?? 0;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
const gridOptions: VxeTableGridOptions = {
|
||||
columns: [
|
||||
{ type: 'seq', width: 50, title: '#' },
|
||||
{ field: 'presaleNo', title: '预售证号', width: 190 },
|
||||
{ field: 'communityName', title: '楼盘/项目名', width: 200 },
|
||||
{
|
||||
field: 'developer',
|
||||
title: '开发商',
|
||||
width: 200,
|
||||
showOverflow: 'tooltip',
|
||||
},
|
||||
{ field: 'region', title: '区县', width: 100 },
|
||||
{ field: 'address', title: '位置', width: 220, showOverflow: 'tooltip' },
|
||||
{ field: 'buildingNo', title: '楼栋号', width: 110 },
|
||||
{ field: 'houseCount', title: '套数', width: 70 },
|
||||
{ field: 'area', title: '面积㎡', width: 100 },
|
||||
{ field: 'purpose', title: '用途', width: 80 },
|
||||
{ field: 'issueDate', title: '发证日期', width: 120 },
|
||||
],
|
||||
pagerConfig: { pageSize: 10 },
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({
|
||||
page,
|
||||
}: {
|
||||
page: { currentPage: number; pageSize: number };
|
||||
}) => {
|
||||
const data = await getPresaleList({
|
||||
page: page.currentPage,
|
||||
size: page.pageSize,
|
||||
keyword: query.keyword || undefined,
|
||||
region: query.region || undefined,
|
||||
purpose: query.purpose || undefined,
|
||||
});
|
||||
return { items: data.list ?? [], total: data.total ?? 0 };
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({ gridOptions });
|
||||
|
||||
/** 触发查询(保留分页条件,因 grid 自动管理分页) */
|
||||
function search() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 重置筛选并回到第一页重新查询 */
|
||||
function reset() {
|
||||
query.keyword = '';
|
||||
query.region = '';
|
||||
query.purpose = '';
|
||||
query.page = 1;
|
||||
pagination.current = 1;
|
||||
load();
|
||||
gridApi.reload();
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{ colKey: 'presaleNo', title: '预售证号', width: 190 },
|
||||
{ colKey: 'communityName', title: '楼盘/项目名', width: 200 },
|
||||
{ colKey: 'developer', title: '开发商', width: 200 },
|
||||
{ colKey: 'region', title: '区县', width: 100 },
|
||||
{ colKey: 'address', title: '位置', width: 220, ellipsis: true },
|
||||
{ colKey: 'buildingNo', title: '楼栋号', width: 110 },
|
||||
{ colKey: 'houseCount', title: '套数', width: 70 },
|
||||
{ colKey: 'area', title: '面积㎡', width: 100 },
|
||||
{ colKey: 'purpose', title: '用途', width: 80 },
|
||||
{ colKey: 'issueDate', title: '发证日期', width: 120 },
|
||||
];
|
||||
|
||||
function onPageChange(p: { current: number; pageSize: number }) {
|
||||
query.page = p.current;
|
||||
query.size = p.pageSize;
|
||||
pagination.current = p.current;
|
||||
pagination.pageSize = p.pageSize;
|
||||
load();
|
||||
}
|
||||
|
||||
onMounted(load);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -91,7 +95,7 @@ onMounted(load);
|
||||
clearable
|
||||
placeholder="楼盘名/开发商/预售证号"
|
||||
style="width: 240px"
|
||||
@enter="load"
|
||||
@enter="search"
|
||||
/>
|
||||
<t-select
|
||||
v-model="query.region"
|
||||
@ -107,17 +111,11 @@ onMounted(load);
|
||||
placeholder="用途"
|
||||
style="width: 120px"
|
||||
/>
|
||||
<t-button theme="primary" @click="load">查询</t-button>
|
||||
<t-button theme="primary" @click="search">查询</t-button>
|
||||
<t-button variant="outline" @click="reset">重置</t-button>
|
||||
</div>
|
||||
|
||||
<t-table
|
||||
row-key="id"
|
||||
:data="list"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
@page-change="onPageChange"
|
||||
/>
|
||||
<!-- 预售许可列表(Vben Vxe Table) -->
|
||||
<Grid />
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
@ -1,31 +1,27 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { SourceItem } from '#/api/recruitment';
|
||||
|
||||
// 数据源与抓取运维:数据源列表 + 手动触发抓取 + 运行摘要。
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { h, onMounted, ref } from 'vue';
|
||||
|
||||
import { useAccess } from '@vben/access';
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { Page, VbenTableAction } from '@vben/common-ui';
|
||||
|
||||
import { DialogPlugin, MessagePlugin } from 'tdesign-vue-next';
|
||||
import { DialogPlugin, MessagePlugin, Tag } from 'tdesign-vue-next';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getSourceList, triggerCrawl } from '#/api/recruitment';
|
||||
|
||||
const { hasAccessByCodes } = useAccess();
|
||||
|
||||
const loading = ref(false);
|
||||
const list = ref<SourceItem[]>([]);
|
||||
const crawling = ref(false);
|
||||
const runningSummary = ref('');
|
||||
|
||||
/** 拉取数据源列表并写入本地数据表格 */
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const data = await getSourceList();
|
||||
list.value = data.list ?? [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
const data = await getSourceList();
|
||||
gridApi.setGridOptions({ data: data.list ?? [] });
|
||||
}
|
||||
|
||||
function onTriggerAll() {
|
||||
@ -55,16 +51,79 @@ async function doTrigger(sourceId: number, force: boolean) {
|
||||
}
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{ colKey: 'id', title: 'ID', width: 70 },
|
||||
{ colKey: 'name', title: '数据源', minWidth: 200 },
|
||||
{ colKey: 'region', title: '地区', width: 100 },
|
||||
{ colKey: 'baseUrl', title: '地址', minWidth: 220, ellipsis: true },
|
||||
{ colKey: 'enabled', title: '启用', width: 80 },
|
||||
{ colKey: 'lastSuccessAt', title: '上次成功', width: 160 },
|
||||
{ colKey: 'failCount', title: '连续失败', width: 90 },
|
||||
{ colKey: 'op', title: '操作', width: 100, fixed: 'right' },
|
||||
];
|
||||
// 数据源表格(本地数据模式:关闭 proxyConfig,数据由 load() 写入)
|
||||
const gridOptions: VxeTableGridOptions = {
|
||||
columns: [
|
||||
{ type: 'seq', width: 50, title: '#' },
|
||||
{ field: 'id', title: 'ID', width: 70 },
|
||||
{ field: 'name', title: '数据源', minWidth: 200 },
|
||||
{ field: 'region', title: '地区', width: 100 },
|
||||
{ field: 'baseUrl', title: '地址', minWidth: 220, showOverflow: 'tooltip' },
|
||||
{
|
||||
field: 'enabled',
|
||||
title: '启用',
|
||||
width: 80,
|
||||
slots: {
|
||||
default: ({ row }: { row: SourceItem }) =>
|
||||
h(
|
||||
Tag,
|
||||
{
|
||||
theme: row.enabled === 1 ? 'success' : 'default',
|
||||
variant: 'light',
|
||||
size: 'small',
|
||||
},
|
||||
row.enabled === 1 ? '已启用' : '已停用',
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'lastSuccessAt',
|
||||
title: '上次成功',
|
||||
width: 160,
|
||||
slots: {
|
||||
default: ({ row }: { row: SourceItem }) =>
|
||||
h('span', row.lastSuccessAt || '—'),
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'failCount',
|
||||
title: '连续失败',
|
||||
width: 90,
|
||||
slots: {
|
||||
default: ({ row }: { row: SourceItem }) =>
|
||||
h(
|
||||
'span',
|
||||
{ class: row.failCount > 0 ? 'text-red-500' : '' },
|
||||
String(row.failCount),
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'op',
|
||||
title: '操作',
|
||||
width: 100,
|
||||
fixed: 'right',
|
||||
slots: {
|
||||
default: ({ row }: { row: SourceItem }) =>
|
||||
h(VbenTableAction, {
|
||||
actions: [
|
||||
// 单源手动抓取:停用状态禁用,按权限码显隐
|
||||
{
|
||||
text: '抓取',
|
||||
onClick: () => onTriggerOne(row),
|
||||
auth: ['recruitment:crawler:trigger'],
|
||||
disabled: row.enabled !== 1,
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
},
|
||||
],
|
||||
pagerConfig: { enabled: false }, // 数据源通常较少,不分页
|
||||
proxyConfig: { enabled: false }, // 本地数据模式
|
||||
};
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({ gridOptions });
|
||||
|
||||
onMounted(load);
|
||||
</script>
|
||||
@ -83,38 +142,8 @@ onMounted(load);
|
||||
<span class="text-xs opacity-60">定时任务:每日 03:00 自动抓取</span>
|
||||
</div>
|
||||
|
||||
<t-table row-key="id" :data="list" :columns="columns" :loading="loading">
|
||||
<template #enabled="{ row }">
|
||||
<t-tag
|
||||
:theme="row.enabled === 1 ? 'success' : 'default'"
|
||||
variant="light"
|
||||
size="small"
|
||||
>
|
||||
{{ row.enabled === 1 ? '已启用' : '已停用' }}
|
||||
</t-tag>
|
||||
</template>
|
||||
<template #lastSuccessAt="{ row }">
|
||||
{{ row.lastSuccessAt || '—' }}
|
||||
</template>
|
||||
<template #failCount="{ row }">
|
||||
<span :class="row.failCount > 0 ? 'text-[#E34D59]' : ''">{{
|
||||
row.failCount
|
||||
}}</span>
|
||||
</template>
|
||||
<template #op="{ row }">
|
||||
<t-button
|
||||
v-if="hasAccessByCodes(['recruitment:crawler:trigger'])"
|
||||
size="small"
|
||||
theme="primary"
|
||||
variant="text"
|
||||
:disabled="row.enabled !== 1"
|
||||
:loading="crawling"
|
||||
@click="onTriggerOne(row)"
|
||||
>
|
||||
抓取
|
||||
</t-button>
|
||||
</template>
|
||||
</t-table>
|
||||
<!-- 数据源列表(Vben Vxe Table) -->
|
||||
<Grid />
|
||||
|
||||
<div v-if="runningSummary" class="mt-4 rounded-lg border p-3">
|
||||
<p class="mb-1 text-sm font-medium">本次运行摘要</p>
|
||||
|
||||
@ -1,13 +1,15 @@
|
||||
<script lang="ts" setup>
|
||||
// 招聘公告列表:多维筛选 + 表格 + 详情抽屉。
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
// 招聘公告列表:多维筛选 + Vben Vxe Table + Vben TableAction 行操作 + 详情抽屉。
|
||||
import type { RecruitmentItem } from '#/api/recruitment';
|
||||
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
import { h, reactive, ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { Page, VbenTableAction } from '@vben/common-ui';
|
||||
|
||||
import { MessagePlugin } from 'tdesign-vue-next';
|
||||
import { MessagePlugin, Tag } from 'tdesign-vue-next';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
CATEGORY_OPTIONS,
|
||||
getRecruitmentDetail,
|
||||
@ -16,18 +18,13 @@ import {
|
||||
STATUS_OPTIONS,
|
||||
} from '#/api/recruitment';
|
||||
|
||||
const loading = ref(false);
|
||||
const list = ref<RecruitmentItem[]>([]);
|
||||
const pagination = reactive({ current: 1, pageSize: 10, total: 0 });
|
||||
|
||||
// 详情抽屉
|
||||
const detailOpen = ref(false);
|
||||
const detailLoading = ref(false);
|
||||
const detail = ref<null | RecruitmentItem>(null);
|
||||
|
||||
// 筛选条件(分页由 grid 自动管理)
|
||||
const query = reactive({
|
||||
page: 1,
|
||||
size: 10,
|
||||
region: '',
|
||||
category: 0,
|
||||
keyword: '',
|
||||
@ -39,29 +36,94 @@ const query = reactive({
|
||||
onlyNewToday: false,
|
||||
});
|
||||
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const data = await getRecruitmentList({
|
||||
page: query.page,
|
||||
size: query.size,
|
||||
region: query.region || undefined,
|
||||
category: query.category || undefined,
|
||||
keyword: query.keyword || undefined,
|
||||
status: query.status < 0 ? undefined : query.status,
|
||||
dateFrom: query.dateFrom || undefined,
|
||||
dateTo: query.dateTo || undefined,
|
||||
orgName: query.orgName || undefined,
|
||||
sourceId: query.sourceId || undefined,
|
||||
onlyNewToday: query.onlyNewToday || undefined,
|
||||
});
|
||||
list.value = data.list ?? [];
|
||||
pagination.total = data.total ?? 0;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
// 表格列定义(statusName/op 用 slots 函数式渲染)
|
||||
/** 状态码 -> TDesign 标签主题色(0 招集中 / 1 报名中 / 其余默认) */
|
||||
function statusTheme(status: number) {
|
||||
if (status === 0) return 'success';
|
||||
if (status === 1) return 'warning';
|
||||
return 'default';
|
||||
}
|
||||
|
||||
const gridOptions: VxeTableGridOptions = {
|
||||
columns: [
|
||||
{ type: 'seq', width: 50, title: '#' },
|
||||
{ field: 'id', title: 'ID', width: 70 },
|
||||
{ field: 'title', title: '标题', minWidth: 260, showOverflow: 'tooltip' },
|
||||
{ field: 'categoryName', title: '分类', width: 90 },
|
||||
{
|
||||
field: 'orgName',
|
||||
title: '发布单位',
|
||||
width: 180,
|
||||
showOverflow: 'tooltip',
|
||||
},
|
||||
{ field: 'region', title: '地区', width: 90 },
|
||||
{ field: 'publishDate', title: '发布日期', width: 110 },
|
||||
{ field: 'deadline', title: '截止', width: 110 },
|
||||
{
|
||||
field: 'statusName',
|
||||
title: '状态',
|
||||
width: 90,
|
||||
slots: {
|
||||
default: ({ row }: { row: RecruitmentItem }) =>
|
||||
h(
|
||||
Tag,
|
||||
{
|
||||
theme: statusTheme(row.status),
|
||||
variant: 'light',
|
||||
size: 'small',
|
||||
},
|
||||
row.statusName,
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'op',
|
||||
title: '操作',
|
||||
width: 90,
|
||||
fixed: 'right',
|
||||
slots: {
|
||||
default: ({ row }: { row: RecruitmentItem }) =>
|
||||
h(VbenTableAction, {
|
||||
actions: [{ text: '详情', onClick: () => openDetail(row) }],
|
||||
}),
|
||||
},
|
||||
},
|
||||
],
|
||||
pagerConfig: { pageSize: 10 },
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({
|
||||
page,
|
||||
}: {
|
||||
page: { currentPage: number; pageSize: number };
|
||||
}) => {
|
||||
const data = await getRecruitmentList({
|
||||
page: page.currentPage,
|
||||
size: page.pageSize,
|
||||
region: query.region || undefined,
|
||||
category: query.category || undefined,
|
||||
keyword: query.keyword || undefined,
|
||||
status: query.status < 0 ? undefined : query.status,
|
||||
dateFrom: query.dateFrom || undefined,
|
||||
dateTo: query.dateTo || undefined,
|
||||
orgName: query.orgName || undefined,
|
||||
sourceId: query.sourceId || undefined,
|
||||
onlyNewToday: query.onlyNewToday || undefined,
|
||||
});
|
||||
return { items: data.list ?? [], total: data.total ?? 0 };
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({ gridOptions });
|
||||
|
||||
/** 触发查询(保留分页条件,因 grid 自动管理分页) */
|
||||
function search() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 重置筛选并回到第一页重新查询 */
|
||||
function reset() {
|
||||
Object.assign(query, {
|
||||
region: '',
|
||||
@ -74,17 +136,7 @@ function reset() {
|
||||
sourceId: 0,
|
||||
onlyNewToday: false,
|
||||
});
|
||||
query.page = 1;
|
||||
pagination.current = 1;
|
||||
load();
|
||||
}
|
||||
|
||||
function onPageChange(p: { current: number; pageSize: number }) {
|
||||
query.page = p.current;
|
||||
query.size = p.pageSize;
|
||||
pagination.current = p.current;
|
||||
pagination.pageSize = p.pageSize;
|
||||
load();
|
||||
gridApi.reload();
|
||||
}
|
||||
|
||||
async function openDetail(row: RecruitmentItem) {
|
||||
@ -99,26 +151,13 @@ async function openDetail(row: RecruitmentItem) {
|
||||
}
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{ colKey: 'id', title: 'ID', width: 70 },
|
||||
{ colKey: 'title', title: '标题', minWidth: 260, ellipsis: true },
|
||||
{ colKey: 'categoryName', title: '分类', width: 90 },
|
||||
{ colKey: 'orgName', title: '发布单位', width: 180, ellipsis: true },
|
||||
{ colKey: 'region', title: '地区', width: 90 },
|
||||
{ colKey: 'publishDate', title: '发布日期', width: 110 },
|
||||
{ colKey: 'deadline', title: '截止', width: 110 },
|
||||
{ colKey: 'statusName', title: '状态', width: 90 },
|
||||
{ colKey: 'op', title: '操作', width: 90, fixed: 'right' },
|
||||
];
|
||||
|
||||
/** 复制公告原文链接 */
|
||||
function copyUrl() {
|
||||
if (detail.value?.url) {
|
||||
navigator.clipboard?.writeText(detail.value.url);
|
||||
MessagePlugin.success('链接已复制');
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(load);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -129,7 +168,7 @@ onMounted(load);
|
||||
clearable
|
||||
placeholder="标题/正文/单位关键字"
|
||||
style="width: 200px"
|
||||
@enter="load"
|
||||
@enter="search"
|
||||
/>
|
||||
<t-select
|
||||
v-model="query.region"
|
||||
@ -168,47 +207,15 @@ onMounted(load);
|
||||
clearable
|
||||
placeholder="发布主体模糊"
|
||||
style="width: 150px"
|
||||
@enter="load"
|
||||
@enter="search"
|
||||
/>
|
||||
<t-checkbox v-model="query.onlyNewToday">仅今日新增</t-checkbox>
|
||||
<t-button theme="primary" @click="load">查询</t-button>
|
||||
<t-button theme="primary" @click="search">查询</t-button>
|
||||
<t-button variant="outline" @click="reset">重置</t-button>
|
||||
</div>
|
||||
|
||||
<t-table
|
||||
row-key="id"
|
||||
:data="list"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
@page-change="onPageChange"
|
||||
>
|
||||
<template #statusName="{ row }">
|
||||
<t-tag
|
||||
:theme="
|
||||
row.status === 0
|
||||
? 'success'
|
||||
: row.status === 1
|
||||
? 'warning'
|
||||
: 'default'
|
||||
"
|
||||
variant="light"
|
||||
size="small"
|
||||
>
|
||||
{{ row.statusName }}
|
||||
</t-tag>
|
||||
</template>
|
||||
<template #op="{ row }">
|
||||
<t-button
|
||||
size="small"
|
||||
theme="primary"
|
||||
variant="text"
|
||||
@click="openDetail(row)"
|
||||
>
|
||||
详情
|
||||
</t-button>
|
||||
</template>
|
||||
</t-table>
|
||||
<!-- 招聘公告列表(Vben Vxe Table) -->
|
||||
<Grid />
|
||||
|
||||
<t-drawer
|
||||
v-model:visible="detailOpen"
|
||||
@ -220,10 +227,8 @@ onMounted(load);
|
||||
<div v-else-if="detail">
|
||||
<div class="mb-3 flex flex-wrap gap-2 text-xs">
|
||||
<t-tag theme="primary" variant="light">
|
||||
{{
|
||||
detail.categoryName
|
||||
}}
|
||||
</t-tag>
|
||||
{{ detail.categoryName }}
|
||||
</t-tag>
|
||||
<t-tag theme="success" variant="light">{{ detail.statusName }}</t-tag>
|
||||
<span class="opacity-60">{{ detail.region }}</span>
|
||||
<span class="opacity-60">发布:{{ detail.publishDate }}</span>
|
||||
|
||||
@ -1,14 +1,16 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
// 推送订阅管理:订阅列表 + 新增/编辑 + 删除 + Bark 推送测试。
|
||||
import type { SubscriptionItem } from '#/api/recruitment';
|
||||
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
import { h, onMounted, reactive, ref } from 'vue';
|
||||
|
||||
import { useAccess } from '@vben/access';
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { Page, VbenTableAction } from '@vben/common-ui';
|
||||
|
||||
import { DialogPlugin, MessagePlugin } from 'tdesign-vue-next';
|
||||
import { DialogPlugin, MessagePlugin, Tag } from 'tdesign-vue-next';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
CATEGORY_OPTIONS,
|
||||
deleteSubscription,
|
||||
@ -29,9 +31,6 @@ function categoryLabels(cats: number[] | undefined): string {
|
||||
|
||||
const { hasAccessByCodes } = useAccess();
|
||||
|
||||
const loading = ref(false);
|
||||
const list = ref<SubscriptionItem[]>([]);
|
||||
|
||||
// 编辑弹窗
|
||||
const dialogOpen = ref(false);
|
||||
const submitting = ref(false);
|
||||
@ -46,14 +45,10 @@ const form = reactive({
|
||||
enabled: 1,
|
||||
});
|
||||
|
||||
/** 拉取订阅列表并写入本地数据表格 */
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const data = await getSubscriptionList();
|
||||
list.value = data.list ?? [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
const data = await getSubscriptionList();
|
||||
gridApi.setGridOptions({ data: data.list ?? [] });
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
@ -109,6 +104,7 @@ async function submit() {
|
||||
}
|
||||
}
|
||||
|
||||
/** 删除订阅(保留原确认弹窗行为) */
|
||||
function onDelete(row: SubscriptionItem) {
|
||||
const d = DialogPlugin.confirm({
|
||||
header: '删除该订阅?',
|
||||
@ -121,6 +117,7 @@ function onDelete(row: SubscriptionItem) {
|
||||
});
|
||||
}
|
||||
|
||||
/** 发送 Bark 推送测试消息 */
|
||||
async function onTest(row: SubscriptionItem) {
|
||||
const data = await testPush({
|
||||
subscriptionId: row.id,
|
||||
@ -134,17 +131,111 @@ async function onTest(row: SubscriptionItem) {
|
||||
}
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{ colKey: 'id', title: 'ID', width: 70 },
|
||||
{ colKey: 'name', title: '名称', width: 140 },
|
||||
{ colKey: 'deviceKey', title: '设备 Key', width: 200, ellipsis: true },
|
||||
{ colKey: 'regionsText', title: '地区', width: 160, ellipsis: true },
|
||||
{ colKey: 'categoriesText', title: '分类', width: 160, ellipsis: true },
|
||||
{ colKey: 'onlyNew', title: '仅新公告', width: 90 },
|
||||
{ colKey: 'pushTime', title: '推送时间', width: 90 },
|
||||
{ colKey: 'enabled', title: '启用', width: 80 },
|
||||
{ colKey: 'op', title: '操作', width: 200, fixed: 'right' },
|
||||
];
|
||||
// 订阅表格(本地数据模式:关闭 proxyConfig,数据由 load() 写入)
|
||||
const gridOptions: VxeTableGridOptions = {
|
||||
columns: [
|
||||
{ type: 'seq', width: 50, title: '#' },
|
||||
{ field: 'id', title: 'ID', width: 70 },
|
||||
{ field: 'name', title: '名称', width: 140 },
|
||||
{
|
||||
field: 'deviceKey',
|
||||
title: '设备 Key',
|
||||
width: 200,
|
||||
showOverflow: 'tooltip',
|
||||
},
|
||||
{
|
||||
field: 'regionsText',
|
||||
title: '地区',
|
||||
width: 160,
|
||||
showOverflow: 'tooltip',
|
||||
slots: {
|
||||
default: ({ row }: { row: SubscriptionItem }) =>
|
||||
h('span', (row.regions ?? []).join('、') || '全部'),
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'categoriesText',
|
||||
title: '分类',
|
||||
width: 160,
|
||||
showOverflow: 'tooltip',
|
||||
slots: {
|
||||
default: ({ row }: { row: SubscriptionItem }) =>
|
||||
h('span', categoryLabels(row.categories)),
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'onlyNew',
|
||||
title: '仅新公告',
|
||||
width: 90,
|
||||
slots: {
|
||||
default: ({ row }: { row: SubscriptionItem }) =>
|
||||
h(
|
||||
Tag,
|
||||
{
|
||||
theme: row.onlyNew === 1 ? 'success' : 'default',
|
||||
variant: 'light',
|
||||
size: 'small',
|
||||
},
|
||||
row.onlyNew === 1 ? '是' : '否',
|
||||
),
|
||||
},
|
||||
},
|
||||
{ field: 'pushTime', title: '推送时间', width: 90 },
|
||||
{
|
||||
field: 'enabled',
|
||||
title: '启用',
|
||||
width: 80,
|
||||
slots: {
|
||||
default: ({ row }: { row: SubscriptionItem }) =>
|
||||
h(
|
||||
Tag,
|
||||
{
|
||||
theme: row.enabled === 1 ? 'success' : 'default',
|
||||
variant: 'light',
|
||||
size: 'small',
|
||||
},
|
||||
row.enabled === 1 ? '启用' : '停用',
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'op',
|
||||
title: '操作',
|
||||
width: 200,
|
||||
fixed: 'right',
|
||||
slots: {
|
||||
default: ({ row }: { row: SubscriptionItem }) =>
|
||||
h(VbenTableAction, {
|
||||
actions: [
|
||||
// 编辑:按权限码显隐
|
||||
{
|
||||
text: '编辑',
|
||||
onClick: () => openEdit(row),
|
||||
auth: ['recruitment:push:save'],
|
||||
},
|
||||
// 测试推送
|
||||
{
|
||||
text: '测试',
|
||||
onClick: () => onTest(row),
|
||||
auth: ['recruitment:push:test'],
|
||||
},
|
||||
// 删除(危险操作标红)
|
||||
{
|
||||
text: '删除',
|
||||
danger: true,
|
||||
onClick: () => onDelete(row),
|
||||
auth: ['recruitment:push:delete'],
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
},
|
||||
],
|
||||
pagerConfig: { enabled: false }, // 订阅数量较少,不分页
|
||||
proxyConfig: { enabled: false }, // 本地数据模式
|
||||
};
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({ gridOptions });
|
||||
|
||||
onMounted(load);
|
||||
</script>
|
||||
@ -161,61 +252,8 @@ onMounted(load);
|
||||
</t-button>
|
||||
</div>
|
||||
|
||||
<t-table row-key="id" :data="list" :columns="columns" :loading="loading">
|
||||
<template #regionsText="{ row }">
|
||||
{{ (row.regions ?? []).join('、') || '全部' }}
|
||||
</template>
|
||||
<template #categoriesText="{ row }">
|
||||
{{ categoryLabels(row.categories) }}
|
||||
</template>
|
||||
<template #onlyNew="{ row }">
|
||||
<t-tag
|
||||
:theme="row.onlyNew === 1 ? 'success' : 'default'"
|
||||
variant="light"
|
||||
size="small"
|
||||
>
|
||||
{{ row.onlyNew === 1 ? '是' : '否' }}
|
||||
</t-tag>
|
||||
</template>
|
||||
<template #enabled="{ row }">
|
||||
<t-tag
|
||||
:theme="row.enabled === 1 ? 'success' : 'default'"
|
||||
variant="light"
|
||||
size="small"
|
||||
>
|
||||
{{ row.enabled === 1 ? '启用' : '停用' }}
|
||||
</t-tag>
|
||||
</template>
|
||||
<template #op="{ row }">
|
||||
<t-button
|
||||
v-if="hasAccessByCodes(['recruitment:push:save'])"
|
||||
size="small"
|
||||
theme="primary"
|
||||
variant="text"
|
||||
@click="openEdit(row)"
|
||||
>
|
||||
编辑
|
||||
</t-button>
|
||||
<t-button
|
||||
v-if="hasAccessByCodes(['recruitment:push:test'])"
|
||||
size="small"
|
||||
theme="default"
|
||||
variant="text"
|
||||
@click="onTest(row)"
|
||||
>
|
||||
测试
|
||||
</t-button>
|
||||
<t-button
|
||||
v-if="hasAccessByCodes(['recruitment:push:delete'])"
|
||||
size="small"
|
||||
theme="danger"
|
||||
variant="text"
|
||||
@click="onDelete(row)"
|
||||
>
|
||||
删除
|
||||
</t-button>
|
||||
</template>
|
||||
</t-table>
|
||||
<!-- 订阅列表(Vben Vxe Table + TableAction) -->
|
||||
<Grid />
|
||||
|
||||
<t-dialog
|
||||
v-model:visible="dialogOpen"
|
||||
|
||||
@ -1,13 +1,15 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { AdminItem, RoleItem } from '#/api/system';
|
||||
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
import { h, onMounted, reactive, ref } from 'vue';
|
||||
|
||||
import { useAccess } from '@vben/access';
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { Page, VbenTableAction } from '@vben/common-ui';
|
||||
|
||||
import { DialogPlugin, MessagePlugin } from 'tdesign-vue-next';
|
||||
import { DialogPlugin, MessagePlugin, Tag } from 'tdesign-vue-next';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
createAdmin,
|
||||
deleteAdmin,
|
||||
@ -17,16 +19,15 @@ import {
|
||||
updateAdmin,
|
||||
} from '#/api/system';
|
||||
|
||||
// 人员管理页面:Vben Vxe Table + Vben TableAction 行操作(权限码控制显隐)
|
||||
const { hasAccessByCodes } = useAccess();
|
||||
|
||||
const loading = ref(false);
|
||||
const list = ref<AdminItem[]>([]);
|
||||
const roleOptions = ref<{ label: string; value: number }[]>([]);
|
||||
const dialogOpen = ref(false);
|
||||
const editing = ref<AdminItem | null>(null);
|
||||
|
||||
const query = reactive({ page: 1, size: 10, keyword: '' });
|
||||
const pagination = reactive({ current: 1, pageSize: 10, total: 0 });
|
||||
// 筛选条件(分页由 grid 自动管理)
|
||||
const query = reactive({ keyword: '' });
|
||||
const form = reactive({
|
||||
username: '',
|
||||
password: '',
|
||||
@ -35,17 +36,6 @@ const form = reactive({
|
||||
roleIds: [] as number[],
|
||||
});
|
||||
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const data = await getAdminList(query);
|
||||
list.value = data.list ?? [];
|
||||
pagination.total = data.total ?? 0;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadRoles() {
|
||||
const data = await getRoleList({ page: 1, size: 100 });
|
||||
roleOptions.value = (data.list ?? []).map((r: RoleItem) => ({
|
||||
@ -79,37 +69,38 @@ function openEdit(row: AdminItem) {
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
if (editing.value) {
|
||||
await updateAdmin(editing.value.id, {
|
||||
nickname: form.nickname,
|
||||
status: form.status,
|
||||
roleIds: form.roleIds,
|
||||
});
|
||||
} else {
|
||||
await createAdmin({
|
||||
username: form.username,
|
||||
password: form.password,
|
||||
nickname: form.nickname,
|
||||
roleIds: form.roleIds,
|
||||
});
|
||||
}
|
||||
// 编辑走更新、新增走创建
|
||||
editing.value
|
||||
? await updateAdmin(editing.value.id, {
|
||||
nickname: form.nickname,
|
||||
status: form.status,
|
||||
roleIds: form.roleIds,
|
||||
})
|
||||
: await createAdmin({
|
||||
username: form.username,
|
||||
password: form.password,
|
||||
nickname: form.nickname,
|
||||
roleIds: form.roleIds,
|
||||
});
|
||||
MessagePlugin.success('保存成功');
|
||||
dialogOpen.value = false;
|
||||
load();
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 删除人员(保留原确认弹窗行为) */
|
||||
function onDelete(row: AdminItem) {
|
||||
const d = DialogPlugin.confirm({
|
||||
header: '确认删除该人员?',
|
||||
onConfirm: async () => {
|
||||
await deleteAdmin(row.id);
|
||||
MessagePlugin.success('删除成功');
|
||||
load();
|
||||
gridApi.query();
|
||||
d.destroy();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** 重置密码(需要输入新密码,保留原弹窗交互) */
|
||||
function onResetPwd(row: AdminItem) {
|
||||
let password = '';
|
||||
const d = DialogPlugin.confirm({
|
||||
@ -127,28 +118,98 @@ function onResetPwd(row: AdminItem) {
|
||||
});
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{ colKey: 'id', title: 'ID', width: 70 },
|
||||
{ colKey: 'username', title: '用户名' },
|
||||
{ colKey: 'nickname', title: '昵称' },
|
||||
{ colKey: 'statusText', title: '状态', width: 90 },
|
||||
{ colKey: 'roleNamesText', title: '角色' },
|
||||
{ colKey: 'createdAt', title: '创建时间', width: 170 },
|
||||
{ colKey: 'op', title: '操作', width: 200 },
|
||||
];
|
||||
const gridOptions: VxeTableGridOptions = {
|
||||
columns: [
|
||||
{ type: 'seq', width: 50, title: '#' },
|
||||
{ field: 'id', title: 'ID', width: 70 },
|
||||
{ field: 'username', title: '用户名' },
|
||||
{ field: 'nickname', title: '昵称' },
|
||||
{
|
||||
field: 'statusText',
|
||||
title: '状态',
|
||||
width: 90,
|
||||
slots: {
|
||||
default: ({ row }: { row: AdminItem }) =>
|
||||
h(
|
||||
Tag,
|
||||
{
|
||||
theme: row.status === 1 ? 'success' : 'danger',
|
||||
variant: 'light',
|
||||
size: 'small',
|
||||
},
|
||||
row.status === 1 ? '启用' : '禁用',
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'roleNamesText',
|
||||
title: '角色',
|
||||
slots: {
|
||||
default: ({ row }: { row: AdminItem }) =>
|
||||
h('span', (row.roleNames ?? []).join(', ') || '-'),
|
||||
},
|
||||
},
|
||||
{ field: 'createdAt', title: '创建时间', width: 170 },
|
||||
{
|
||||
field: 'op',
|
||||
title: '操作',
|
||||
width: 200,
|
||||
fixed: 'right',
|
||||
slots: {
|
||||
default: ({ row }: { row: AdminItem }) =>
|
||||
h(VbenTableAction, {
|
||||
actions: [
|
||||
// 编辑:按权限码显隐
|
||||
{
|
||||
text: '编辑',
|
||||
onClick: () => openEdit(row),
|
||||
auth: ['system:personnel:update'],
|
||||
},
|
||||
// 重置密码
|
||||
{
|
||||
text: '重置密码',
|
||||
onClick: () => onResetPwd(row),
|
||||
auth: ['system:personnel:resetPwd'],
|
||||
},
|
||||
// 删除(危险操作标红)
|
||||
{
|
||||
text: '删除',
|
||||
danger: true,
|
||||
onClick: () => onDelete(row),
|
||||
auth: ['system:personnel:delete'],
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
},
|
||||
],
|
||||
pagerConfig: { pageSize: 10 },
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({
|
||||
page,
|
||||
}: {
|
||||
page: { currentPage: number; pageSize: number };
|
||||
}) => {
|
||||
const data = await getAdminList({
|
||||
page: page.currentPage,
|
||||
size: page.pageSize,
|
||||
keyword: query.keyword || undefined,
|
||||
});
|
||||
return { items: data.list ?? [], total: data.total ?? 0 };
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
function onPageChange(p: { current: number; pageSize: number }) {
|
||||
query.page = p.current;
|
||||
query.size = p.pageSize;
|
||||
pagination.current = p.current;
|
||||
pagination.pageSize = p.pageSize;
|
||||
load();
|
||||
const [Grid, gridApi] = useVbenVxeGrid({ gridOptions });
|
||||
|
||||
/** 触发查询(保留分页条件,因 grid 自动管理分页) */
|
||||
function search() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
load();
|
||||
loadRoles();
|
||||
});
|
||||
onMounted(loadRoles);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -166,9 +227,9 @@ onMounted(() => {
|
||||
clearable
|
||||
placeholder="用户名/昵称"
|
||||
style="width: 220px"
|
||||
@enter="load"
|
||||
@enter="search"
|
||||
/>
|
||||
<t-button theme="primary" @click="load">查询</t-button>
|
||||
<t-button theme="primary" @click="search">查询</t-button>
|
||||
</div>
|
||||
<t-button
|
||||
v-if="hasAccessByCodes(['system:personnel:create'])"
|
||||
@ -180,55 +241,9 @@ onMounted(() => {
|
||||
</div>
|
||||
</t-card>
|
||||
|
||||
<!-- 人员列表(Vben Vxe Table + TableAction) -->
|
||||
<t-card :bordered="false" class="wb-table-card">
|
||||
<t-table
|
||||
row-key="id"
|
||||
:data="list"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
@page-change="onPageChange"
|
||||
>
|
||||
<template #statusText="{ row }">
|
||||
{{
|
||||
row.status === 1 ? '启用' : '禁用'
|
||||
}}
|
||||
</template>
|
||||
<template #roleNamesText="{ row }">
|
||||
{{
|
||||
(row.roleNames ?? []).join(', ')
|
||||
}}
|
||||
</template>
|
||||
<template #op="{ row }">
|
||||
<t-button
|
||||
v-if="hasAccessByCodes(['system:personnel:update'])"
|
||||
size="small"
|
||||
theme="primary"
|
||||
variant="text"
|
||||
@click="openEdit(row)"
|
||||
>
|
||||
编辑
|
||||
</t-button>
|
||||
<t-button
|
||||
v-if="hasAccessByCodes(['system:personnel:resetPwd'])"
|
||||
size="small"
|
||||
theme="primary"
|
||||
variant="text"
|
||||
@click="onResetPwd(row)"
|
||||
>
|
||||
重置密码
|
||||
</t-button>
|
||||
<t-button
|
||||
v-if="hasAccessByCodes(['system:personnel:delete'])"
|
||||
size="small"
|
||||
theme="danger"
|
||||
variant="text"
|
||||
@click="onDelete(row)"
|
||||
>
|
||||
删除
|
||||
</t-button>
|
||||
</template>
|
||||
</t-table>
|
||||
<Grid />
|
||||
</t-card>
|
||||
|
||||
<t-dialog
|
||||
|
||||
@ -1,69 +1,102 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { LoginLogItem } from '#/api/system';
|
||||
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
import { h, reactive } from 'vue';
|
||||
|
||||
import { useAccess } from '@vben/access';
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { Tag } from 'tdesign-vue-next';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getLoginLogList } from '#/api/system';
|
||||
|
||||
// 登录日志页面:账号 + 结果筛选、分页列表
|
||||
// 登录日志页面:Vben Vxe Table 分页列表 + 账号/结果筛选(筛选区保留 TDesign 组件,行为与原版一致)
|
||||
const { hasAccessByCodes } = useAccess();
|
||||
|
||||
const loading = ref(false);
|
||||
const list = ref<LoginLogItem[]>([]);
|
||||
|
||||
// 查询条件:username 匹配账号,status 按结果过滤(-1 全部)
|
||||
const query = reactive({ page: 1, size: 10, username: '', status: -1 });
|
||||
const pagination = reactive({ current: 1, pageSize: 10, total: 0 });
|
||||
const query = reactive({ username: '', status: -1 });
|
||||
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const data = await getLoginLogList({
|
||||
page: query.page,
|
||||
size: query.size,
|
||||
username: query.username || undefined,
|
||||
status: query.status >= 0 ? query.status : undefined,
|
||||
});
|
||||
list.value = data.list ?? [];
|
||||
pagination.total = data.total ?? 0;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
// 表格列定义(status/failReason 用 slots 函数式渲染)
|
||||
const gridOptions: VxeTableGridOptions = {
|
||||
columns: [
|
||||
{ type: 'seq', width: 50, title: '#' },
|
||||
{ field: 'id', title: 'ID', width: 70 },
|
||||
{ field: 'username', title: '登录账号', width: 140 },
|
||||
{ field: 'ip', title: '来源 IP', width: 140 },
|
||||
{
|
||||
field: 'userAgent',
|
||||
title: '浏览器 UA',
|
||||
width: 240,
|
||||
showOverflow: 'tooltip',
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
title: '结果',
|
||||
width: 90,
|
||||
slots: {
|
||||
default: ({ row }: { row: LoginLogItem }) =>
|
||||
h(
|
||||
Tag,
|
||||
{
|
||||
theme: row.status === 1 ? 'success' : 'danger',
|
||||
variant: 'light',
|
||||
size: 'small',
|
||||
},
|
||||
row.status === 1 ? '成功' : '失败',
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'failReason',
|
||||
title: '失败原因',
|
||||
width: 220,
|
||||
showOverflow: 'tooltip',
|
||||
slots: {
|
||||
default: ({ row }: { row: LoginLogItem }) =>
|
||||
h(
|
||||
'span',
|
||||
{ class: row.failReason ? '' : 'text-gray-400' },
|
||||
row.failReason || '-',
|
||||
),
|
||||
},
|
||||
},
|
||||
{ field: 'createdAt', title: '登录时间', width: 170 },
|
||||
],
|
||||
pagerConfig: { pageSize: 10 },
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({
|
||||
page,
|
||||
}: {
|
||||
page: { currentPage: number; pageSize: number };
|
||||
}) => {
|
||||
const res = await getLoginLogList({
|
||||
page: page.currentPage,
|
||||
size: page.pageSize,
|
||||
username: query.username || undefined,
|
||||
status: query.status >= 0 ? query.status : undefined,
|
||||
});
|
||||
return { items: res.list ?? [], total: res.total ?? 0 };
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({ gridOptions });
|
||||
|
||||
/** 触发查询(保留分页条件,因 grid 自动管理分页) */
|
||||
function search() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 重置筛选并回到第一页 */
|
||||
/** 重置筛选并回到第一页重新查询 */
|
||||
function reset() {
|
||||
query.username = '';
|
||||
query.status = -1;
|
||||
query.page = 1;
|
||||
pagination.current = 1;
|
||||
load();
|
||||
gridApi.reload();
|
||||
}
|
||||
|
||||
/** 分页变化回调 */
|
||||
function onPageChange(p: { current: number; pageSize: number }) {
|
||||
query.page = p.current;
|
||||
query.size = p.pageSize;
|
||||
pagination.current = p.current;
|
||||
pagination.pageSize = p.pageSize;
|
||||
load();
|
||||
}
|
||||
|
||||
// 表格列定义
|
||||
const columns = [
|
||||
{ colKey: 'id', title: 'ID', width: 70 },
|
||||
{ colKey: 'username', title: '登录账号', width: 140 },
|
||||
{ colKey: 'ip', title: '来源 IP', width: 140 },
|
||||
{ colKey: 'userAgent', title: '浏览器 UA', width: 240 },
|
||||
{ colKey: 'status', title: '结果', width: 90 },
|
||||
{ colKey: 'failReason', title: '失败原因', width: 220 },
|
||||
{ colKey: 'createdAt', title: '登录时间', width: 170 },
|
||||
];
|
||||
|
||||
onMounted(load);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -82,7 +115,7 @@ onMounted(load);
|
||||
clearable
|
||||
placeholder="登录账号"
|
||||
style="width: 180px"
|
||||
@enter="load"
|
||||
@enter="search"
|
||||
/>
|
||||
<t-select
|
||||
v-model="query.status"
|
||||
@ -96,7 +129,7 @@ onMounted(load);
|
||||
<t-button
|
||||
v-if="hasAccessByCodes(['system:login-log:list'])"
|
||||
theme="primary"
|
||||
@click="load"
|
||||
@click="search"
|
||||
>
|
||||
查询
|
||||
</t-button>
|
||||
@ -105,31 +138,9 @@ onMounted(load);
|
||||
</div>
|
||||
</t-card>
|
||||
|
||||
<!-- 登录日志列表 -->
|
||||
<!-- 登录日志列表(Vben Vxe Table) -->
|
||||
<t-card :bordered="false" class="wb-table-card">
|
||||
<t-table
|
||||
row-key="id"
|
||||
:data="list"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
@page-change="onPageChange"
|
||||
>
|
||||
<template #status="{ row }">
|
||||
<t-tag
|
||||
:theme="row.status === 1 ? 'success' : 'danger'"
|
||||
variant="light"
|
||||
size="small"
|
||||
>
|
||||
{{ row.status === 1 ? '成功' : '失败' }}
|
||||
</t-tag>
|
||||
</template>
|
||||
<template #failReason="{ row }">
|
||||
<span :title="row.failReason">{{
|
||||
(row.failReason || '-').slice(0, 30)
|
||||
}}</span>
|
||||
</template>
|
||||
</t-table>
|
||||
<Grid />
|
||||
</t-card>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
@ -1,13 +1,15 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { MenuNode, RoleItem } from '#/api/system';
|
||||
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
import { h, onMounted, reactive, ref } from 'vue';
|
||||
|
||||
import { useAccess } from '@vben/access';
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { Page, VbenTableAction } from '@vben/common-ui';
|
||||
|
||||
import { DialogPlugin, MessagePlugin } from 'tdesign-vue-next';
|
||||
import { DialogPlugin, MessagePlugin, Tag } from 'tdesign-vue-next';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
createRole,
|
||||
deleteRole,
|
||||
@ -16,16 +18,15 @@ import {
|
||||
updateRole,
|
||||
} from '#/api/system';
|
||||
|
||||
// 角色管理页面:Vben Vxe Table + Vben TableAction 行操作(权限码控制显隐)
|
||||
const { hasAccessByCodes } = useAccess();
|
||||
|
||||
const loading = ref(false);
|
||||
const list = ref<RoleItem[]>([]);
|
||||
const menuTree = ref<any[]>([]);
|
||||
const dialogOpen = ref(false);
|
||||
const editing = ref<null | RoleItem>(null);
|
||||
|
||||
const query = reactive({ page: 1, size: 10, keyword: '' });
|
||||
const pagination = reactive({ current: 1, pageSize: 10, total: 0 });
|
||||
// 筛选条件(分页由 grid 自动管理)
|
||||
const query = reactive({ keyword: '' });
|
||||
const form = reactive({
|
||||
code: '',
|
||||
name: '',
|
||||
@ -33,6 +34,7 @@ const form = reactive({
|
||||
menuIds: [] as number[],
|
||||
});
|
||||
|
||||
/** 后端菜单树转 TDesign 树选择器数据 */
|
||||
function toTree(nodes: MenuNode[]): any[] {
|
||||
return (nodes ?? []).map((n) => ({
|
||||
label: n.name,
|
||||
@ -41,17 +43,6 @@ function toTree(nodes: MenuNode[]): any[] {
|
||||
}));
|
||||
}
|
||||
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const data = await getRoleList(query);
|
||||
list.value = data.list ?? [];
|
||||
pagination.total = data.total ?? 0;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMenus() {
|
||||
const data = await getMenuTree();
|
||||
menuTree.value = toTree(data.tree ?? []);
|
||||
@ -75,58 +66,115 @@ function openEdit(row: RoleItem) {
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
if (editing.value) {
|
||||
await updateRole(editing.value.id, {
|
||||
name: form.name,
|
||||
status: form.status,
|
||||
menuIds: form.menuIds,
|
||||
});
|
||||
} else {
|
||||
await createRole({
|
||||
code: form.code,
|
||||
name: form.name,
|
||||
status: form.status,
|
||||
menuIds: form.menuIds,
|
||||
});
|
||||
}
|
||||
// 编辑走更新、新增走创建
|
||||
editing.value
|
||||
? await updateRole(editing.value.id, {
|
||||
name: form.name,
|
||||
status: form.status,
|
||||
menuIds: form.menuIds,
|
||||
})
|
||||
: await createRole({
|
||||
code: form.code,
|
||||
name: form.name,
|
||||
status: form.status,
|
||||
menuIds: form.menuIds,
|
||||
});
|
||||
MessagePlugin.success('保存成功');
|
||||
dialogOpen.value = false;
|
||||
load();
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 删除角色(保留原确认弹窗行为) */
|
||||
function onDelete(row: RoleItem) {
|
||||
const d = DialogPlugin.confirm({
|
||||
header: `确认删除角色「${row.name}」?`,
|
||||
onConfirm: async () => {
|
||||
await deleteRole(row.id);
|
||||
MessagePlugin.success('删除成功');
|
||||
load();
|
||||
gridApi.query();
|
||||
d.destroy();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{ colKey: 'id', title: 'ID', width: 70 },
|
||||
{ colKey: 'code', title: '角色编码' },
|
||||
{ colKey: 'name', title: '角色名称' },
|
||||
{ colKey: 'statusText', title: '状态', width: 90 },
|
||||
{ colKey: 'createdAt', title: '创建时间', width: 170 },
|
||||
{ colKey: 'op', title: '操作', width: 150 },
|
||||
];
|
||||
const gridOptions: VxeTableGridOptions = {
|
||||
columns: [
|
||||
{ type: 'seq', width: 50, title: '#' },
|
||||
{ field: 'id', title: 'ID', width: 70 },
|
||||
{ field: 'code', title: '角色编码' },
|
||||
{ field: 'name', title: '角色名称' },
|
||||
{
|
||||
field: 'statusText',
|
||||
title: '状态',
|
||||
width: 90,
|
||||
slots: {
|
||||
default: ({ row }: { row: RoleItem }) =>
|
||||
h(
|
||||
Tag,
|
||||
{
|
||||
theme: row.status === 1 ? 'success' : 'danger',
|
||||
variant: 'light',
|
||||
size: 'small',
|
||||
},
|
||||
row.status === 1 ? '启用' : '禁用',
|
||||
),
|
||||
},
|
||||
},
|
||||
{ field: 'createdAt', title: '创建时间', width: 170 },
|
||||
{
|
||||
field: 'op',
|
||||
title: '操作',
|
||||
width: 150,
|
||||
fixed: 'right',
|
||||
slots: {
|
||||
default: ({ row }: { row: RoleItem }) =>
|
||||
h(VbenTableAction, {
|
||||
actions: [
|
||||
// 编辑:按权限码显隐
|
||||
{
|
||||
text: '编辑',
|
||||
onClick: () => openEdit(row),
|
||||
auth: ['system:role:update'],
|
||||
},
|
||||
// 删除(危险操作标红)
|
||||
{
|
||||
text: '删除',
|
||||
danger: true,
|
||||
onClick: () => onDelete(row),
|
||||
auth: ['system:role:delete'],
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
},
|
||||
],
|
||||
pagerConfig: { pageSize: 10 },
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({
|
||||
page,
|
||||
}: {
|
||||
page: { currentPage: number; pageSize: number };
|
||||
}) => {
|
||||
const data = await getRoleList({
|
||||
page: page.currentPage,
|
||||
size: page.pageSize,
|
||||
keyword: query.keyword || undefined,
|
||||
});
|
||||
return { items: data.list ?? [], total: data.total ?? 0 };
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
function onPageChange(p: { current: number; pageSize: number }) {
|
||||
query.page = p.current;
|
||||
query.size = p.pageSize;
|
||||
pagination.current = p.current;
|
||||
pagination.pageSize = p.pageSize;
|
||||
load();
|
||||
const [Grid, gridApi] = useVbenVxeGrid({ gridOptions });
|
||||
|
||||
/** 触发查询(保留分页条件,因 grid 自动管理分页) */
|
||||
function search() {
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
load();
|
||||
loadMenus();
|
||||
});
|
||||
onMounted(loadMenus);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -144,9 +192,9 @@ onMounted(() => {
|
||||
clearable
|
||||
placeholder="角色编码/名称"
|
||||
style="width: 220px"
|
||||
@enter="load"
|
||||
@enter="search"
|
||||
/>
|
||||
<t-button theme="primary" @click="load">查询</t-button>
|
||||
<t-button theme="primary" @click="search">查询</t-button>
|
||||
</div>
|
||||
<t-button
|
||||
v-if="hasAccessByCodes(['system:role:create'])"
|
||||
@ -158,41 +206,9 @@ onMounted(() => {
|
||||
</div>
|
||||
</t-card>
|
||||
|
||||
<!-- 角色列表(Vben Vxe Table + TableAction) -->
|
||||
<t-card :bordered="false" class="wb-table-card">
|
||||
<t-table
|
||||
row-key="id"
|
||||
:data="list"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
@page-change="onPageChange"
|
||||
>
|
||||
<template #statusText="{ row }">
|
||||
{{
|
||||
row.status === 1 ? '启用' : '禁用'
|
||||
}}
|
||||
</template>
|
||||
<template #op="{ row }">
|
||||
<t-button
|
||||
v-if="hasAccessByCodes(['system:role:update'])"
|
||||
size="small"
|
||||
theme="primary"
|
||||
variant="text"
|
||||
@click="openEdit(row)"
|
||||
>
|
||||
编辑
|
||||
</t-button>
|
||||
<t-button
|
||||
v-if="hasAccessByCodes(['system:role:delete'])"
|
||||
size="small"
|
||||
theme="danger"
|
||||
variant="text"
|
||||
@click="onDelete(row)"
|
||||
>
|
||||
删除
|
||||
</t-button>
|
||||
</template>
|
||||
</t-table>
|
||||
<Grid />
|
||||
</t-card>
|
||||
|
||||
<t-dialog
|
||||
|
||||
Loading…
Reference in New Issue
Block a user