feat(admin): 优化字典管理与QQBot账号页
This commit is contained in:
parent
e4a2bace24
commit
3c8455e8ac
@ -43,10 +43,23 @@ export namespace QqbotApi {
|
||||
lastError?: string;
|
||||
lastHeartbeatAt?: string;
|
||||
name: string;
|
||||
napcat?: AccountNapcatRuntime | null;
|
||||
remark?: string;
|
||||
selfId: string;
|
||||
}
|
||||
|
||||
export interface AccountNapcatRuntime {
|
||||
bindStatus?: 'bound' | 'disabled' | 'pending';
|
||||
containerId?: string;
|
||||
containerName?: string;
|
||||
containerStatus?: 'creating' | 'error' | 'running' | 'stopped';
|
||||
lastCheckedAt?: string;
|
||||
lastError?: string;
|
||||
lastLoginAt?: string;
|
||||
lastStartedAt?: string;
|
||||
webuiPort?: null | number;
|
||||
}
|
||||
|
||||
export interface AccountBody {
|
||||
accessToken?: string;
|
||||
connectionMode?: 'reverse-ws';
|
||||
|
||||
@ -21,6 +21,14 @@ export namespace SystemDictApi {
|
||||
treeKey: string;
|
||||
}
|
||||
|
||||
export interface DictGroup {
|
||||
dictCode: string;
|
||||
id: string;
|
||||
itemCount: number;
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export type DictInput = Omit<DictItem, 'createTime' | 'id' | 'updateTime'>;
|
||||
|
||||
export interface DictCodeOption {
|
||||
@ -47,6 +55,13 @@ async function getDictTree(params: Recordable<any>) {
|
||||
});
|
||||
}
|
||||
|
||||
async function getDictGroups(params: Recordable<any>) {
|
||||
return requestClient.get<SystemDictApi.PageResult<SystemDictApi.DictGroup>>(
|
||||
'/dict/groups',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
async function getDictCodeOptions() {
|
||||
return requestClient.get<SystemDictApi.DictCodeOption[]>('/dict/codes');
|
||||
}
|
||||
@ -82,6 +97,7 @@ export {
|
||||
createDict,
|
||||
deleteDict,
|
||||
getDictCodeOptions,
|
||||
getDictGroups,
|
||||
getDictList,
|
||||
getDictTree,
|
||||
toggleDictStatus,
|
||||
|
||||
@ -469,29 +469,63 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
/**
|
||||
* 为可调整行高的行追加 class 和高度 CSS 变量。
|
||||
* 为表格行追加交互、选中和可调整行高属性。
|
||||
*
|
||||
* @param record 当前行数据。
|
||||
*/
|
||||
function resolveRowProps(record: KtTableRecord) {
|
||||
if (!props.rowResizable) return {};
|
||||
const recordKey = String(resolveRecordKey(record));
|
||||
const classNames = [
|
||||
props.rowResizable ? 'kt-table__row--resizable' : '',
|
||||
props.onRowClick ? 'kt-table__row--clickable' : '',
|
||||
props.activeRowKey !== undefined &&
|
||||
String(props.activeRowKey) === recordKey
|
||||
? 'kt-table__row--active'
|
||||
: '',
|
||||
resolveCustomRowClassName(record),
|
||||
].filter(Boolean);
|
||||
const height = rowHeights[recordKey];
|
||||
const rowProps: KtTableRecord = {};
|
||||
|
||||
const height = rowHeights[String(resolveRecordKey(record))];
|
||||
if (classNames.length > 0) {
|
||||
rowProps.class = classNames.join(' ');
|
||||
}
|
||||
|
||||
return {
|
||||
class: 'kt-table__row--resizable',
|
||||
onMousedown: (event: MouseEvent) => {
|
||||
if (props.rowResizable) {
|
||||
rowProps.onMousedown = (event: MouseEvent) => {
|
||||
handleRowResizeMouseDown(event, record);
|
||||
},
|
||||
style: height
|
||||
};
|
||||
rowProps.style = height
|
||||
? {
|
||||
'--kt-table-row-height': `${height}px`,
|
||||
height: `${height}px`,
|
||||
}
|
||||
: undefined,
|
||||
: undefined;
|
||||
}
|
||||
|
||||
if (props.onRowClick) {
|
||||
rowProps.onClick = () => {
|
||||
props.onRowClick?.(record, context);
|
||||
};
|
||||
}
|
||||
|
||||
return rowProps;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析业务侧传入的行 class。
|
||||
*
|
||||
* @param record 当前行数据。
|
||||
*/
|
||||
function resolveCustomRowClassName(record: KtTableRecord) {
|
||||
if (!props.rowClassName) return '';
|
||||
if (typeof props.rowClassName === 'function') {
|
||||
return props.rowClassName(record, context) || '';
|
||||
}
|
||||
|
||||
return props.rowClassName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取查询参数,并按需触发表单校验。
|
||||
*
|
||||
|
||||
@ -35,6 +35,7 @@ export const DEFAULT_TABLE_SETTING: Required<KtTableSetting> = {
|
||||
export const KT_TABLE_PROP_KEYS = [
|
||||
'afterFetch',
|
||||
'api',
|
||||
'activeRowKey',
|
||||
'beforeFetch',
|
||||
'buttons',
|
||||
'columns',
|
||||
@ -43,10 +44,12 @@ export const KT_TABLE_PROP_KEYS = [
|
||||
'hooks',
|
||||
'immediate',
|
||||
'modules',
|
||||
'onRowClick',
|
||||
'pageSize',
|
||||
'pageSizeOptions',
|
||||
'rowActions',
|
||||
'rowActionVisibleCount',
|
||||
'rowClassName',
|
||||
'rowResizeMaxHeight',
|
||||
'rowResizeMinHeight',
|
||||
'rowResizable',
|
||||
@ -74,6 +77,7 @@ export function createDefaultTableProps(): KtTableResolvedProps<
|
||||
return {
|
||||
afterFetch: undefined,
|
||||
api: undefined,
|
||||
activeRowKey: undefined,
|
||||
beforeFetch: undefined,
|
||||
buttons: [],
|
||||
columns: [],
|
||||
@ -82,10 +86,12 @@ export function createDefaultTableProps(): KtTableResolvedProps<
|
||||
hooks: [],
|
||||
immediate: true,
|
||||
modules: [],
|
||||
onRowClick: undefined,
|
||||
pageSize: KT_TABLE_DEFAULT_PAGE_SIZE,
|
||||
pageSizeOptions: KT_TABLE_DEFAULT_PAGE_SIZE_OPTIONS,
|
||||
rowActions: [],
|
||||
rowActionVisibleCount: KT_TABLE_ROW_ACTION_VISIBLE_COUNT,
|
||||
rowClassName: undefined,
|
||||
rowResizeMaxHeight: KT_TABLE_DEFAULT_ROW_RESIZE_MAX_HEIGHT,
|
||||
rowResizeMinHeight: KT_TABLE_DEFAULT_ROW_RESIZE_MIN_HEIGHT,
|
||||
rowResizable: false,
|
||||
@ -109,6 +115,10 @@ export const ktTableProps = {
|
||||
default: undefined,
|
||||
type: Function as PropType<KtTableProps['afterFetch']>,
|
||||
},
|
||||
activeRowKey: {
|
||||
default: undefined,
|
||||
type: [Number, String] as PropType<KtTableProps['activeRowKey']>,
|
||||
},
|
||||
api: {
|
||||
default: undefined,
|
||||
type: Object as PropType<KtTableModule['api']>,
|
||||
@ -145,6 +155,10 @@ export const ktTableProps = {
|
||||
default: () => [],
|
||||
type: Array as PropType<KtTableModule[]>,
|
||||
},
|
||||
onRowClick: {
|
||||
default: undefined,
|
||||
type: Function as PropType<KtTableProps['onRowClick']>,
|
||||
},
|
||||
pageSize: {
|
||||
default: KT_TABLE_DEFAULT_PAGE_SIZE,
|
||||
type: Number,
|
||||
@ -161,6 +175,10 @@ export const ktTableProps = {
|
||||
default: KT_TABLE_ROW_ACTION_VISIBLE_COUNT,
|
||||
type: Number,
|
||||
},
|
||||
rowClassName: {
|
||||
default: undefined,
|
||||
type: [Function, String] as PropType<KtTableProps['rowClassName']>,
|
||||
},
|
||||
rowResizeMaxHeight: {
|
||||
default: KT_TABLE_DEFAULT_ROW_RESIZE_MAX_HEIGHT,
|
||||
type: Number,
|
||||
|
||||
@ -97,6 +97,22 @@
|
||||
background: hsl(var(--accent)) !important;
|
||||
}
|
||||
|
||||
.ant-table-tbody > tr#{kt.$block}__row--clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ant-table-tbody > tr#{kt.$block}__row--active > td,
|
||||
.ant-table-tbody > tr#{kt.$block}__row--active > td.ant-table-cell-fix-left,
|
||||
.ant-table-tbody
|
||||
> tr#{kt.$block}__row--active
|
||||
> td.ant-table-cell-fix-right,
|
||||
.ant-table-tbody
|
||||
> tr#{kt.$block}__row--active
|
||||
> td.ant-table-cell-fix-start,
|
||||
.ant-table-tbody > tr#{kt.$block}__row--active > td.ant-table-cell-fix-end {
|
||||
background: hsl(var(--accent)) !important;
|
||||
}
|
||||
|
||||
.ant-table-summary .ant-table-cell-fix-left,
|
||||
.ant-table-summary .ant-table-cell-fix-right,
|
||||
.ant-table-summary .ant-table-cell-fix-start,
|
||||
|
||||
@ -9,6 +9,7 @@ import type {
|
||||
} from '@vben/common-ui';
|
||||
|
||||
export type KtTableRecord = Record<string, any>;
|
||||
export type KtTableRowKey = number | string;
|
||||
|
||||
export type KtTableSize = 'large' | 'middle' | 'small';
|
||||
|
||||
@ -217,6 +218,7 @@ export interface KtTableProps<
|
||||
result: KtTablePageResult<Row> | Row[],
|
||||
context: KtTableContext<Row, SearchValues>,
|
||||
) => KtTablePageResult<Row> | Promise<KtTablePageResult<Row> | Row[]> | Row[];
|
||||
activeRowKey?: KtTableRowKey;
|
||||
beforeFetch?: (
|
||||
params: KtTableRecord & SearchValues,
|
||||
context: KtTableContext<Row, SearchValues>,
|
||||
@ -231,10 +233,17 @@ export interface KtTableProps<
|
||||
hooks?: Array<KtTableHook<Row, SearchValues>>;
|
||||
immediate?: boolean;
|
||||
modules?: Array<KtTableModule<Row, SearchValues>>;
|
||||
onRowClick?: (
|
||||
row: Row,
|
||||
context: KtTableContext<Row, SearchValues>,
|
||||
) => Promise<void> | void;
|
||||
pageSize?: number;
|
||||
pageSizeOptions?: string[];
|
||||
rowActions?: Array<KtTableRowAction<Row, SearchValues>>;
|
||||
rowActionVisibleCount?: number;
|
||||
rowClassName?:
|
||||
| ((row: Row, context: KtTableContext<Row, SearchValues>) => string)
|
||||
| string;
|
||||
rowResizeMaxHeight?: number;
|
||||
rowResizeMinHeight?: number;
|
||||
rowResizable?: boolean;
|
||||
@ -256,15 +265,23 @@ export type KtTableResolvedProps<
|
||||
Row extends KtTableRecord = KtTableRecord,
|
||||
SearchValues extends KtTableRecord = KtTableRecord,
|
||||
> = KtTableProps<Row, SearchValues> & {
|
||||
activeRowKey?: KtTableRowKey;
|
||||
buttons: Array<KtTableButton<Row, SearchValues>>;
|
||||
columns: Array<TableColumnType<Row>>;
|
||||
hooks: Array<KtTableHook<Row, SearchValues>>;
|
||||
immediate: boolean;
|
||||
modules: Array<KtTableModule<Row, SearchValues>>;
|
||||
onRowClick?: (
|
||||
row: Row,
|
||||
context: KtTableContext<Row, SearchValues>,
|
||||
) => Promise<void> | void;
|
||||
pageSize: number;
|
||||
pageSizeOptions: string[];
|
||||
rowActions: Array<KtTableRowAction<Row, SearchValues>>;
|
||||
rowActionVisibleCount: number;
|
||||
rowClassName?:
|
||||
| ((row: Row, context: KtTableContext<Row, SearchValues>) => string)
|
||||
| string;
|
||||
rowKey: ((row: Row) => string) | keyof Row | string;
|
||||
rowResizable: boolean;
|
||||
rowResizeMaxHeight: number;
|
||||
|
||||
@ -104,7 +104,9 @@ export default defineComponent({
|
||||
: 'default'
|
||||
}
|
||||
>
|
||||
{account.value.connectStatus === 'online' ? '在线' : '离线'}
|
||||
{account.value.connectStatus === 'online'
|
||||
? 'OneBot 在线'
|
||||
: 'OneBot 离线'}
|
||||
</Tag>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@ -34,6 +34,7 @@ import { KtTable, useKtTable } from '#/components/ktTable';
|
||||
const AKtTable = KtTable as any;
|
||||
const AButton = Button as any;
|
||||
const ATypographyLink = Typography.Link as any;
|
||||
const ATypographyText = Typography.Text as any;
|
||||
|
||||
export default defineComponent({
|
||||
name: 'QqBotAccountList',
|
||||
@ -41,6 +42,7 @@ export default defineComponent({
|
||||
const editingId = ref<string>();
|
||||
const router = useRouter();
|
||||
const scanLoading = ref(false);
|
||||
const scanQrcodeImageFailed = ref(false);
|
||||
const scanQrcodeText = ref('');
|
||||
const scanState = reactive<{
|
||||
containerId?: string;
|
||||
@ -61,6 +63,14 @@ export default defineComponent({
|
||||
margin: 2,
|
||||
scale: 8,
|
||||
});
|
||||
const scanQrcodeImageSrc = computed(() => {
|
||||
const qrcode = scanQrcodeText.value.trim();
|
||||
if (!qrcode) return '';
|
||||
if (!scanQrcodeImageFailed.value && isQrcodeImageCandidate(qrcode)) {
|
||||
return normalizeQrcodeImageSrc(qrcode);
|
||||
}
|
||||
return scanQrcode.value;
|
||||
});
|
||||
let scanTimer: number | undefined;
|
||||
|
||||
const [AccountForm, accountFormApi] = useVbenForm({
|
||||
@ -112,31 +122,31 @@ export default defineComponent({
|
||||
});
|
||||
|
||||
const columns: Array<TableColumnType<QqbotApi.Account>> = [
|
||||
{ dataIndex: 'selfId', key: 'selfId', title: 'Self ID', width: 160 },
|
||||
{ dataIndex: 'name', key: 'name', title: '账号名称', width: 180 },
|
||||
{ dataIndex: 'selfId', key: 'selfId', title: 'Self ID', width: 140 },
|
||||
{ dataIndex: 'name', key: 'name', title: '账号名称', width: 150 },
|
||||
{
|
||||
dataIndex: 'connectStatus',
|
||||
key: 'connectStatus',
|
||||
title: '连接状态',
|
||||
width: 120,
|
||||
key: 'accountOnlineStatus',
|
||||
title: '账号在线',
|
||||
width: 130,
|
||||
},
|
||||
{
|
||||
dataIndex: 'clientRole',
|
||||
key: 'clientRole',
|
||||
title: '连接角色',
|
||||
width: 120,
|
||||
dataIndex: 'napcat',
|
||||
key: 'napcatRuntime',
|
||||
title: 'NapCat 容器',
|
||||
width: 220,
|
||||
},
|
||||
{
|
||||
dataIndex: 'lastHeartbeatAt',
|
||||
key: 'lastHeartbeatAt',
|
||||
title: '最后心跳',
|
||||
title: '最近活动',
|
||||
width: 190,
|
||||
},
|
||||
{
|
||||
dataIndex: 'lastError',
|
||||
key: 'lastError',
|
||||
title: '错误信息',
|
||||
width: 260,
|
||||
key: 'runtimeSummary',
|
||||
title: '运行说明',
|
||||
width: 220,
|
||||
},
|
||||
];
|
||||
|
||||
@ -230,12 +240,12 @@ export default defineComponent({
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: [
|
||||
{ label: '在线', value: 'online' },
|
||||
{ label: '离线', value: 'offline' },
|
||||
{ label: 'OneBot 在线', value: 'online' },
|
||||
{ label: 'OneBot 离线', value: 'offline' },
|
||||
],
|
||||
},
|
||||
fieldName: 'connectStatus',
|
||||
label: '连接状态',
|
||||
label: '账号在线',
|
||||
},
|
||||
],
|
||||
},
|
||||
@ -323,7 +333,11 @@ export default defineComponent({
|
||||
scanState.sessionId = result.sessionId;
|
||||
scanState.status = result.status;
|
||||
scanState.webuiPort = result.webuiPort;
|
||||
scanQrcodeText.value = result.qrcode || '';
|
||||
const nextQrcode = result.qrcode || '';
|
||||
if (nextQrcode !== scanQrcodeText.value) {
|
||||
scanQrcodeImageFailed.value = false;
|
||||
}
|
||||
scanQrcodeText.value = nextQrcode;
|
||||
|
||||
if (result.status === 'pending') {
|
||||
startScanPolling();
|
||||
@ -389,6 +403,7 @@ export default defineComponent({
|
||||
status: 'idle',
|
||||
webuiPort: undefined,
|
||||
});
|
||||
scanQrcodeImageFailed.value = false;
|
||||
scanQrcodeText.value = '';
|
||||
}
|
||||
|
||||
@ -431,6 +446,157 @@ export default defineComponent({
|
||||
return '扫码登录请求失败';
|
||||
}
|
||||
|
||||
function isQrcodeImageCandidate(value: string) {
|
||||
return (
|
||||
/^data:image\//i.test(value) ||
|
||||
/^https?:\/\//i.test(value) ||
|
||||
isRawBase64Image(value)
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeQrcodeImageSrc(value: string) {
|
||||
if (isRawBase64Image(value)) {
|
||||
return `data:image/png;base64,${value}`;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function isRawBase64Image(value: string) {
|
||||
const normalized = value.trim();
|
||||
return (
|
||||
normalized.startsWith('iVBORw0KGgo') ||
|
||||
normalized.startsWith('/9j/') ||
|
||||
normalized.startsWith('R0lGOD')
|
||||
);
|
||||
}
|
||||
|
||||
const renderAccountOnlineStatus = (row: QqbotApi.Account) => {
|
||||
if (!row.enabled) {
|
||||
return <Tag color="default">已停用</Tag>;
|
||||
}
|
||||
return (
|
||||
<Tag color={row.connectStatus === 'online' ? 'success' : 'default'}>
|
||||
{row.connectStatus === 'online' ? 'OneBot 在线' : 'OneBot 离线'}
|
||||
</Tag>
|
||||
);
|
||||
};
|
||||
|
||||
const renderNapcatRuntime = (row: QqbotApi.Account) => {
|
||||
const napcat = row.napcat;
|
||||
const meta = getNapcatStatusMeta(napcat);
|
||||
return (
|
||||
<Space direction="vertical" size={2}>
|
||||
<Tag color={meta.color}>{meta.label}</Tag>
|
||||
{napcat?.containerName ? (
|
||||
<ATypographyText type="secondary">
|
||||
{napcat.containerName}
|
||||
{napcat.webuiPort ? `:${napcat.webuiPort}` : ''}
|
||||
</ATypographyText>
|
||||
) : null}
|
||||
</Space>
|
||||
);
|
||||
};
|
||||
|
||||
const renderRecentActivity = (row: QqbotApi.Account) => {
|
||||
const active = getRecentActivity(row);
|
||||
return (
|
||||
<Space direction="vertical" size={2}>
|
||||
<span>{active.label}</span>
|
||||
<ATypographyText type="secondary">
|
||||
{active.time || '暂无记录'}
|
||||
</ATypographyText>
|
||||
</Space>
|
||||
);
|
||||
};
|
||||
|
||||
const renderRuntimeSummary = (row: QqbotApi.Account) => {
|
||||
const summary = getRuntimeSummary(row);
|
||||
return (
|
||||
<ATypographyText
|
||||
title={summary.text}
|
||||
type={summary.level === 'warning' ? 'warning' : undefined}
|
||||
>
|
||||
{summary.text}
|
||||
</ATypographyText>
|
||||
);
|
||||
};
|
||||
|
||||
function getNapcatStatusMeta(
|
||||
napcat?: null | QqbotApi.AccountNapcatRuntime,
|
||||
) {
|
||||
if (!napcat) {
|
||||
return { color: 'default', label: '未绑定专属容器' };
|
||||
}
|
||||
if (!napcat.containerStatus) {
|
||||
return { color: 'warning', label: '容器记录缺失' };
|
||||
}
|
||||
const statusMap: Record<
|
||||
NonNullable<QqbotApi.AccountNapcatRuntime['containerStatus']>,
|
||||
{ color: string; label: string }
|
||||
> = {
|
||||
creating: { color: 'processing', label: '容器创建中' },
|
||||
error: { color: 'error', label: '容器异常' },
|
||||
running: { color: 'success', label: '容器运行中' },
|
||||
stopped: { color: 'default', label: '容器已停止' },
|
||||
};
|
||||
return statusMap[napcat.containerStatus];
|
||||
}
|
||||
|
||||
function getRecentActivity(row: QqbotApi.Account) {
|
||||
const candidates = [
|
||||
{ label: '最近心跳', value: row.lastHeartbeatAt },
|
||||
{ label: '最近连接', value: row.lastConnectedAt },
|
||||
{ label: '最近扫码登录', value: row.napcat?.lastLoginAt },
|
||||
{ label: '容器启动', value: row.napcat?.lastStartedAt },
|
||||
].filter((item) => item.value);
|
||||
const latest = candidates.toSorted(
|
||||
(left, right) =>
|
||||
new Date(right.value || '').getTime() -
|
||||
new Date(left.value || '').getTime(),
|
||||
)[0];
|
||||
return {
|
||||
label: latest?.label || '暂无活动',
|
||||
time: latest?.value ? formatDisplayTime(latest.value) : '',
|
||||
};
|
||||
}
|
||||
|
||||
function getRuntimeSummary(row: QqbotApi.Account) {
|
||||
if (!row.enabled) {
|
||||
return { level: 'warning', text: '账号已停用' };
|
||||
}
|
||||
if (row.lastError) {
|
||||
return { level: 'warning', text: `账号异常:${row.lastError}` };
|
||||
}
|
||||
if (row.napcat?.lastError) {
|
||||
return { level: 'warning', text: `容器异常:${row.napcat.lastError}` };
|
||||
}
|
||||
if (row.connectStatus === 'online') {
|
||||
return { level: 'normal', text: '消息链路可用' };
|
||||
}
|
||||
if (row.napcat?.containerStatus === 'running') {
|
||||
return { level: 'warning', text: '等待反向 WS' };
|
||||
}
|
||||
if (row.napcat?.containerStatus === 'creating') {
|
||||
return { level: 'warning', text: '容器创建中' };
|
||||
}
|
||||
if (row.napcat?.containerStatus === 'stopped') {
|
||||
return { level: 'warning', text: '容器已停止' };
|
||||
}
|
||||
if (!row.napcat) {
|
||||
return {
|
||||
level: 'warning',
|
||||
text: '可更新登录绑定容器',
|
||||
};
|
||||
}
|
||||
return { level: 'normal', text: '暂无异常记录' };
|
||||
}
|
||||
|
||||
function formatDisplayTime(value: string) {
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return value;
|
||||
return date.toLocaleString('zh-CN', { hour12: false });
|
||||
}
|
||||
|
||||
function getAccountFormDefaults(): QqbotApi.AccountBody {
|
||||
return {
|
||||
accessToken: '',
|
||||
@ -516,16 +682,17 @@ export default defineComponent({
|
||||
v-slots={{
|
||||
bodyCell: ({ column, record }: any) => {
|
||||
const row = record as QqbotApi.Account;
|
||||
if (column.key === 'connectStatus') {
|
||||
return (
|
||||
<Tag
|
||||
color={
|
||||
row.connectStatus === 'online' ? 'success' : 'default'
|
||||
if (column.key === 'accountOnlineStatus') {
|
||||
return renderAccountOnlineStatus(row);
|
||||
}
|
||||
>
|
||||
{row.connectStatus === 'online' ? '在线' : '离线'}
|
||||
</Tag>
|
||||
);
|
||||
if (column.key === 'napcatRuntime') {
|
||||
return renderNapcatRuntime(row);
|
||||
}
|
||||
if (column.key === 'lastHeartbeatAt') {
|
||||
return renderRecentActivity(row);
|
||||
}
|
||||
if (column.key === 'runtimeSummary') {
|
||||
return renderRuntimeSummary(row);
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
@ -579,7 +746,12 @@ export default defineComponent({
|
||||
{scanQrcodeText.value ? (
|
||||
<img
|
||||
alt="qqbot-login-qrcode"
|
||||
src={scanQrcode.value}
|
||||
onError={() => {
|
||||
if (isQrcodeImageCandidate(scanQrcodeText.value)) {
|
||||
scanQrcodeImageFailed.value = true;
|
||||
}
|
||||
}}
|
||||
src={scanQrcodeImageSrc.value}
|
||||
style={{
|
||||
background: '#fff',
|
||||
borderRadius: '8px',
|
||||
|
||||
@ -125,3 +125,17 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function useGroupFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '如 COMPONENT_TYPE',
|
||||
},
|
||||
fieldName: 'keyword',
|
||||
label: $t('system.dict.dictCode'),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@ -6,22 +6,33 @@ import type {
|
||||
KtTableApi,
|
||||
KtTableButton,
|
||||
KtTableContext,
|
||||
KtTablePageResult,
|
||||
KtTableRegisterApi,
|
||||
KtTableRowAction,
|
||||
} from '#/components/ktTable';
|
||||
|
||||
import { h } from 'vue';
|
||||
import { h, nextTick, ref, watch } from 'vue';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { Plus } from '@vben/icons';
|
||||
|
||||
import { message, Tag } from 'antdv-next';
|
||||
|
||||
import { deleteDict, getDictTree, toggleDictStatus } from '#/api/system/dict';
|
||||
import {
|
||||
deleteDict,
|
||||
getDictGroups,
|
||||
getDictList,
|
||||
toggleDictStatus,
|
||||
} from '#/api/system/dict';
|
||||
import { KtTable, useKtTable } from '#/components/ktTable';
|
||||
import { clearDictCache } from '#/hooks/useDict';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { getStatusOptions, useGridFormSchema } from './data';
|
||||
import {
|
||||
getStatusOptions,
|
||||
useGridFormSchema,
|
||||
useGroupFormSchema,
|
||||
} from './data';
|
||||
import Form from './modules/form.vue';
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
@ -30,8 +41,26 @@ const [FormModal, formModalApi] = useVbenModal({
|
||||
});
|
||||
|
||||
const statusOptions = getStatusOptions();
|
||||
const selectedDictCode = ref('');
|
||||
const itemTableRegistered = ref(false);
|
||||
|
||||
const columns: Array<TableColumnType<SystemDictApi.DictTreeItem>> = [
|
||||
const groupColumns: Array<TableColumnType<SystemDictApi.DictGroup>> = [
|
||||
{
|
||||
dataIndex: 'dictCode',
|
||||
key: 'dictCode',
|
||||
title: $t('system.dict.dictCode'),
|
||||
width: 220,
|
||||
},
|
||||
{
|
||||
align: 'right',
|
||||
dataIndex: 'itemCount',
|
||||
key: 'itemCount',
|
||||
title: '项数',
|
||||
width: 88,
|
||||
},
|
||||
];
|
||||
|
||||
const columns: Array<TableColumnType<SystemDictApi.DictItem>> = [
|
||||
{
|
||||
dataIndex: 'dictCode',
|
||||
fixed: 'left',
|
||||
@ -79,11 +108,27 @@ const columns: Array<TableColumnType<SystemDictApi.DictTreeItem>> = [
|
||||
},
|
||||
];
|
||||
|
||||
const api: KtTableApi<SystemDictApi.DictTreeItem> = {
|
||||
list: async (params) => await getDictTree(params),
|
||||
const groupApi: KtTableApi<SystemDictApi.DictGroup> = {
|
||||
list: async (params) => await getDictGroups(params),
|
||||
};
|
||||
|
||||
const buttons: Array<KtTableButton<SystemDictApi.DictTreeItem>> = [
|
||||
const api: KtTableApi<SystemDictApi.DictItem> = {
|
||||
list: async (params) => {
|
||||
if (!selectedDictCode.value) {
|
||||
return {
|
||||
items: [],
|
||||
total: 0,
|
||||
};
|
||||
}
|
||||
|
||||
return await getDictList({
|
||||
...params,
|
||||
dictCode: selectedDictCode.value,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
const buttons: Array<KtTableButton<SystemDictApi.DictItem>> = [
|
||||
{
|
||||
icon: () => h(Plus, { class: 'kt-table__button-icon' }),
|
||||
key: 'create',
|
||||
@ -94,7 +139,7 @@ const buttons: Array<KtTableButton<SystemDictApi.DictTreeItem>> = [
|
||||
},
|
||||
];
|
||||
|
||||
const rowActions: Array<KtTableRowAction<SystemDictApi.DictTreeItem>> = [
|
||||
const rowActions: Array<KtTableRowAction<SystemDictApi.DictItem>> = [
|
||||
{
|
||||
key: 'toggle',
|
||||
label: $t('system.dict.toggle'),
|
||||
@ -118,34 +163,130 @@ const rowActions: Array<KtTableRowAction<SystemDictApi.DictTreeItem>> = [
|
||||
},
|
||||
];
|
||||
|
||||
const [registerTable, tableApi] = useKtTable<SystemDictApi.DictTreeItem>({
|
||||
const [registerGroupTable, groupTableApi] = useKtTable<SystemDictApi.DictGroup>(
|
||||
{
|
||||
activeRowKey: selectedDictCode.value,
|
||||
afterFetch: onGroupAfterFetch,
|
||||
api: groupApi,
|
||||
columns: groupColumns,
|
||||
formOptions: {
|
||||
formGrid: {
|
||||
actionMinWidth: 180,
|
||||
actionSpan: 8,
|
||||
contentSpan: 16,
|
||||
fieldSpan: 16,
|
||||
},
|
||||
schema: useGroupFormSchema(),
|
||||
},
|
||||
onRowClick: onGroupRowClick,
|
||||
pageSize: 20,
|
||||
rowKey: 'dictCode',
|
||||
showIndex: false,
|
||||
showSelection: false,
|
||||
showTableSetting: false,
|
||||
tableTitle: '字典编码',
|
||||
},
|
||||
);
|
||||
|
||||
const [registerItemTable, tableApi] = useKtTable<SystemDictApi.DictItem>({
|
||||
api,
|
||||
buttons,
|
||||
columns,
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
schema: useGridFormSchema().filter((item) => item.fieldName !== 'dictCode'),
|
||||
},
|
||||
immediate: false,
|
||||
rowActions,
|
||||
rowKey: 'treeKey',
|
||||
showPagination: false,
|
||||
tableTitle: $t('system.dict.list'),
|
||||
rowKey: 'id',
|
||||
showPagination: true,
|
||||
tableTitle: getItemTableTitle(),
|
||||
});
|
||||
|
||||
function getStatusOption(status: SystemDictApi.DictTreeItem['status']) {
|
||||
watch(selectedDictCode, (dictCode) => {
|
||||
groupTableApi.setProps({
|
||||
activeRowKey: dictCode,
|
||||
});
|
||||
tableApi.setProps({
|
||||
tableTitle: getItemTableTitle(),
|
||||
});
|
||||
});
|
||||
|
||||
function getItemTableTitle() {
|
||||
return selectedDictCode.value
|
||||
? `字典项:${selectedDictCode.value}`
|
||||
: '字典项';
|
||||
}
|
||||
|
||||
function getStatusOption(status: SystemDictApi.DictItem['status']) {
|
||||
return statusOptions.find((item) => item.value === status);
|
||||
}
|
||||
|
||||
function onCreate() {
|
||||
formModalApi.setData(undefined).open();
|
||||
function normalizeGroupRows(
|
||||
result:
|
||||
| KtTablePageResult<SystemDictApi.DictGroup>
|
||||
| SystemDictApi.DictGroup[],
|
||||
) {
|
||||
if (Array.isArray(result)) return result;
|
||||
|
||||
return result.items || result.list || result.records || [];
|
||||
}
|
||||
|
||||
function onEdit(row: SystemDictApi.DictTreeItem) {
|
||||
async function onGroupAfterFetch(
|
||||
result:
|
||||
| KtTablePageResult<SystemDictApi.DictGroup>
|
||||
| SystemDictApi.DictGroup[],
|
||||
) {
|
||||
const rows = normalizeGroupRows(result);
|
||||
const selectedExists = rows.some(
|
||||
(item) => item.dictCode === selectedDictCode.value,
|
||||
);
|
||||
if (!selectedExists) {
|
||||
selectedDictCode.value = rows[0]?.dictCode || '';
|
||||
}
|
||||
|
||||
await reloadItemTable();
|
||||
return result;
|
||||
}
|
||||
|
||||
async function onGroupRowClick(row: SystemDictApi.DictGroup) {
|
||||
if (selectedDictCode.value === row.dictCode) return;
|
||||
|
||||
selectedDictCode.value = row.dictCode;
|
||||
await reloadItemTable();
|
||||
}
|
||||
|
||||
function onItemTableRegister(api: KtTableRegisterApi<SystemDictApi.DictItem>) {
|
||||
registerItemTable(api);
|
||||
itemTableRegistered.value = true;
|
||||
reloadItemTable();
|
||||
}
|
||||
|
||||
async function reloadItemTable() {
|
||||
if (!itemTableRegistered.value) return;
|
||||
|
||||
await nextTick();
|
||||
await tableApi.search();
|
||||
}
|
||||
|
||||
function onCreate() {
|
||||
formModalApi
|
||||
.setData(
|
||||
selectedDictCode.value
|
||||
? {
|
||||
dictCode: selectedDictCode.value,
|
||||
}
|
||||
: undefined,
|
||||
)
|
||||
.open();
|
||||
}
|
||||
|
||||
function onEdit(row: SystemDictApi.DictItem) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
async function onToggle(
|
||||
row: SystemDictApi.DictTreeItem,
|
||||
context: KtTableContext<SystemDictApi.DictTreeItem>,
|
||||
row: SystemDictApi.DictItem,
|
||||
context: KtTableContext<SystemDictApi.DictItem>,
|
||||
) {
|
||||
const nextStatus = row.status === 1 ? 0 : 1;
|
||||
await toggleDictStatus(row.id, nextStatus);
|
||||
@ -159,8 +300,8 @@ async function onToggle(
|
||||
}
|
||||
|
||||
async function onDelete(
|
||||
row: SystemDictApi.DictTreeItem,
|
||||
context?: KtTableContext<SystemDictApi.DictTreeItem>,
|
||||
row: SystemDictApi.DictItem,
|
||||
context?: KtTableContext<SystemDictApi.DictItem>,
|
||||
) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.label]),
|
||||
@ -176,20 +317,27 @@ async function onDelete(
|
||||
key: 'action_process_msg',
|
||||
});
|
||||
await (context || tableApi).reload();
|
||||
await groupTableApi.reload();
|
||||
} catch {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
function onRefresh() {
|
||||
tableApi.reload();
|
||||
async function onRefresh() {
|
||||
await groupTableApi.reload();
|
||||
await tableApi.reload();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<FormModal @success="onRefresh" />
|
||||
<KtTable @register="registerTable">
|
||||
<div class="dict-page">
|
||||
<section class="dict-page__groups">
|
||||
<KtTable @register="registerGroupTable" />
|
||||
</section>
|
||||
<section class="dict-page__items">
|
||||
<KtTable @register="onItemTableRegister">
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'childrenCode'">
|
||||
{{ record.childrenCode || '-' }}
|
||||
@ -201,5 +349,29 @@ function onRefresh() {
|
||||
</template>
|
||||
</template>
|
||||
</KtTable>
|
||||
</section>
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.dict-page {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(460px, 520px) minmax(0, 1fr);
|
||||
gap: 12px;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.dict-page__groups,
|
||||
.dict-page__items {
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.dict-page {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -18,7 +18,7 @@ const emit = defineEmits<{
|
||||
success: [];
|
||||
}>();
|
||||
|
||||
const formData = ref<SystemDictApi.DictItem>();
|
||||
const formData = ref<Partial<SystemDictApi.DictItem>>();
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', [$t('system.dict.name')])
|
||||
@ -51,7 +51,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||
},
|
||||
onOpenChange(isOpen) {
|
||||
if (!isOpen) return;
|
||||
const data = modalApi.getData<SystemDictApi.DictItem>();
|
||||
const data = modalApi.getData<Partial<SystemDictApi.DictItem>>();
|
||||
formData.value = data || undefined;
|
||||
resetForm();
|
||||
},
|
||||
|
||||
Loading…
Reference in New Issue
Block a user