This commit is contained in:
zwt13703 2026-03-23 19:17:13 +08:00
parent 94d28b4805
commit 3f214226b5
6 changed files with 446 additions and 147 deletions

View File

@ -15,3 +15,52 @@
1. 提取并扁平化嵌套路由中标记为 `constant` 的页面。 1. 提取并扁平化嵌套路由中标记为 `constant` 的页面。
2. 动态模式初始化常量路由时合并上述嵌套常量路由并去重。 2. 动态模式初始化常量路由时合并上述嵌套常量路由并去重。
- **执行结果**: 动态模式下 `system_oss-config` 已注册到路由表,跳转不再报错。 - **执行结果**: 动态模式下 `system_oss-config` 已注册到路由表,跳转不再报错。
## 会话 ID: 2026-03-22-03
- [2026-03-22 15:00:30]
- **执行原因**: 客户用户列表增加平台关联跳转,并提供操作列固定的通用能力
- **执行过程**:
1. 在表格 hook 中新增操作列固定配置,并在用户列表启用固定到右侧。
2. 用户列表操作栏新增“平台”按钮并携带用户 ID 跳转到平台用户页。
3. 平台用户页读取路由 query.userId 并联动筛选。
- **执行结果**: 用户列表可直接跳转平台用户关联页,操作列可固定显示,平台页支持按用户 ID 过滤。
## 会话 ID: 2026-03-22-04
- [2026-03-22 15:04:47]
- **执行原因**: 用户列表“平台”改为弹窗关联显示,避免页面跳转
- **执行过程**:
1. 用户列表引入平台用户列表组件并通过弹窗展示。
2. 平台用户列表支持接收预设用户 ID并自动联动筛选。
- **执行结果**: 点击“平台”在弹窗中展示对应平台用户关联信息。
## 会话 ID: 2026-03-22-05
- [2026-03-22 17:10:25]
- **执行原因**: 弹窗内列表被遮挡,需优化可视区域与滚动
- **执行过程**:
1. 平台用户列表增加嵌入式布局样式,放开滚动并限制高度。
2. 弹窗内容外层增加最大高度与滚动容器。
- **执行结果**: 弹窗内列表显示完整且可滚动查看。
## 会话 ID: 2026-03-22-06
- [2026-03-22 17:14:33]
- **执行原因**: 弹窗内仅显示表头与分页,表格内容被折叠
- **执行过程**:
1. 嵌入式场景关闭表格 flex 高度以避免容器高度为 0。
2. 嵌入式场景调整表格容器 class 以保证自适应高度展示。
- **执行结果**: 弹窗内表格内容正常显示。
## 会话 ID: 2026-03-22-07
- [2026-03-22 17:20:06]
- **执行原因**: 操作列 fixed 未生效,需确保横向滚动触发
- **执行过程**:
1. 平台用户列表设置最小 `scroll-x`,保证横向滚动容器存在。
2. 表格使用新的 `scroll-x` 以触发固定列效果。
- **执行结果**: 固定列在横向滚动时生效。
## 会话 ID: 2026-03-22-08
- [2026-03-22 17:25:37]
- **执行原因**: 弹窗内固定列仍无效果,需强化表格布局与滚动
- **执行过程**:
1. 弹窗嵌入场景提高最小 `scroll-x` 以确保横向滚动。
2. 弹窗嵌入场景强制表格 `table-layout``fixed`
- **执行结果**: 弹窗内固定列可随横向滚动保持固定。

View File

