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 变更记录
|
# admin.xpcool.com 变更记录
|
||||||
> 倒序:最新在上。格式:YYYY-MM-DD | 类型 | 摘要
|
> 倒序:最新在上。格式: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-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 | 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 仅首次渲染生效)——替代先前扁平化+缩进的自造方案
|
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>
|
<script lang="ts" setup>
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
import type { LoginLogItem, LogItem, MenuNode } from '#/api/system';
|
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 { useRouter } from 'vue-router';
|
||||||
|
|
||||||
import { Page } from '@vben/common-ui';
|
import { Page } from '@vben/common-ui';
|
||||||
import { useUserStore } from '@vben/stores';
|
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 {
|
import {
|
||||||
getAdminList,
|
getAdminList,
|
||||||
getLoginLogList,
|
getLoginLogList,
|
||||||
@ -103,20 +105,54 @@ const quickNavs = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const opColumns = [
|
// 最新操作日志(本地数据小表:关闭 proxyConfig,数据由 load() 写入)
|
||||||
{ colKey: 'adminId', title: '操作人ID', width: 90 },
|
const opGridOptions: VxeTableGridOptions = {
|
||||||
{ colKey: 'permission', title: '权限码', ellipsis: true },
|
columns: [
|
||||||
{ colKey: 'path', title: '接口', ellipsis: true },
|
{ type: 'seq', width: 50, title: '#' },
|
||||||
{ colKey: 'ip', title: 'IP', width: 130 },
|
{ field: 'adminId', title: '操作人ID', width: 90 },
|
||||||
{ colKey: 'createdAt', title: '时间', width: 170 },
|
{ 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 = [
|
const [OpGrid, opGridApi] = useVbenVxeGrid({ gridOptions: opGridOptions });
|
||||||
{ colKey: 'username', title: '账号', width: 120 },
|
|
||||||
{ colKey: 'ip', title: 'IP', width: 130 },
|
// 最新登录日志(本地数据小表)
|
||||||
{ colKey: 'statusText', title: '结果', width: 90 },
|
const loginGridOptions: VxeTableGridOptions = {
|
||||||
{ colKey: 'createdAt', title: '时间', width: 170 },
|
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() {
|
function formatToday() {
|
||||||
const d = new Date();
|
const d = new Date();
|
||||||
@ -155,9 +191,13 @@ async function load() {
|
|||||||
if (opRes.status === 'fulfilled') {
|
if (opRes.status === 'fulfilled') {
|
||||||
stats.opLogCount = opRes.value.total ?? 0;
|
stats.opLogCount = opRes.value.total ?? 0;
|
||||||
opLogs.value = opRes.value.list ?? [];
|
opLogs.value = opRes.value.list ?? [];
|
||||||
|
// 本地数据模式:直接写入表格
|
||||||
|
opGridApi.setGridOptions({ data: opLogs.value });
|
||||||
}
|
}
|
||||||
if (loginRes.status === 'fulfilled')
|
if (loginRes.status === 'fulfilled') {
|
||||||
loginLogs.value = loginRes.value.list ?? [];
|
loginLogs.value = loginRes.value.list ?? [];
|
||||||
|
loginGridApi.setGridOptions({ data: loginLogs.value });
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
MessagePlugin.warning('部分数据加载失败');
|
MessagePlugin.warning('部分数据加载失败');
|
||||||
} finally {
|
} finally {
|
||||||
@ -224,33 +264,10 @@ onMounted(load);
|
|||||||
<section class="wb-grid">
|
<section class="wb-grid">
|
||||||
<div class="wb-col">
|
<div class="wb-col">
|
||||||
<t-card title="最新操作日志" :bordered="false" class="wb-card">
|
<t-card title="最新操作日志" :bordered="false" class="wb-card">
|
||||||
<t-table
|
<OpGrid />
|
||||||
row-key="id"
|
|
||||||
:data="opLogs"
|
|
||||||
:columns="opColumns"
|
|
||||||
:loading="loading"
|
|
||||||
size="small"
|
|
||||||
:pagination="false"
|
|
||||||
/>
|
|
||||||
</t-card>
|
</t-card>
|
||||||
<t-card title="最新登录日志" :bordered="false" class="wb-card">
|
<t-card title="最新登录日志" :bordered="false" class="wb-card">
|
||||||
<t-table
|
<LoginGrid />
|
||||||
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>
|
|
||||||
</t-card>
|
</t-card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -315,11 +332,7 @@ onMounted(load);
|
|||||||
width: 360px;
|
width: 360px;
|
||||||
height: 360px;
|
height: 360px;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
background: radial-gradient(
|
background: radial-gradient(circle, rgb(255 255 255 / 35%), transparent 65%);
|
||||||
circle,
|
|
||||||
rgb(255 255 255 / 35%),
|
|
||||||
transparent 65%
|
|
||||||
);
|
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
filter: blur(8px);
|
filter: blur(8px);
|
||||||
animation: wb-float 9s ease-in-out infinite;
|
animation: wb-float 9s ease-in-out infinite;
|
||||||
|
|||||||
@ -1,13 +1,15 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
import type { CommunityItem } from '#/api/house';
|
import type { CommunityItem } from '#/api/house';
|
||||||
|
|
||||||
import { onMounted, reactive, ref } from 'vue';
|
import { h, reactive, ref } from 'vue';
|
||||||
|
|
||||||
import { useAccess } from '@vben/access';
|
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 } from 'tdesign-vue-next';
|
||||||
|
|
||||||
|
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
import {
|
import {
|
||||||
createCommunity,
|
createCommunity,
|
||||||
deleteCommunity,
|
deleteCommunity,
|
||||||
@ -18,18 +20,24 @@ import {
|
|||||||
// 小区管理:列表 + 新增/编辑弹窗 + 删除(软删除)
|
// 小区管理:列表 + 新增/编辑弹窗 + 删除(软删除)
|
||||||
const { hasAccessByCodes } = useAccess();
|
const { hasAccessByCodes } = useAccess();
|
||||||
|
|
||||||
const loading = ref(false);
|
|
||||||
const list = ref<CommunityItem[]>([]);
|
|
||||||
const dialogOpen = ref(false);
|
const dialogOpen = ref(false);
|
||||||
const editing = ref<null | CommunityItem>(null);
|
const editing = ref<CommunityItem | null>(null);
|
||||||
|
|
||||||
const regionOptions = [
|
const regionOptions = [
|
||||||
'云岩区', '南明区', '观山湖区', '花溪区', '乌当区', '白云区',
|
'云岩区',
|
||||||
'清镇市', '修文县', '开阳县', '息烽县',
|
'南明区',
|
||||||
|
'观山湖区',
|
||||||
|
'花溪区',
|
||||||
|
'乌当区',
|
||||||
|
'白云区',
|
||||||
|
'清镇市',
|
||||||
|
'修文县',
|
||||||
|
'开阳县',
|
||||||
|
'息烽县',
|
||||||
].map((r) => ({ label: r, value: r }));
|
].map((r) => ({ label: r, value: r }));
|
||||||
|
|
||||||
const query = reactive({ page: 1, size: 10, keyword: '', region: '' });
|
// 筛选条件(分页由 grid 自动管理)
|
||||||
const pagination = reactive({ current: 1, pageSize: 10, total: 0 });
|
const query = reactive({ keyword: '', region: '' });
|
||||||
|
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
name: '',
|
name: '',
|
||||||
@ -47,23 +55,22 @@ const form = reactive({
|
|||||||
developer: '',
|
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() {
|
function openCreate() {
|
||||||
editing.value = null;
|
editing.value = null;
|
||||||
Object.assign(form, {
|
Object.assign(form, {
|
||||||
name: '', region: '', businessDistrict: '', address: '', lng: 0, lat: 0,
|
name: '',
|
||||||
buildYear: 0, households: 0, plotRatio: 0, greenRate: 0,
|
region: '',
|
||||||
propertyCompany: '', propertyFee: 0, developer: '',
|
businessDistrict: '',
|
||||||
|
address: '',
|
||||||
|
lng: 0,
|
||||||
|
lat: 0,
|
||||||
|
buildYear: 0,
|
||||||
|
households: 0,
|
||||||
|
plotRatio: 0,
|
||||||
|
greenRate: 0,
|
||||||
|
propertyCompany: '',
|
||||||
|
propertyFee: 0,
|
||||||
|
developer: '',
|
||||||
});
|
});
|
||||||
dialogOpen.value = true;
|
dialogOpen.value = true;
|
||||||
}
|
}
|
||||||
@ -75,48 +82,98 @@ function openEdit(row: CommunityItem) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function submit() {
|
async function submit() {
|
||||||
if (editing.value) {
|
// 编辑走更新、新增走创建
|
||||||
await updateCommunity(editing.value.id, form);
|
editing.value
|
||||||
} else {
|
? await updateCommunity(editing.value.id, form)
|
||||||
await createCommunity(form);
|
: await createCommunity(form);
|
||||||
}
|
|
||||||
MessagePlugin.success('保存成功');
|
MessagePlugin.success('保存成功');
|
||||||
dialogOpen.value = false;
|
dialogOpen.value = false;
|
||||||
load();
|
gridApi.query();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 删除小区(保留原确认弹窗行为) */
|
||||||
function onDelete(row: CommunityItem) {
|
function onDelete(row: CommunityItem) {
|
||||||
const d = DialogPlugin.confirm({
|
const d = DialogPlugin.confirm({
|
||||||
header: `确认删除「${row.name}」?`,
|
header: `确认删除「${row.name}」?`,
|
||||||
onConfirm: async () => {
|
onConfirm: async () => {
|
||||||
await deleteCommunity(row.id);
|
await deleteCommunity(row.id);
|
||||||
MessagePlugin.success('删除成功');
|
MessagePlugin.success('删除成功');
|
||||||
load();
|
gridApi.query();
|
||||||
d.destroy();
|
d.destroy();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const columns = [
|
const gridOptions: VxeTableGridOptions = {
|
||||||
{ colKey: 'id', title: 'ID', width: 70 },
|
columns: [
|
||||||
{ colKey: 'name', title: '小区名称', width: 180 },
|
{ type: 'seq', width: 50, title: '#' },
|
||||||
{ colKey: 'region', title: '区县', width: 100 },
|
{ field: 'id', title: 'ID', width: 70 },
|
||||||
{ colKey: 'businessDistrict', title: '板块', width: 120 },
|
{ field: 'name', title: '小区名称', width: 180 },
|
||||||
{ colKey: 'address', title: '地址' },
|
{ field: 'region', title: '区县', width: 100 },
|
||||||
{ colKey: 'buildYear', title: '建成年份', width: 90 },
|
{ field: 'businessDistrict', title: '板块', width: 120 },
|
||||||
{ colKey: 'propertyCompany', title: '物业', width: 140 },
|
{ field: 'address', title: '地址', showOverflow: 'tooltip' },
|
||||||
{ colKey: 'op', title: '操作', width: 160 },
|
{ 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 }) {
|
const [Grid, gridApi] = useVbenVxeGrid({ gridOptions });
|
||||||
query.page = p.current;
|
|
||||||
query.size = p.pageSize;
|
/** 触发查询(保留分页条件,因 grid 自动管理分页) */
|
||||||
pagination.current = p.current;
|
function search() {
|
||||||
pagination.pageSize = p.pageSize;
|
gridApi.query();
|
||||||
load();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(load);
|
/** 重置筛选并回到第一页重新查询 */
|
||||||
|
function reset() {
|
||||||
|
query.keyword = '';
|
||||||
|
query.region = '';
|
||||||
|
gridApi.reload();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -128,7 +185,7 @@ onMounted(load);
|
|||||||
clearable
|
clearable
|
||||||
placeholder="小区名称"
|
placeholder="小区名称"
|
||||||
style="width: 200px"
|
style="width: 200px"
|
||||||
@enter="load"
|
@enter="search"
|
||||||
/>
|
/>
|
||||||
<t-select
|
<t-select
|
||||||
v-model="query.region"
|
v-model="query.region"
|
||||||
@ -137,7 +194,8 @@ onMounted(load);
|
|||||||
placeholder="区县"
|
placeholder="区县"
|
||||||
style="width: 140px"
|
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>
|
</div>
|
||||||
<t-button
|
<t-button
|
||||||
v-if="hasAccessByCodes(['house:community:create'])"
|
v-if="hasAccessByCodes(['house:community:create'])"
|
||||||
@ -148,35 +206,8 @@ onMounted(load);
|
|||||||
</t-button>
|
</t-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<t-table
|
<!-- 小区列表(Vben Vxe Table + TableAction) -->
|
||||||
row-key="id"
|
<Grid />
|
||||||
: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>
|
|
||||||
|
|
||||||
<t-dialog
|
<t-dialog
|
||||||
v-model:visible="dialogOpen"
|
v-model:visible="dialogOpen"
|
||||||
@ -190,7 +221,11 @@ onMounted(load);
|
|||||||
<t-input v-model="form.name" placeholder="如:中天未来方舟" />
|
<t-input v-model="form.name" placeholder="如:中天未来方舟" />
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
<t-form-item label="区县">
|
<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>
|
||||||
<t-form-item label="板块">
|
<t-form-item label="板块">
|
||||||
<t-input v-model="form.businessDistrict" placeholder="如:会展城" />
|
<t-input v-model="form.businessDistrict" placeholder="如:会展城" />
|
||||||
@ -208,10 +243,18 @@ onMounted(load);
|
|||||||
<t-input v-model="form.address" />
|
<t-input v-model="form.address" />
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
<t-form-item label="经度">
|
<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>
|
||||||
<t-form-item label="纬度">
|
<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>
|
</t-form-item>
|
||||||
</div>
|
</div>
|
||||||
</t-form>
|
</t-form>
|
||||||
|
|||||||
@ -1,16 +1,14 @@
|
|||||||
<script lang="ts" setup>
|
<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 { Page } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
import { getTransactionList } from '#/api/house';
|
import { getTransactionList } from '#/api/house';
|
||||||
|
|
||||||
// 数据明细:成交记录列表(价格快照/配套/学区划片在后续阶段接入)
|
// 数据明细:成交记录列表(Vben Vxe Table,价格快照/配套/学区划片在后续阶段接入)
|
||||||
const loading = ref(false);
|
|
||||||
const list = ref<TransactionItem[]>([]);
|
|
||||||
|
|
||||||
const regionOptions = [
|
const regionOptions = [
|
||||||
'云岩区',
|
'云岩区',
|
||||||
'南明区',
|
'南明区',
|
||||||
@ -24,49 +22,55 @@ const regionOptions = [
|
|||||||
'息烽县',
|
'息烽县',
|
||||||
].map((r) => ({ label: r, value: r }));
|
].map((r) => ({ label: r, value: r }));
|
||||||
|
|
||||||
const query = reactive({ page: 1, size: 10, keyword: '', region: '' });
|
// 筛选条件(分页由 grid 自动管理)
|
||||||
const pagination = reactive({ current: 1, pageSize: 10, total: 0 });
|
const query = reactive({ keyword: '', region: '' });
|
||||||
|
|
||||||
async function load() {
|
const gridOptions: VxeTableGridOptions = {
|
||||||
loading.value = true;
|
columns: [
|
||||||
try {
|
{ type: 'seq', width: 50, title: '#' },
|
||||||
const data = await getTransactionList(query);
|
{ field: 'id', title: 'ID', width: 70 },
|
||||||
list.value = data.list ?? [];
|
{ field: 'communityName', title: '小区', width: 180 },
|
||||||
pagination.total = data.total ?? 0;
|
{ field: 'layout', title: '户型', width: 100 },
|
||||||
} finally {
|
{ field: 'area', title: '面积㎡', width: 90 },
|
||||||
loading.value = false;
|
{ 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() {
|
function reset() {
|
||||||
query.keyword = '';
|
query.keyword = '';
|
||||||
query.region = '';
|
query.region = '';
|
||||||
query.page = 1;
|
gridApi.reload();
|
||||||
pagination.current = 1;
|
|
||||||
load();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -77,7 +81,7 @@ onMounted(load);
|
|||||||
clearable
|
clearable
|
||||||
placeholder="小区名/户型"
|
placeholder="小区名/户型"
|
||||||
style="width: 200px"
|
style="width: 200px"
|
||||||
@enter="load"
|
@enter="search"
|
||||||
/>
|
/>
|
||||||
<t-select
|
<t-select
|
||||||
v-model="query.region"
|
v-model="query.region"
|
||||||
@ -86,17 +90,11 @@ onMounted(load);
|
|||||||
placeholder="区县"
|
placeholder="区县"
|
||||||
style="width: 140px"
|
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>
|
<t-button variant="outline" @click="reset">重置</t-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<t-table
|
<!-- 成交记录列表(Vben Vxe Table) -->
|
||||||
row-key="id"
|
<Grid />
|
||||||
:data="list"
|
|
||||||
:columns="columns"
|
|
||||||
:loading="loading"
|
|
||||||
:pagination="pagination"
|
|
||||||
@page-change="onPageChange"
|
|
||||||
/>
|
|
||||||
</Page>
|
</Page>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -1,13 +1,15 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
import type { ListingItem } from '#/api/house';
|
import type { ListingItem } from '#/api/house';
|
||||||
|
|
||||||
import { onMounted, reactive, ref } from 'vue';
|
import { h, reactive, ref } from 'vue';
|
||||||
|
|
||||||
import { useAccess } from '@vben/access';
|
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 {
|
import {
|
||||||
batchMarkListing,
|
batchMarkListing,
|
||||||
deleteListing,
|
deleteListing,
|
||||||
@ -18,20 +20,24 @@ import {
|
|||||||
// 房源管理:多维筛选 + 多选批量标记(笋盘/低可信/状态)+ 编辑/删除
|
// 房源管理:多维筛选 + 多选批量标记(笋盘/低可信/状态)+ 编辑/删除
|
||||||
const { hasAccessByCodes } = useAccess();
|
const { hasAccessByCodes } = useAccess();
|
||||||
|
|
||||||
const loading = ref(false);
|
|
||||||
const list = ref<ListingItem[]>([]);
|
|
||||||
const selectedKeys = ref<number[]>([]);
|
|
||||||
const dialogOpen = ref(false);
|
const dialogOpen = ref(false);
|
||||||
const editing = ref<null | ListingItem>(null);
|
const editing = ref<ListingItem | null>(null);
|
||||||
|
|
||||||
const regionOptions = [
|
const regionOptions = [
|
||||||
'云岩区', '南明区', '观山湖区', '花溪区', '乌当区', '白云区',
|
'云岩区',
|
||||||
'清镇市', '修文县', '开阳县', '息烽县',
|
'南明区',
|
||||||
|
'观山湖区',
|
||||||
|
'花溪区',
|
||||||
|
'乌当区',
|
||||||
|
'白云区',
|
||||||
|
'清镇市',
|
||||||
|
'修文县',
|
||||||
|
'开阳县',
|
||||||
|
'息烽县',
|
||||||
].map((r) => ({ label: r, value: r }));
|
].map((r) => ({ label: r, value: r }));
|
||||||
|
|
||||||
|
// 筛选条件(分页由 grid 自动管理)
|
||||||
const query = reactive({
|
const query = reactive({
|
||||||
page: 1,
|
|
||||||
size: 10,
|
|
||||||
keyword: '',
|
keyword: '',
|
||||||
region: '',
|
region: '',
|
||||||
layout: '',
|
layout: '',
|
||||||
@ -42,42 +48,23 @@ const query = reactive({
|
|||||||
isBargain: 0,
|
isBargain: 0,
|
||||||
confidence: 0,
|
confidence: 0,
|
||||||
});
|
});
|
||||||
const pagination = reactive({ current: 1, pageSize: 10, total: 0 });
|
|
||||||
|
|
||||||
async function load() {
|
/** 收集当前勾选的房源 ID */
|
||||||
loading.value = true;
|
function getSelectedIds(): number[] {
|
||||||
try {
|
const records = (gridApi.grid?.getCheckboxRecords() ?? []) as ListingItem[];
|
||||||
const data = await getListingList(query);
|
return records.map((r) => Number(r.id));
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 批量标记(笋盘/低可信/状态) */
|
/** 批量标记(笋盘/低可信/状态) */
|
||||||
async function onBatchMark(field: string, value: number) {
|
async function onBatchMark(field: string, value: number) {
|
||||||
if (selectedKeys.value.length === 0) {
|
const ids = getSelectedIds();
|
||||||
|
if (ids.length === 0) {
|
||||||
MessagePlugin.warning('请先勾选房源');
|
MessagePlugin.warning('请先勾选房源');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await batchMarkListing({ ids: selectedKeys.value, field, value });
|
await batchMarkListing({ ids, field, value });
|
||||||
MessagePlugin.success('标记成功');
|
MessagePlugin.success('标记成功');
|
||||||
load();
|
gridApi.query();
|
||||||
}
|
}
|
||||||
|
|
||||||
function openEdit(row: ListingItem) {
|
function openEdit(row: ListingItem) {
|
||||||
@ -98,45 +85,174 @@ async function submit() {
|
|||||||
}
|
}
|
||||||
MessagePlugin.success('保存成功');
|
MessagePlugin.success('保存成功');
|
||||||
dialogOpen.value = false;
|
dialogOpen.value = false;
|
||||||
load();
|
gridApi.query();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 删除房源(保留原确认弹窗行为) */
|
||||||
function onDelete(row: ListingItem) {
|
function onDelete(row: ListingItem) {
|
||||||
const d = DialogPlugin.confirm({
|
const d = DialogPlugin.confirm({
|
||||||
header: '确认删除该房源?',
|
header: '确认删除该房源?',
|
||||||
onConfirm: async () => {
|
onConfirm: async () => {
|
||||||
await deleteListing(row.id);
|
await deleteListing(row.id);
|
||||||
MessagePlugin.success('删除成功');
|
MessagePlugin.success('删除成功');
|
||||||
load();
|
gridApi.query();
|
||||||
d.destroy();
|
d.destroy();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const columns = [
|
/** 状态码 -> TDesign 标签主题色(1 在售 / 2 下架 / 3 成交) */
|
||||||
{ colKey: 'row-select', type: 'multiple', width: 46 },
|
function statusTheme(status: number) {
|
||||||
{ colKey: 'id', title: 'ID', width: 70 },
|
if (status === 1) return 'success';
|
||||||
{ colKey: 'communityName', title: '小区', width: 160 },
|
if (status === 2) return 'default';
|
||||||
{ colKey: 'layout', title: '户型', width: 100 },
|
return 'primary';
|
||||||
{ 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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -147,7 +263,7 @@ onMounted(load);
|
|||||||
clearable
|
clearable
|
||||||
placeholder="房号/户型"
|
placeholder="房号/户型"
|
||||||
style="width: 160px"
|
style="width: 160px"
|
||||||
@enter="load"
|
@enter="search"
|
||||||
/>
|
/>
|
||||||
<t-select
|
<t-select
|
||||||
v-model="query.region"
|
v-model="query.region"
|
||||||
@ -184,7 +300,7 @@ onMounted(load);
|
|||||||
]"
|
]"
|
||||||
style="width: 110px"
|
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>
|
<t-button variant="outline" @click="reset">重置</t-button>
|
||||||
<span class="ml-auto flex items-center gap-2">
|
<span class="ml-auto flex items-center gap-2">
|
||||||
<t-button
|
<t-button
|
||||||
@ -204,52 +320,8 @@ onMounted(load);
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<t-table
|
<!-- 房源列表(Vben Vxe Table + TableAction) -->
|
||||||
row-key="id"
|
<Grid />
|
||||||
: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>
|
|
||||||
|
|
||||||
<t-dialog
|
<t-dialog
|
||||||
v-model:visible="dialogOpen"
|
v-model:visible="dialogOpen"
|
||||||
|
|||||||
@ -1,16 +1,14 @@
|
|||||||
<script lang="ts" setup>
|
<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 { Page } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
import { getPresaleList } from '#/api/house';
|
import { getPresaleList } from '#/api/house';
|
||||||
|
|
||||||
// 新房预售:预售许可证列表(数据源:贵阳市住建局预售公示)
|
// 新房预售:预售许可证列表(Vben Vxe Table,数据源:贵阳市住建局预售公示)
|
||||||
const loading = ref(false);
|
|
||||||
const list = ref<PresaleItem[]>([]);
|
|
||||||
|
|
||||||
const regionOptions = [
|
const regionOptions = [
|
||||||
'云岩区',
|
'云岩区',
|
||||||
'南明区',
|
'南明区',
|
||||||
@ -30,57 +28,63 @@ const purposeOptions = [
|
|||||||
{ label: '商业', value: '商业' },
|
{ label: '商业', value: '商业' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const query = reactive({
|
// 筛选条件(分页由 grid 自动管理)
|
||||||
page: 1,
|
const query = reactive({ keyword: '', region: '', purpose: '' });
|
||||||
size: 10,
|
|
||||||
keyword: '',
|
|
||||||
region: '',
|
|
||||||
purpose: '',
|
|
||||||
});
|
|
||||||
const pagination = reactive({ current: 1, pageSize: 10, total: 0 });
|
|
||||||
|
|
||||||
async function load() {
|
const gridOptions: VxeTableGridOptions = {
|
||||||
loading.value = true;
|
columns: [
|
||||||
try {
|
{ type: 'seq', width: 50, title: '#' },
|
||||||
const data = await getPresaleList(query);
|
{ field: 'presaleNo', title: '预售证号', width: 190 },
|
||||||
list.value = data.list ?? [];
|
{ field: 'communityName', title: '楼盘/项目名', width: 200 },
|
||||||
pagination.total = data.total ?? 0;
|
{
|
||||||
} finally {
|
field: 'developer',
|
||||||
loading.value = false;
|
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() {
|
function reset() {
|
||||||
query.keyword = '';
|
query.keyword = '';
|
||||||
query.region = '';
|
query.region = '';
|
||||||
query.purpose = '';
|
query.purpose = '';
|
||||||
query.page = 1;
|
gridApi.reload();
|
||||||
pagination.current = 1;
|
|
||||||
load();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -91,7 +95,7 @@ onMounted(load);
|
|||||||
clearable
|
clearable
|
||||||
placeholder="楼盘名/开发商/预售证号"
|
placeholder="楼盘名/开发商/预售证号"
|
||||||
style="width: 240px"
|
style="width: 240px"
|
||||||
@enter="load"
|
@enter="search"
|
||||||
/>
|
/>
|
||||||
<t-select
|
<t-select
|
||||||
v-model="query.region"
|
v-model="query.region"
|
||||||
@ -107,17 +111,11 @@ onMounted(load);
|
|||||||
placeholder="用途"
|
placeholder="用途"
|
||||||
style="width: 120px"
|
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>
|
<t-button variant="outline" @click="reset">重置</t-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<t-table
|
<!-- 预售许可列表(Vben Vxe Table) -->
|
||||||
row-key="id"
|
<Grid />
|
||||||
:data="list"
|
|
||||||
:columns="columns"
|
|
||||||
:loading="loading"
|
|
||||||
:pagination="pagination"
|
|
||||||
@page-change="onPageChange"
|
|
||||||
/>
|
|
||||||
</Page>
|
</Page>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -1,31 +1,27 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
import type { SourceItem } from '#/api/recruitment';
|
import type { SourceItem } from '#/api/recruitment';
|
||||||
|
|
||||||
// 数据源与抓取运维:数据源列表 + 手动触发抓取 + 运行摘要。
|
// 数据源与抓取运维:数据源列表 + 手动触发抓取 + 运行摘要。
|
||||||
import { onMounted, ref } from 'vue';
|
import { h, onMounted, ref } from 'vue';
|
||||||
|
|
||||||
import { useAccess } from '@vben/access';
|
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';
|
import { getSourceList, triggerCrawl } from '#/api/recruitment';
|
||||||
|
|
||||||
const { hasAccessByCodes } = useAccess();
|
const { hasAccessByCodes } = useAccess();
|
||||||
|
|
||||||
const loading = ref(false);
|
|
||||||
const list = ref<SourceItem[]>([]);
|
|
||||||
const crawling = ref(false);
|
const crawling = ref(false);
|
||||||
const runningSummary = ref('');
|
const runningSummary = ref('');
|
||||||
|
|
||||||
|
/** 拉取数据源列表并写入本地数据表格 */
|
||||||
async function load() {
|
async function load() {
|
||||||
loading.value = true;
|
const data = await getSourceList();
|
||||||
try {
|
gridApi.setGridOptions({ data: data.list ?? [] });
|
||||||
const data = await getSourceList();
|
|
||||||
list.value = data.list ?? [];
|
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onTriggerAll() {
|
function onTriggerAll() {
|
||||||
@ -55,16 +51,79 @@ async function doTrigger(sourceId: number, force: boolean) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const columns = [
|
// 数据源表格(本地数据模式:关闭 proxyConfig,数据由 load() 写入)
|
||||||
{ colKey: 'id', title: 'ID', width: 70 },
|
const gridOptions: VxeTableGridOptions = {
|
||||||
{ colKey: 'name', title: '数据源', minWidth: 200 },
|
columns: [
|
||||||
{ colKey: 'region', title: '地区', width: 100 },
|
{ type: 'seq', width: 50, title: '#' },
|
||||||
{ colKey: 'baseUrl', title: '地址', minWidth: 220, ellipsis: true },
|
{ field: 'id', title: 'ID', width: 70 },
|
||||||
{ colKey: 'enabled', title: '启用', width: 80 },
|
{ field: 'name', title: '数据源', minWidth: 200 },
|
||||||
{ colKey: 'lastSuccessAt', title: '上次成功', width: 160 },
|
{ field: 'region', title: '地区', width: 100 },
|
||||||
{ colKey: 'failCount', title: '连续失败', width: 90 },
|
{ field: 'baseUrl', title: '地址', minWidth: 220, showOverflow: 'tooltip' },
|
||||||
{ colKey: 'op', title: '操作', width: 100, fixed: 'right' },
|
{
|
||||||
];
|
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);
|
onMounted(load);
|
||||||
</script>
|
</script>
|
||||||
@ -83,38 +142,8 @@ onMounted(load);
|
|||||||
<span class="text-xs opacity-60">定时任务:每日 03:00 自动抓取</span>
|
<span class="text-xs opacity-60">定时任务:每日 03:00 自动抓取</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<t-table row-key="id" :data="list" :columns="columns" :loading="loading">
|
<!-- 数据源列表(Vben Vxe Table) -->
|
||||||
<template #enabled="{ row }">
|
<Grid />
|
||||||
<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>
|
|
||||||
|
|
||||||
<div v-if="runningSummary" class="mt-4 rounded-lg border p-3">
|
<div v-if="runningSummary" class="mt-4 rounded-lg border p-3">
|
||||||
<p class="mb-1 text-sm font-medium">本次运行摘要</p>
|
<p class="mb-1 text-sm font-medium">本次运行摘要</p>
|
||||||
|
|||||||
@ -1,13 +1,15 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
// 招聘公告列表:多维筛选 + 表格 + 详情抽屉。
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
// 招聘公告列表:多维筛选 + Vben Vxe Table + Vben TableAction 行操作 + 详情抽屉。
|
||||||
import type { RecruitmentItem } from '#/api/recruitment';
|
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 {
|
import {
|
||||||
CATEGORY_OPTIONS,
|
CATEGORY_OPTIONS,
|
||||||
getRecruitmentDetail,
|
getRecruitmentDetail,
|
||||||
@ -16,18 +18,13 @@ import {
|
|||||||
STATUS_OPTIONS,
|
STATUS_OPTIONS,
|
||||||
} from '#/api/recruitment';
|
} 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 detailOpen = ref(false);
|
||||||
const detailLoading = ref(false);
|
const detailLoading = ref(false);
|
||||||
const detail = ref<null | RecruitmentItem>(null);
|
const detail = ref<null | RecruitmentItem>(null);
|
||||||
|
|
||||||
|
// 筛选条件(分页由 grid 自动管理)
|
||||||
const query = reactive({
|
const query = reactive({
|
||||||
page: 1,
|
|
||||||
size: 10,
|
|
||||||
region: '',
|
region: '',
|
||||||
category: 0,
|
category: 0,
|
||||||
keyword: '',
|
keyword: '',
|
||||||
@ -39,29 +36,94 @@ const query = reactive({
|
|||||||
onlyNewToday: false,
|
onlyNewToday: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
async function load() {
|
// 表格列定义(statusName/op 用 slots 函数式渲染)
|
||||||
loading.value = true;
|
/** 状态码 -> TDesign 标签主题色(0 招集中 / 1 报名中 / 其余默认) */
|
||||||
try {
|
function statusTheme(status: number) {
|
||||||
const data = await getRecruitmentList({
|
if (status === 0) return 'success';
|
||||||
page: query.page,
|
if (status === 1) return 'warning';
|
||||||
size: query.size,
|
return 'default';
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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() {
|
function reset() {
|
||||||
Object.assign(query, {
|
Object.assign(query, {
|
||||||
region: '',
|
region: '',
|
||||||
@ -74,17 +136,7 @@ function reset() {
|
|||||||
sourceId: 0,
|
sourceId: 0,
|
||||||
onlyNewToday: false,
|
onlyNewToday: false,
|
||||||
});
|
});
|
||||||
query.page = 1;
|
gridApi.reload();
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openDetail(row: RecruitmentItem) {
|
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() {
|
function copyUrl() {
|
||||||
if (detail.value?.url) {
|
if (detail.value?.url) {
|
||||||
navigator.clipboard?.writeText(detail.value.url);
|
navigator.clipboard?.writeText(detail.value.url);
|
||||||
MessagePlugin.success('链接已复制');
|
MessagePlugin.success('链接已复制');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(load);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -129,7 +168,7 @@ onMounted(load);
|
|||||||
clearable
|
clearable
|
||||||
placeholder="标题/正文/单位关键字"
|
placeholder="标题/正文/单位关键字"
|
||||||
style="width: 200px"
|
style="width: 200px"
|
||||||
@enter="load"
|
@enter="search"
|
||||||
/>
|
/>
|
||||||
<t-select
|
<t-select
|
||||||
v-model="query.region"
|
v-model="query.region"
|
||||||
@ -168,47 +207,15 @@ onMounted(load);
|
|||||||
clearable
|
clearable
|
||||||
placeholder="发布主体模糊"
|
placeholder="发布主体模糊"
|
||||||
style="width: 150px"
|
style="width: 150px"
|
||||||
@enter="load"
|
@enter="search"
|
||||||
/>
|
/>
|
||||||
<t-checkbox v-model="query.onlyNewToday">仅今日新增</t-checkbox>
|
<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>
|
<t-button variant="outline" @click="reset">重置</t-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<t-table
|
<!-- 招聘公告列表(Vben Vxe Table) -->
|
||||||
row-key="id"
|
<Grid />
|
||||||
: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>
|
|
||||||
|
|
||||||
<t-drawer
|
<t-drawer
|
||||||
v-model:visible="detailOpen"
|
v-model:visible="detailOpen"
|
||||||
@ -220,10 +227,8 @@ onMounted(load);
|
|||||||
<div v-else-if="detail">
|
<div v-else-if="detail">
|
||||||
<div class="mb-3 flex flex-wrap gap-2 text-xs">
|
<div class="mb-3 flex flex-wrap gap-2 text-xs">
|
||||||
<t-tag theme="primary" variant="light">
|
<t-tag theme="primary" variant="light">
|
||||||
{{
|
{{ detail.categoryName }}
|
||||||
detail.categoryName
|
</t-tag>
|
||||||
}}
|
|
||||||
</t-tag>
|
|
||||||
<t-tag theme="success" variant="light">{{ detail.statusName }}</t-tag>
|
<t-tag theme="success" variant="light">{{ detail.statusName }}</t-tag>
|
||||||
<span class="opacity-60">{{ detail.region }}</span>
|
<span class="opacity-60">{{ detail.region }}</span>
|
||||||
<span class="opacity-60">发布:{{ detail.publishDate }}</span>
|
<span class="opacity-60">发布:{{ detail.publishDate }}</span>
|
||||||
|
|||||||
@ -1,14 +1,16 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
// 推送订阅管理:订阅列表 + 新增/编辑 + 删除 + Bark 推送测试。
|
// 推送订阅管理:订阅列表 + 新增/编辑 + 删除 + Bark 推送测试。
|
||||||
import type { SubscriptionItem } from '#/api/recruitment';
|
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 { 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 {
|
import {
|
||||||
CATEGORY_OPTIONS,
|
CATEGORY_OPTIONS,
|
||||||
deleteSubscription,
|
deleteSubscription,
|
||||||
@ -29,9 +31,6 @@ function categoryLabels(cats: number[] | undefined): string {
|
|||||||
|
|
||||||
const { hasAccessByCodes } = useAccess();
|
const { hasAccessByCodes } = useAccess();
|
||||||
|
|
||||||
const loading = ref(false);
|
|
||||||
const list = ref<SubscriptionItem[]>([]);
|
|
||||||
|
|
||||||
// 编辑弹窗
|
// 编辑弹窗
|
||||||
const dialogOpen = ref(false);
|
const dialogOpen = ref(false);
|
||||||
const submitting = ref(false);
|
const submitting = ref(false);
|
||||||
@ -46,14 +45,10 @@ const form = reactive({
|
|||||||
enabled: 1,
|
enabled: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** 拉取订阅列表并写入本地数据表格 */
|
||||||
async function load() {
|
async function load() {
|
||||||
loading.value = true;
|
const data = await getSubscriptionList();
|
||||||
try {
|
gridApi.setGridOptions({ data: data.list ?? [] });
|
||||||
const data = await getSubscriptionList();
|
|
||||||
list.value = data.list ?? [];
|
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function openCreate() {
|
function openCreate() {
|
||||||
@ -109,6 +104,7 @@ async function submit() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 删除订阅(保留原确认弹窗行为) */
|
||||||
function onDelete(row: SubscriptionItem) {
|
function onDelete(row: SubscriptionItem) {
|
||||||
const d = DialogPlugin.confirm({
|
const d = DialogPlugin.confirm({
|
||||||
header: '删除该订阅?',
|
header: '删除该订阅?',
|
||||||
@ -121,6 +117,7 @@ function onDelete(row: SubscriptionItem) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 发送 Bark 推送测试消息 */
|
||||||
async function onTest(row: SubscriptionItem) {
|
async function onTest(row: SubscriptionItem) {
|
||||||
const data = await testPush({
|
const data = await testPush({
|
||||||
subscriptionId: row.id,
|
subscriptionId: row.id,
|
||||||
@ -134,17 +131,111 @@ async function onTest(row: SubscriptionItem) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const columns = [
|
// 订阅表格(本地数据模式:关闭 proxyConfig,数据由 load() 写入)
|
||||||
{ colKey: 'id', title: 'ID', width: 70 },
|
const gridOptions: VxeTableGridOptions = {
|
||||||
{ colKey: 'name', title: '名称', width: 140 },
|
columns: [
|
||||||
{ colKey: 'deviceKey', title: '设备 Key', width: 200, ellipsis: true },
|
{ type: 'seq', width: 50, title: '#' },
|
||||||
{ colKey: 'regionsText', title: '地区', width: 160, ellipsis: true },
|
{ field: 'id', title: 'ID', width: 70 },
|
||||||
{ colKey: 'categoriesText', title: '分类', width: 160, ellipsis: true },
|
{ field: 'name', title: '名称', width: 140 },
|
||||||
{ colKey: 'onlyNew', title: '仅新公告', width: 90 },
|
{
|
||||||
{ colKey: 'pushTime', title: '推送时间', width: 90 },
|
field: 'deviceKey',
|
||||||
{ colKey: 'enabled', title: '启用', width: 80 },
|
title: '设备 Key',
|
||||||
{ colKey: 'op', title: '操作', width: 200, fixed: 'right' },
|
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);
|
onMounted(load);
|
||||||
</script>
|
</script>
|
||||||
@ -161,61 +252,8 @@ onMounted(load);
|
|||||||
</t-button>
|
</t-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<t-table row-key="id" :data="list" :columns="columns" :loading="loading">
|
<!-- 订阅列表(Vben Vxe Table + TableAction) -->
|
||||||
<template #regionsText="{ row }">
|
<Grid />
|
||||||
{{ (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>
|
|
||||||
|
|
||||||
<t-dialog
|
<t-dialog
|
||||||
v-model:visible="dialogOpen"
|
v-model:visible="dialogOpen"
|
||||||
|
|||||||
@ -1,13 +1,15 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
import type { AdminItem, RoleItem } from '#/api/system';
|
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 { 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 {
|
import {
|
||||||
createAdmin,
|
createAdmin,
|
||||||
deleteAdmin,
|
deleteAdmin,
|
||||||
@ -17,16 +19,15 @@ import {
|
|||||||
updateAdmin,
|
updateAdmin,
|
||||||
} from '#/api/system';
|
} from '#/api/system';
|
||||||
|
|
||||||
|
// 人员管理页面:Vben Vxe Table + Vben TableAction 行操作(权限码控制显隐)
|
||||||
const { hasAccessByCodes } = useAccess();
|
const { hasAccessByCodes } = useAccess();
|
||||||
|
|
||||||
const loading = ref(false);
|
|
||||||
const list = ref<AdminItem[]>([]);
|
|
||||||
const roleOptions = ref<{ label: string; value: number }[]>([]);
|
const roleOptions = ref<{ label: string; value: number }[]>([]);
|
||||||
const dialogOpen = ref(false);
|
const dialogOpen = ref(false);
|
||||||
const editing = ref<AdminItem | null>(null);
|
const editing = ref<AdminItem | null>(null);
|
||||||
|
|
||||||
const query = reactive({ page: 1, size: 10, keyword: '' });
|
// 筛选条件(分页由 grid 自动管理)
|
||||||
const pagination = reactive({ current: 1, pageSize: 10, total: 0 });
|
const query = reactive({ keyword: '' });
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
username: '',
|
username: '',
|
||||||
password: '',
|
password: '',
|
||||||
@ -35,17 +36,6 @@ const form = reactive({
|
|||||||
roleIds: [] as number[],
|
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() {
|
async function loadRoles() {
|
||||||
const data = await getRoleList({ page: 1, size: 100 });
|
const data = await getRoleList({ page: 1, size: 100 });
|
||||||
roleOptions.value = (data.list ?? []).map((r: RoleItem) => ({
|
roleOptions.value = (data.list ?? []).map((r: RoleItem) => ({
|
||||||
@ -79,37 +69,38 @@ function openEdit(row: AdminItem) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function submit() {
|
async function submit() {
|
||||||
if (editing.value) {
|
// 编辑走更新、新增走创建
|
||||||
await updateAdmin(editing.value.id, {
|
editing.value
|
||||||
nickname: form.nickname,
|
? await updateAdmin(editing.value.id, {
|
||||||
status: form.status,
|
nickname: form.nickname,
|
||||||
roleIds: form.roleIds,
|
status: form.status,
|
||||||
});
|
roleIds: form.roleIds,
|
||||||
} else {
|
})
|
||||||
await createAdmin({
|
: await createAdmin({
|
||||||
username: form.username,
|
username: form.username,
|
||||||
password: form.password,
|
password: form.password,
|
||||||
nickname: form.nickname,
|
nickname: form.nickname,
|
||||||
roleIds: form.roleIds,
|
roleIds: form.roleIds,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
MessagePlugin.success('保存成功');
|
MessagePlugin.success('保存成功');
|
||||||
dialogOpen.value = false;
|
dialogOpen.value = false;
|
||||||
load();
|
gridApi.query();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 删除人员(保留原确认弹窗行为) */
|
||||||
function onDelete(row: AdminItem) {
|
function onDelete(row: AdminItem) {
|
||||||
const d = DialogPlugin.confirm({
|
const d = DialogPlugin.confirm({
|
||||||
header: '确认删除该人员?',
|
header: '确认删除该人员?',
|
||||||
onConfirm: async () => {
|
onConfirm: async () => {
|
||||||
await deleteAdmin(row.id);
|
await deleteAdmin(row.id);
|
||||||
MessagePlugin.success('删除成功');
|
MessagePlugin.success('删除成功');
|
||||||
load();
|
gridApi.query();
|
||||||
d.destroy();
|
d.destroy();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 重置密码(需要输入新密码,保留原弹窗交互) */
|
||||||
function onResetPwd(row: AdminItem) {
|
function onResetPwd(row: AdminItem) {
|
||||||
let password = '';
|
let password = '';
|
||||||
const d = DialogPlugin.confirm({
|
const d = DialogPlugin.confirm({
|
||||||
@ -127,28 +118,98 @@ function onResetPwd(row: AdminItem) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const columns = [
|
const gridOptions: VxeTableGridOptions = {
|
||||||
{ colKey: 'id', title: 'ID', width: 70 },
|
columns: [
|
||||||
{ colKey: 'username', title: '用户名' },
|
{ type: 'seq', width: 50, title: '#' },
|
||||||
{ colKey: 'nickname', title: '昵称' },
|
{ field: 'id', title: 'ID', width: 70 },
|
||||||
{ colKey: 'statusText', title: '状态', width: 90 },
|
{ field: 'username', title: '用户名' },
|
||||||
{ colKey: 'roleNamesText', title: '角色' },
|
{ field: 'nickname', title: '昵称' },
|
||||||
{ colKey: 'createdAt', title: '创建时间', width: 170 },
|
{
|
||||||
{ colKey: 'op', title: '操作', width: 200 },
|
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 }) {
|
const [Grid, gridApi] = useVbenVxeGrid({ gridOptions });
|
||||||
query.page = p.current;
|
|
||||||
query.size = p.pageSize;
|
/** 触发查询(保留分页条件,因 grid 自动管理分页) */
|
||||||
pagination.current = p.current;
|
function search() {
|
||||||
pagination.pageSize = p.pageSize;
|
gridApi.query();
|
||||||
load();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(loadRoles);
|
||||||
load();
|
|
||||||
loadRoles();
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -166,9 +227,9 @@ onMounted(() => {
|
|||||||
clearable
|
clearable
|
||||||
placeholder="用户名/昵称"
|
placeholder="用户名/昵称"
|
||||||
style="width: 220px"
|
style="width: 220px"
|
||||||
@enter="load"
|
@enter="search"
|
||||||
/>
|
/>
|
||||||
<t-button theme="primary" @click="load">查询</t-button>
|
<t-button theme="primary" @click="search">查询</t-button>
|
||||||
</div>
|
</div>
|
||||||
<t-button
|
<t-button
|
||||||
v-if="hasAccessByCodes(['system:personnel:create'])"
|
v-if="hasAccessByCodes(['system:personnel:create'])"
|
||||||
@ -180,55 +241,9 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</t-card>
|
</t-card>
|
||||||
|
|
||||||
|
<!-- 人员列表(Vben Vxe Table + TableAction) -->
|
||||||
<t-card :bordered="false" class="wb-table-card">
|
<t-card :bordered="false" class="wb-table-card">
|
||||||
<t-table
|
<Grid />
|
||||||
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>
|
|
||||||
</t-card>
|
</t-card>
|
||||||
|
|
||||||
<t-dialog
|
<t-dialog
|
||||||
|
|||||||
@ -1,69 +1,102 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
import type { LoginLogItem } from '#/api/system';
|
import type { LoginLogItem } from '#/api/system';
|
||||||
|
|
||||||
import { onMounted, reactive, ref } from 'vue';
|
import { h, reactive } from 'vue';
|
||||||
|
|
||||||
import { useAccess } from '@vben/access';
|
import { useAccess } from '@vben/access';
|
||||||
import { Page } from '@vben/common-ui';
|
import { Page } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { Tag } from 'tdesign-vue-next';
|
||||||
|
|
||||||
|
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
import { getLoginLogList } from '#/api/system';
|
import { getLoginLogList } from '#/api/system';
|
||||||
|
|
||||||
// 登录日志页面:账号 + 结果筛选、分页列表
|
// 登录日志页面:Vben Vxe Table 分页列表 + 账号/结果筛选(筛选区保留 TDesign 组件,行为与原版一致)
|
||||||
const { hasAccessByCodes } = useAccess();
|
const { hasAccessByCodes } = useAccess();
|
||||||
|
|
||||||
const loading = ref(false);
|
|
||||||
const list = ref<LoginLogItem[]>([]);
|
|
||||||
|
|
||||||
// 查询条件:username 匹配账号,status 按结果过滤(-1 全部)
|
// 查询条件:username 匹配账号,status 按结果过滤(-1 全部)
|
||||||
const query = reactive({ page: 1, size: 10, username: '', status: -1 });
|
const query = reactive({ username: '', status: -1 });
|
||||||
const pagination = reactive({ current: 1, pageSize: 10, total: 0 });
|
|
||||||
|
|
||||||
async function load() {
|
// 表格列定义(status/failReason 用 slots 函数式渲染)
|
||||||
loading.value = true;
|
const gridOptions: VxeTableGridOptions = {
|
||||||
try {
|
columns: [
|
||||||
const data = await getLoginLogList({
|
{ type: 'seq', width: 50, title: '#' },
|
||||||
page: query.page,
|
{ field: 'id', title: 'ID', width: 70 },
|
||||||
size: query.size,
|
{ field: 'username', title: '登录账号', width: 140 },
|
||||||
username: query.username || undefined,
|
{ field: 'ip', title: '来源 IP', width: 140 },
|
||||||
status: query.status >= 0 ? query.status : undefined,
|
{
|
||||||
});
|
field: 'userAgent',
|
||||||
list.value = data.list ?? [];
|
title: '浏览器 UA',
|
||||||
pagination.total = data.total ?? 0;
|
width: 240,
|
||||||
} finally {
|
showOverflow: 'tooltip',
|
||||||
loading.value = false;
|
},
|
||||||
}
|
{
|
||||||
|
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() {
|
function reset() {
|
||||||
query.username = '';
|
query.username = '';
|
||||||
query.status = -1;
|
query.status = -1;
|
||||||
query.page = 1;
|
gridApi.reload();
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 表格列定义
|
|
||||||
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -82,7 +115,7 @@ onMounted(load);
|
|||||||
clearable
|
clearable
|
||||||
placeholder="登录账号"
|
placeholder="登录账号"
|
||||||
style="width: 180px"
|
style="width: 180px"
|
||||||
@enter="load"
|
@enter="search"
|
||||||
/>
|
/>
|
||||||
<t-select
|
<t-select
|
||||||
v-model="query.status"
|
v-model="query.status"
|
||||||
@ -96,7 +129,7 @@ onMounted(load);
|
|||||||
<t-button
|
<t-button
|
||||||
v-if="hasAccessByCodes(['system:login-log:list'])"
|
v-if="hasAccessByCodes(['system:login-log:list'])"
|
||||||
theme="primary"
|
theme="primary"
|
||||||
@click="load"
|
@click="search"
|
||||||
>
|
>
|
||||||
查询
|
查询
|
||||||
</t-button>
|
</t-button>
|
||||||
@ -105,31 +138,9 @@ onMounted(load);
|
|||||||
</div>
|
</div>
|
||||||
</t-card>
|
</t-card>
|
||||||
|
|
||||||
<!-- 登录日志列表 -->
|
<!-- 登录日志列表(Vben Vxe Table) -->
|
||||||
<t-card :bordered="false" class="wb-table-card">
|
<t-card :bordered="false" class="wb-table-card">
|
||||||
<t-table
|
<Grid />
|
||||||
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>
|
|
||||||
</t-card>
|
</t-card>
|
||||||
</Page>
|
</Page>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -1,13 +1,15 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
import type { MenuNode, RoleItem } from '#/api/system';
|
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 { 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 {
|
import {
|
||||||
createRole,
|
createRole,
|
||||||
deleteRole,
|
deleteRole,
|
||||||
@ -16,16 +18,15 @@ import {
|
|||||||
updateRole,
|
updateRole,
|
||||||
} from '#/api/system';
|
} from '#/api/system';
|
||||||
|
|
||||||
|
// 角色管理页面:Vben Vxe Table + Vben TableAction 行操作(权限码控制显隐)
|
||||||
const { hasAccessByCodes } = useAccess();
|
const { hasAccessByCodes } = useAccess();
|
||||||
|
|
||||||
const loading = ref(false);
|
|
||||||
const list = ref<RoleItem[]>([]);
|
|
||||||
const menuTree = ref<any[]>([]);
|
const menuTree = ref<any[]>([]);
|
||||||
const dialogOpen = ref(false);
|
const dialogOpen = ref(false);
|
||||||
const editing = ref<null | RoleItem>(null);
|
const editing = ref<null | RoleItem>(null);
|
||||||
|
|
||||||
const query = reactive({ page: 1, size: 10, keyword: '' });
|
// 筛选条件(分页由 grid 自动管理)
|
||||||
const pagination = reactive({ current: 1, pageSize: 10, total: 0 });
|
const query = reactive({ keyword: '' });
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
code: '',
|
code: '',
|
||||||
name: '',
|
name: '',
|
||||||
@ -33,6 +34,7 @@ const form = reactive({
|
|||||||
menuIds: [] as number[],
|
menuIds: [] as number[],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** 后端菜单树转 TDesign 树选择器数据 */
|
||||||
function toTree(nodes: MenuNode[]): any[] {
|
function toTree(nodes: MenuNode[]): any[] {
|
||||||
return (nodes ?? []).map((n) => ({
|
return (nodes ?? []).map((n) => ({
|
||||||
label: n.name,
|
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() {
|
async function loadMenus() {
|
||||||
const data = await getMenuTree();
|
const data = await getMenuTree();
|
||||||
menuTree.value = toTree(data.tree ?? []);
|
menuTree.value = toTree(data.tree ?? []);
|
||||||
@ -75,58 +66,115 @@ function openEdit(row: RoleItem) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function submit() {
|
async function submit() {
|
||||||
if (editing.value) {
|
// 编辑走更新、新增走创建
|
||||||
await updateRole(editing.value.id, {
|
editing.value
|
||||||
name: form.name,
|
? await updateRole(editing.value.id, {
|
||||||
status: form.status,
|
name: form.name,
|
||||||
menuIds: form.menuIds,
|
status: form.status,
|
||||||
});
|
menuIds: form.menuIds,
|
||||||
} else {
|
})
|
||||||
await createRole({
|
: await createRole({
|
||||||
code: form.code,
|
code: form.code,
|
||||||
name: form.name,
|
name: form.name,
|
||||||
status: form.status,
|
status: form.status,
|
||||||
menuIds: form.menuIds,
|
menuIds: form.menuIds,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
MessagePlugin.success('保存成功');
|
MessagePlugin.success('保存成功');
|
||||||
dialogOpen.value = false;
|
dialogOpen.value = false;
|
||||||
load();
|
gridApi.query();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 删除角色(保留原确认弹窗行为) */
|
||||||
function onDelete(row: RoleItem) {
|
function onDelete(row: RoleItem) {
|
||||||
const d = DialogPlugin.confirm({
|
const d = DialogPlugin.confirm({
|
||||||
header: `确认删除角色「${row.name}」?`,
|
header: `确认删除角色「${row.name}」?`,
|
||||||
onConfirm: async () => {
|
onConfirm: async () => {
|
||||||
await deleteRole(row.id);
|
await deleteRole(row.id);
|
||||||
MessagePlugin.success('删除成功');
|
MessagePlugin.success('删除成功');
|
||||||
load();
|
gridApi.query();
|
||||||
d.destroy();
|
d.destroy();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const columns = [
|
const gridOptions: VxeTableGridOptions = {
|
||||||
{ colKey: 'id', title: 'ID', width: 70 },
|
columns: [
|
||||||
{ colKey: 'code', title: '角色编码' },
|
{ type: 'seq', width: 50, title: '#' },
|
||||||
{ colKey: 'name', title: '角色名称' },
|
{ field: 'id', title: 'ID', width: 70 },
|
||||||
{ colKey: 'statusText', title: '状态', width: 90 },
|
{ field: 'code', title: '角色编码' },
|
||||||
{ colKey: 'createdAt', title: '创建时间', width: 170 },
|
{ field: 'name', title: '角色名称' },
|
||||||
{ colKey: 'op', title: '操作', width: 150 },
|
{
|
||||||
];
|
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 }) {
|
const [Grid, gridApi] = useVbenVxeGrid({ gridOptions });
|
||||||
query.page = p.current;
|
|
||||||
query.size = p.pageSize;
|
/** 触发查询(保留分页条件,因 grid 自动管理分页) */
|
||||||
pagination.current = p.current;
|
function search() {
|
||||||
pagination.pageSize = p.pageSize;
|
gridApi.query();
|
||||||
load();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(loadMenus);
|
||||||
load();
|
|
||||||
loadMenus();
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -144,9 +192,9 @@ onMounted(() => {
|
|||||||
clearable
|
clearable
|
||||||
placeholder="角色编码/名称"
|
placeholder="角色编码/名称"
|
||||||
style="width: 220px"
|
style="width: 220px"
|
||||||
@enter="load"
|
@enter="search"
|
||||||
/>
|
/>
|
||||||
<t-button theme="primary" @click="load">查询</t-button>
|
<t-button theme="primary" @click="search">查询</t-button>
|
||||||
</div>
|
</div>
|
||||||
<t-button
|
<t-button
|
||||||
v-if="hasAccessByCodes(['system:role:create'])"
|
v-if="hasAccessByCodes(['system:role:create'])"
|
||||||
@ -158,41 +206,9 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</t-card>
|
</t-card>
|
||||||
|
|
||||||
|
<!-- 角色列表(Vben Vxe Table + TableAction) -->
|
||||||
<t-card :bordered="false" class="wb-table-card">
|
<t-card :bordered="false" class="wb-table-card">
|
||||||
<t-table
|
<Grid />
|
||||||
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>
|
|
||||||
</t-card>
|
</t-card>
|
||||||
|
|
||||||
<t-dialog
|
<t-dialog
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user