@ -23,6 +23,18 @@ export type UseNaiveTableOptions<ResponseData, ApiData, Pagination extends boole
* @returns true if the column is visible, false otherwise * @returns true if the column is visible, false otherwise
*/ */
getColumnVisible?: (column: NaiveUI.TableColumn<ApiData>) => boolean; getColumnVisible?: (column: NaiveUI.TableColumn<ApiData>) => boolean;
/**
* fixed the operate column
*
* @default undefined
*/
operateColumnFixed?: 'left' | 'right';
/**
* the key of operate column
*
* @default 'operate'
*/
operateColumnKey?: string;
}; };
const SELECTION_KEY = '__selection__'; const SELECTION_KEY = '__selection__';
@ -33,8 +45,12 @@ export function useNaiveTable<ResponseData, ApiData>(options: UseNaiveTableOptio
const scope = effectScope(); const scope = effectScope();
const appStore = useAppStore(); const appStore = useAppStore();
const columns = () =>
applyOperateColumnFixed(options.columns(), options.operateColumnFixed, options.operateColumnKey);
const result = useTable<ResponseData, ApiData, NaiveUI.TableColumn<ApiData>, false>({ const result = useTable<ResponseData, ApiData, NaiveUI.TableColumn<ApiData>, false>({
...options, ...options,
columns,
getColumnChecks: cols => getColumnChecks(cols, options.getColumnVisible), getColumnChecks: cols => getColumnChecks(cols, options.getColumnVisible),
getColumns getColumns
}); });
@ -125,9 +141,13 @@ export function useNaivePaginatedTable<ResponseData, ApiData>(
}; };
}); });
const columns = () =>
applyOperateColumnFixed(options.columns(), options.operateColumnFixed, options.operateColumnKey);
const result = useTable<ResponseData, ApiData, NaiveUI.TableColumn<ApiData>, true>({ const result = useTable<ResponseData, ApiData, NaiveUI.TableColumn<ApiData>, true>({
...options, ...options,
pagination: true, pagination: true,
columns,
getColumnChecks: cols => getColumnChecks(cols, options.getColumnVisible), getColumnChecks: cols => getColumnChecks(cols, options.getColumnVisible),
getColumns, getColumns,
onFetched: data => { onFetched: data => {
@ -180,6 +200,25 @@ export function useNaivePaginatedTable<ResponseData, ApiData>(
}; };
} }
function applyOperateColumnFixed<Column extends NaiveUI.TableColumn<any>>(
columns: Column[],
fixed?: 'left' | 'right',
key: string = 'operate'
) {
if (!fixed) return columns;
return columns.map(column => {
if (isTableColumnHasKey(column) && column.key === key && !column.fixed) {
return {
...column,
fixed
};
}
return column;
});
}
export function useTableOperate<TableData>( export function useTableOperate<TableData>(
data: Ref<TableData[]>, data: Ref<TableData[]>,
idKey: keyof TableData, idKey: keyof TableData,

View File

@ -1,122 +1,170 @@
<script setup lang="tsx"> <script setup lang="tsx">
import { ref } from 'vue'; import { computed, ref, watch } from "vue";
import { NDivider } from 'naive-ui'; import { NDivider } from "naive-ui";
import { fetchBatchDeletePlatformUser, fetchGetPlatformUserList } from '@/service/api/client/platform-user'; import { useRoute } from "vue-router";
import { useAppStore } from '@/store/modules/app'; import {
import { useAuth } from '@/hooks/business/auth'; fetchBatchDeletePlatformUser,
import { useDownload } from '@/hooks/business/download'; fetchGetPlatformUserList,
import { defaultTransform, useNaivePaginatedTable, useTableOperate } from '@/hooks/common/table'; } from "@/service/api/client/platform-user";
import { $t } from '@/locales'; import { useAppStore } from "@/store/modules/app";
import ButtonIcon from '@/components/custom/button-icon.vue'; import { useAuth } from "@/hooks/business/auth";
import PlatformUserOperateDrawer from './modules/platform-user-operate-drawer.vue'; import { useDownload } from "@/hooks/business/download";
import PlatformUserSearch from './modules/platform-user-search.vue'; import {
defaultTransform,
useNaivePaginatedTable,
useTableOperate,
} from "@/hooks/common/table";
import { $t } from "@/locales";
import ButtonIcon from "@/components/custom/button-icon.vue";
import PlatformUserOperateDrawer from "./modules/platform-user-operate-drawer.vue";
import PlatformUserSearch from "./modules/platform-user-search.vue";
defineOptions({ defineOptions({
name: 'PlatformUserList' name: "PlatformUserList",
}); });
interface Props {
presetUserId?: CommonType.IdType | null;
embedded?: boolean;
}
const props = defineProps<Props>();
const appStore = useAppStore(); const appStore = useAppStore();
const { download } = useDownload(); const { download } = useDownload();
const { hasAuth } = useAuth(); const { hasAuth } = useAuth();
const route = useRoute();
function getUserIdFromRoute(queryUserId: unknown) {
if (Array.isArray(queryUserId)) {
return queryUserId[0] ?? null;
}
if (typeof queryUserId === "string" && queryUserId.trim()) {
return queryUserId;
}
return null;
}
const resolvedUserId = computed(() => {
if (
props.presetUserId !== undefined &&
props.presetUserId !== null &&
String(props.presetUserId).trim()
) {
return String(props.presetUserId);
}
return getUserIdFromRoute(route.query.userId);
});
const searchParams = ref<Api.Client.PlatformUserSearchParams>({ const searchParams = ref<Api.Client.PlatformUserSearchParams>({
pageNum: 1, pageNum: 1,
pageSize: 10, pageSize: 10,
userId: null, userId: resolvedUserId.value,
platformType: null, platformType: null,
platformOpenid: null, platformOpenid: null,
platformUnionid: null, platformUnionid: null,
platformSessionKey: null, platformSessionKey: null,
platformExtra: null, platformExtra: null,
lastLoginTime: null, lastLoginTime: null,
params: {} params: {},
}); });
const { columns, columnChecks, data, getData, getDataByPage, loading, mobilePagination, scrollX } = const {
useNaivePaginatedTable({ columns,
columnChecks,
data,
getData,
getDataByPage,
loading,
mobilePagination,
scrollX,
} = useNaivePaginatedTable({
api: () => fetchGetPlatformUserList(searchParams.value), api: () => fetchGetPlatformUserList(searchParams.value),
transform: response => defaultTransform(response), transform: (response) => defaultTransform(response),
onPaginationParamsChange: params => { onPaginationParamsChange: (params) => {
searchParams.value.pageNum = params.page; searchParams.value.pageNum = params.page;
searchParams.value.pageSize = params.pageSize; searchParams.value.pageSize = params.pageSize;
}, },
columns: () => [ columns: () => [
{ {
type: 'selection', type: "selection",
align: 'center', align: "center",
width: 48 width: 48,
}, },
{ {
key: 'index', key: "index",
title: $t('common.index'), title: $t("common.index"),
align: 'center', align: "center",
width: 64, width: 64,
render: (_, index) => index + 1 render: (_, index) => index + 1,
}, },
{ {
key: 'id', key: "id",
title: '平台用户ID自增', title: "平台用户ID",
align: 'center', align: "center",
minWidth: 120 width: 120,
}, },
{ {
key: 'userId', key: "platformType",
title: '关联t_user.id', title: "平台类型",
align: 'center', align: "center",
minWidth: 120 width: 120,
render: (row: any) => {
if (row.platformType == 1) return <n-tag>微信小程序</n-tag>;
if (row.platformType == 2) return <n-tag>抖音小程序</n-tag>;
if (row.platformType == 3) return <n-tag>支付宝小程序</n-tag>;
return "未知";
},
}, },
{ {
key: 'platformType', key: "platformOpenid",
title: '平台类型1-微信小程序2-抖音小程序3-支付宝小程序', title: "OPENID",
align: 'center', align: "center",
minWidth: 120 minWidth: 120,
}, },
{ {
key: 'platformOpenid', key: "platformUnionid",
title: '平台唯一标识微信openid/抖音open_id', title: "平台统一标识微信unionid多小程序互通用",
align: 'center', align: "center",
minWidth: 120 minWidth: 120,
}, },
{ {
key: 'platformUnionid', key: "platformSessionKey",
title: '平台统一标识微信unionid多小程序互通用', title: "平台会话密钥微信session_key加密存储",
align: 'center', align: "center",
minWidth: 120 minWidth: 120,
}, },
{ {
key: 'platformSessionKey', key: "platformExtra",
title: '平台会话密钥微信session_key加密存储', title: "平台扩展字段如抖音的user_name、微信的city等",
align: 'center', align: "center",
minWidth: 120 minWidth: 120,
}, },
{ {
key: 'platformExtra', key: "lastLoginTime",
title: '平台扩展字段如抖音的user_name、微信的city等', title: "最后登录时间",
align: 'center', align: "center",
minWidth: 120 minWidth: 120,
}, },
{ {
key: 'lastLoginTime', key: "operate",
title: '最后登录时间', title: $t("common.operate"),
align: 'center', align: "center",
minWidth: 120 fixed: "right",
},
{
key: 'operate',
title: $t('common.operate'),
align: 'center',
width: 130, width: 130,
render: row => { render: (row) => {
const divider = () => { const divider = () => {
if (!hasAuth('client:platformUser:edit') || !hasAuth('client:platformUser:remove')) { if (
!hasAuth("client:platformUser:edit") ||
!hasAuth("client:platformUser:remove")
) {
return null; return null;
} }
return <NDivider vertical />; return <NDivider vertical />;
}; };
const editBtn = () => { const editBtn = () => {
if (!hasAuth('client:platformUser:edit')) { if (!hasAuth("client:platformUser:edit")) {
return null; return null;
} }
return ( return (
@ -124,14 +172,14 @@ const { columns, columnChecks, data, getData, getDataByPage, loading, mobilePagi
text text
type="primary" type="primary"
icon="material-symbols:drive-file-rename-outline-outline" icon="material-symbols:drive-file-rename-outline-outline"
tooltipContent={$t('common.edit')} tooltipContent={$t("common.edit")}
onClick={() => edit(row.id)} onClick={() => edit(row.id)}
/> />
); );
}; };
const deleteBtn = () => { const deleteBtn = () => {
if (!hasAuth('client:platformUser:remove')) { if (!hasAuth("client:platformUser:remove")) {
return null; return null;
} }
return ( return (
@ -139,8 +187,8 @@ const { columns, columnChecks, data, getData, getDataByPage, loading, mobilePagi
text text
type="error" type="error"
icon="material-symbols:delete-outline" icon="material-symbols:delete-outline"
tooltipContent={$t('common.delete')} tooltipContent={$t("common.delete")}
popconfirmContent={$t('common.confirmDelete')} popconfirmContent={$t("common.confirmDelete")}
onPositiveClick={() => handleDelete(row.id)} onPositiveClick={() => handleDelete(row.id)}
/> />
); );
@ -153,13 +201,54 @@ const { columns, columnChecks, data, getData, getDataByPage, loading, mobilePagi
{deleteBtn()} {deleteBtn()}
</div> </div>
); );
} },
} },
] ],
}); });
const { drawerVisible, operateType, editingData, handleAdd, handleEdit, checkedRowKeys, onBatchDeleted, onDeleted } = const {
useTableOperate(data, 'id', getData); drawerVisible,
operateType,
editingData,
handleAdd,
handleEdit,
checkedRowKeys,
onBatchDeleted,
onDeleted,
} = useTableOperate(data, "id", getData);
const rootClass = computed(() =>
props.embedded
? "flex-col-stretch gap-12px max-h-80vh overflow-auto"
: "min-h-500px flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto",
);
const cardClass = computed(() =>
props.embedded ? "card-wrapper" : "card-wrapper sm:flex-1-hidden",
);
const tableFlexHeight = computed(() => !appStore.isMobile && !props.embedded);
const tableClass = computed(() => (props.embedded ? "w-full" : "sm:h-full"));
const tableScrollX = computed(() =>
props.embedded
? Math.max(scrollX.value, 1800)
: Math.max(scrollX.value, 1600),
);
const tableLayout = computed(() => (props.embedded ? "fixed" : "auto"));
function syncUserIdFromRoute() {
const nextUserId = resolvedUserId.value;
if (searchParams.value.userId === nextUserId) return;
searchParams.value.userId = nextUserId;
getDataByPage(1);
}
watch(resolvedUserId, () => {
syncUserIdFromRoute();
});
async function handleBatchDelete() { async function handleBatchDelete() {
// request // request
@ -180,14 +269,23 @@ function edit(id: CommonType.IdType) {
} }
function handleExport() { function handleExport() {
download('/client/platformUser/export', searchParams.value, `平台用户关联(微信/抖音小程序用户信息_${new Date().getTime()}.xlsx`); download(
"/client/platformUser/export",
searchParams.value,
`平台用户关联(微信/抖音小程序用户信息_${new Date().getTime()}.xlsx`,
);
} }
</script> </script>
<template> <template>
<div class="min-h-500px flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto"> <div :class="rootClass">
<PlatformUserSearch v-model:model="searchParams" @search="getDataByPage" /> <PlatformUserSearch v-model:model="searchParams" @search="getDataByPage" />
<NCard title="平台用户关联(微信/抖音小程序用户信息)列表" :bordered="false" size="small" class="card-wrapper sm:flex-1-hidden"> <NCard
title="平台用户关联(微信/抖音小程序用户信息)列表"
:bordered="false"
size="small"
:class="cardClass"
>
<template #header-extra> <template #header-extra>
<TableHeaderOperation <TableHeaderOperation
v-model:columns="columnChecks" v-model:columns="columnChecks"
@ -207,13 +305,14 @@ function handleExport() {
:columns="columns" :columns="columns"
:data="data" :data="data"
size="small" size="small"
:flex-height="!appStore.isMobile" :flex-height="tableFlexHeight"
:scroll-x="scrollX" :table-layout="tableLayout"
:scroll-x="tableScrollX"
:loading="loading" :loading="loading"
remote remote
:row-key="row => row.id" :row-key="(row) => row.id"
:pagination="mobilePagination" :pagination="mobilePagination"
class="sm:h-full" :class="tableClass"
/> />
<PlatformUserOperateDrawer <PlatformUserOperateDrawer
v-model:visible="drawerVisible" v-model:visible="drawerVisible"

View File

@ -1,12 +1,15 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref, watch } from 'vue'; import { computed, ref, watch } from "vue";
import { jsonClone } from '@sa/utils'; import { jsonClone } from "@sa/utils";
import { fetchCreatePlatformUser, fetchUpdatePlatformUser } from '@/service/api/client/platform-user'; import {
import { useFormRules, useNaiveForm } from '@/hooks/common/form'; fetchCreatePlatformUser,
import { $t } from '@/locales'; fetchUpdatePlatformUser,
} from "@/service/api/client/platform-user";
import { useFormRules, useNaiveForm } from "@/hooks/common/form";
import { $t } from "@/locales";
defineOptions({ defineOptions({
name: 'PlatformUserOperateDrawer' name: "PlatformUserOperateDrawer",
}); });
interface Props { interface Props {
@ -19,13 +22,13 @@ interface Props {
const props = defineProps<Props>(); const props = defineProps<Props>();
interface Emits { interface Emits {
(e: 'submitted'): void; (e: "submitted"): void;
} }
const emit = defineEmits<Emits>(); const emit = defineEmits<Emits>();
const visible = defineModel<boolean>('visible', { const visible = defineModel<boolean>("visible", {
default: false default: false,
}); });
const { formRef, validate, restoreValidation } = useNaiveForm(); const { formRef, validate, restoreValidation } = useNaiveForm();
@ -33,8 +36,8 @@ const { createRequiredRule } = useFormRules();
const title = computed(() => { const title = computed(() => {
const titles: Record<NaiveUI.TableOperateType, string> = { const titles: Record<NaiveUI.TableOperateType, string> = {
add: '新增平台用户关联(微信/抖音小程序用户信息)', add: "新增平台用户关联(微信/抖音小程序用户信息)",
edit: '编辑平台用户关联(微信/抖音小程序用户信息)' edit: "编辑平台用户关联(微信/抖音小程序用户信息)",
}; };
return titles[props.operateType]; return titles[props.operateType];
}); });
@ -48,33 +51,34 @@ function createDefaultModel(): Model {
id: null, id: null,
userId: null, userId: null,
platformType: null, platformType: null,
platformOpenid: '', platformOpenid: "",
platformUnionid: '', platformUnionid: "",
platformSessionKey: '', platformSessionKey: "",
platformExtra: '', platformExtra: "",
lastLoginTime: null, lastLoginTime: null,
}; };
} }
type RuleKey = Extract< type RuleKey = Extract<
keyof Model, keyof Model,
| 'id' "id" | "userId" | "platformType" | "platformOpenid"
| 'userId'
| 'platformType'
| 'platformOpenid'
>; >;
const rules: Record<RuleKey, App.Global.FormRule> = { const rules: Record<RuleKey, App.Global.FormRule> = {
id: createRequiredRule('平台用户ID自增不能为空'), id: createRequiredRule("平台用户ID自增不能为空"),
userId: createRequiredRule('关联t_user.id不能为空'), userId: createRequiredRule("关联t_user.id不能为空"),
platformType: createRequiredRule('平台类型1-微信小程序2-抖音小程序3-支付宝小程序不能为空'), platformType: createRequiredRule(
platformOpenid: createRequiredRule('平台唯一标识微信openid/抖音open_id不能为空'), "平台类型1-微信小程序2-抖音小程序3-支付宝小程序不能为空",
),
platformOpenid: createRequiredRule(
"平台唯一标识微信openid/抖音open_id不能为空",
),
}; };
function handleUpdateModelWhenEdit() { function handleUpdateModelWhenEdit() {
model.value = createDefaultModel(); model.value = createDefaultModel();
if (props.operateType === 'edit' && props.rowData) { if (props.operateType === "edit" && props.rowData) {
Object.assign(model.value, jsonClone(props.rowData)); Object.assign(model.value, jsonClone(props.rowData));
} }
} }
@ -86,41 +90,78 @@ function closeDrawer() {
async function handleSubmit() { async function handleSubmit() {
await validate(); await validate();
const { id, userId, platformType, platformOpenid, platformUnionid, platformSessionKey, platformExtra, lastLoginTime } = model.value; const {
id,
userId,
platformType,
platformOpenid,
platformUnionid,
platformSessionKey,
platformExtra,
lastLoginTime,
} = model.value;
// request // request
if (props.operateType === 'add') { if (props.operateType === "add") {
const { error } = await fetchCreatePlatformUser({ userId, platformType, platformOpenid, platformUnionid, platformSessionKey, platformExtra, lastLoginTime }); const { error } = await fetchCreatePlatformUser({
userId,
platformType,
platformOpenid,
platformUnionid,
platformSessionKey,
platformExtra,
lastLoginTime,
});
if (error) return; if (error) return;
} }
if (props.operateType === 'edit') { if (props.operateType === "edit") {
const { error } = await fetchUpdatePlatformUser({ id, userId, platformType, platformOpenid, platformUnionid, platformSessionKey, platformExtra, lastLoginTime }); const { error } = await fetchUpdatePlatformUser({
id,
userId,
platformType,
platformOpenid,
platformUnionid,
platformSessionKey,
platformExtra,
lastLoginTime,
});
if (error) return; if (error) return;
} }
window.$message?.success($t('common.updateSuccess')); window.$message?.success($t("common.updateSuccess"));
closeDrawer(); closeDrawer();
emit('submitted'); emit("submitted");
} }
watch(visible, () => { watch(visible, () => {
if (visible.value) { if (visible.value) {
handleUpdateModelWhenEdit(); handleUpdateModelWhenEdit();
restoreValidation(); restoreValidation();
getTreeList();
} }
}); });
</script> </script>
<template> <template>
<NDrawer v-model:show="visible" :title="title" display-directive="show" :width="800" class="max-w-90%"> <NDrawer
v-model:show="visible"
:title="title"
display-directive="show"
:width="800"
class="max-w-90%"
>
<NDrawerContent :title="title" :native-scrollbar="false" closable> <NDrawerContent :title="title" :native-scrollbar="false" closable>
<NForm ref="formRef" :model="model" :rules="rules"> <NForm ref="formRef" :model="model" :rules="rules">
<NFormItem label="关联t_user.id" path="userId"> <NFormItem label="关联t_user.id" path="userId">
<NInput v-model:value="model.userId" placeholder="请输入关联t_user.id" /> <NInput
v-model:value="model.userId"
placeholder="请输入关联t_user.id"
/>
</NFormItem> </NFormItem>
<NFormItem label="平台类型1-微信小程序2-抖音小程序3-支付宝小程序" path="platformType"> <NFormItem
label="平台类型1-微信小程序2-抖音小程序3-支付宝小程序"
path="platformType"
>
<NSelect <NSelect
v-model:value="model.platformType" v-model:value="model.platformType"
placeholder="请选择平台类型1-微信小程序2-抖音小程序3-支付宝小程序" placeholder="请选择平台类型1-微信小程序2-抖音小程序3-支付宝小程序"
@ -128,17 +169,41 @@ watch(visible, () => {
clearable clearable
/> />
</NFormItem> </NFormItem>
<NFormItem label="平台唯一标识微信openid/抖音open_id" path="platformOpenid"> <NFormItem
<NInput v-model:value="model.platformOpenid" placeholder="请输入平台唯一标识微信openid/抖音open_id" /> label="平台唯一标识微信openid/抖音open_id"
path="platformOpenid"
>
<NInput
v-model:value="model.platformOpenid"
placeholder="请输入平台唯一标识微信openid/抖音open_id"
/>
</NFormItem> </NFormItem>
<NFormItem label="平台统一标识微信unionid多小程序互通用" path="platformUnionid"> <NFormItem
<NInput v-model:value="model.platformUnionid" placeholder="请输入平台统一标识微信unionid多小程序互通用" /> label="平台统一标识微信unionid多小程序互通用"
path="platformUnionid"
>
<NInput
v-model:value="model.platformUnionid"
placeholder="请输入平台统一标识微信unionid多小程序互通用"
/>
</NFormItem> </NFormItem>
<NFormItem label="平台会话密钥微信session_key加密存储" path="platformSessionKey"> <NFormItem
<NInput v-model:value="model.platformSessionKey" placeholder="请输入平台会话密钥微信session_key加密存储" /> label="平台会话密钥微信session_key加密存储"
path="platformSessionKey"
>
<NInput
v-model:value="model.platformSessionKey"
placeholder="请输入平台会话密钥微信session_key加密存储"
/>
</NFormItem> </NFormItem>
<NFormItem label="平台扩展字段如抖音的user_name、微信的city等" path="platformExtra"> <NFormItem
<NInput v-model:value="model.platformExtra" placeholder="请输入平台扩展字段如抖音的user_name、微信的city等" /> label="平台扩展字段如抖音的user_name、微信的city等"
path="platformExtra"
>
<NInput
v-model:value="model.platformExtra"
placeholder="请输入平台扩展字段如抖音的user_name、微信的city等"
/>
</NFormItem> </NFormItem>
<NFormItem label="最后登录时间" path="lastLoginTime"> <NFormItem label="最后登录时间" path="lastLoginTime">
<NDatePicker <NDatePicker
@ -151,8 +216,10 @@ watch(visible, () => {
</NForm> </NForm>
<template #footer> <template #footer>
<NSpace :size="16"> <NSpace :size="16">
<NButton @click="closeDrawer">{{ $t('common.cancel') }}</NButton> <NButton @click="closeDrawer">{{ $t("common.cancel") }}</NButton>
<NButton type="primary" @click="handleSubmit">{{ $t('common.confirm') }}</NButton> <NButton type="primary" @click="handleSubmit">{{
$t("common.confirm")
}}</NButton>
</NSpace> </NSpace>
</template> </template>
</NDrawerContent> </NDrawerContent>

View File

@ -15,6 +15,7 @@ import {
} from "@/hooks/common/table"; } from "@/hooks/common/table";
import { $t } from "@/locales"; import { $t } from "@/locales";
import ButtonIcon from "@/components/custom/button-icon.vue"; import ButtonIcon from "@/components/custom/button-icon.vue";
import PlatformUserList from "@/views/client/platform-user/index.vue";
import UserOperateDrawer from "./modules/user-operate-drawer.vue"; import UserOperateDrawer from "./modules/user-operate-drawer.vue";
import UserSearch from "./modules/user-search.vue"; import UserSearch from "./modules/user-search.vue";
@ -25,6 +26,8 @@ defineOptions({
const appStore = useAppStore(); const appStore = useAppStore();
const { download } = useDownload(); const { download } = useDownload();
const { hasAuth } = useAuth(); const { hasAuth } = useAuth();
const platformUserVisible = ref(false);
const platformUserId = ref<CommonType.IdType | null>(null);
const searchParams = ref<Api.Client.UserSearchParams>({ const searchParams = ref<Api.Client.UserSearchParams>({
pageNum: 1, pageNum: 1,
@ -52,6 +55,7 @@ const {
} = useNaivePaginatedTable({ } = useNaivePaginatedTable({
api: () => fetchGetUserList(searchParams.value), api: () => fetchGetUserList(searchParams.value),
transform: (response) => defaultTransform(response), transform: (response) => defaultTransform(response),
operateColumnFixed: "right",
onPaginationParamsChange: (params) => { onPaginationParamsChange: (params) => {
searchParams.value.pageNum = params.page; searchParams.value.pageNum = params.page;
searchParams.value.pageSize = params.pageSize; searchParams.value.pageSize = params.pageSize;
@ -66,7 +70,7 @@ const {
key: "index", key: "index",
title: $t("common.index"), title: $t("common.index"),
align: "center", align: "center",
width: 64, width: 50,
render: (_, index) => index + 1, render: (_, index) => index + 1,
}, },
{ {
@ -92,6 +96,15 @@ const {
title: "用户头像URL", title: "用户头像URL",
align: "center", align: "center",
minWidth: 120, minWidth: 120,
render: (row: any) => {
return (
<img
src={row.avatarUrl}
alt="avatar"
style="width: 50%; transform: translateX(50%);"
/>
);
},
}, },
{ {
key: "phone", key: "phone",
@ -105,6 +118,7 @@ const {
align: "center", align: "center",
minWidth: 120, minWidth: 120,
render: (row: any) => { render: (row: any) => {
console.log(row.gender);
if (row.gender === 1) return <p></p>; if (row.gender === 1) return <p></p>;
if (row.gender === 2) return <p></p>; if (row.gender === 2) return <p></p>;
return <p>未知</p>; return <p>未知</p>;
@ -126,15 +140,12 @@ const {
align: "center", align: "center",
width: 130, width: 130,
render: (row) => { render: (row) => {
const divider = () => { const canEdit = hasAuth("client:user:edit");
if (!hasAuth("client:user:edit") || !hasAuth("client:user:remove")) { const canDelete = hasAuth("client:user:remove");
return null; const canPlatform = true;
}
return <NDivider vertical />;
};
const editBtn = () => { const editBtn = () => {
if (!hasAuth("client:user:edit")) { if (!canEdit) {
return null; return null;
} }
return ( return (
@ -148,8 +159,23 @@ const {
); );
}; };
const platformBtn = () => {
if (!canPlatform) {
return null;
}
return (
<ButtonIcon
text
type="info"
icon="material-symbols:link"
tooltipContent="平台"
onClick={() => handleToPlatformUser(row.id)}
/>
);
};
const deleteBtn = () => { const deleteBtn = () => {
if (!hasAuth("client:user:remove")) { if (!canDelete) {
return null; return null;
} }
return ( return (
@ -167,7 +193,9 @@ const {
return ( return (
<div class="flex-center gap-8px"> <div class="flex-center gap-8px">
{editBtn()} {editBtn()}
{divider()} {canEdit && canPlatform ? <NDivider vertical /> : null}
{platformBtn()}
{canPlatform && canDelete ? <NDivider vertical /> : null}
{deleteBtn()} {deleteBtn()}
</div> </div>
); );
@ -205,6 +233,11 @@ function edit(id: CommonType.IdType) {
handleEdit(id); handleEdit(id);
} }
function handleToPlatformUser(id: CommonType.IdType) {
platformUserId.value = id;
platformUserVisible.value = true;
}
function handleExport() { function handleExport() {
download( download(
"/client/user/export", "/client/user/export",
@ -219,6 +252,18 @@ function handleExport() {
class="min-h-500px flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto" class="min-h-500px flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto"
> >
<UserSearch v-model:model="searchParams" @search="getDataByPage" /> <UserSearch v-model:model="searchParams" @search="getDataByPage" />
<NModal
v-model:show="platformUserVisible"
preset="card"
title="平台用户关联"
class="max-w-90% w-1200px"
size="huge"
:bordered="false"
>
<div class="max-h-80vh overflow-auto">
<PlatformUserList :preset-user-id="platformUserId" :embedded="true" />
</div>
</NModal>
<NCard <NCard
title="客户用户基础信息列表" title="客户用户基础信息列表"
:bordered="false" :bordered="false"

View File

@ -162,17 +162,17 @@ watch(visible, () => {
placeholder="请输入手机号" placeholder="请输入手机号"
/> />
</NFormItem> </NFormItem>
<NFormItem label="性别0-未知1-男2-女" path="gender"> <NFormItem label="性别" path="gender">
<NRadioGroup v-model:value="model.gender"> <NRadioGroup v-model:value="model.gender">
<NRadio value="0">未知</NRadio> <NRadio :value="0">未知</NRadio>
<NRadio value="1"></NRadio> <NRadio :value="1"></NRadio>
<NRadio value="2"></NRadio> <NRadio :value="2"></NRadio>
</NRadioGroup> </NRadioGroup>
</NFormItem> </NFormItem>
<NFormItem label="状态0-禁用1-正常" path="status"> <NFormItem label="状态" path="status">
<NRadioGroup v-model:value="model.status"> <NRadioGroup v-model:value="model.status">
<NRadio value="0">禁用</NRadio> <NRadio :value="0">禁用</NRadio>
<NRadio value="1">正常</NRadio> <NRadio :value="1">正常</NRadio>
</NRadioGroup> </NRadioGroup>
</NFormItem> </NFormItem>
</NForm> </NForm>