feat: 优化了若干接口页面,可用性存疑
This commit is contained in:
@@ -67,3 +67,59 @@ export async function updateRole(data: Record<string, any>) {
|
||||
export async function deleteRole(data: Record<string, any>) {
|
||||
return requestClient.post<any>(`${prefix}delete`, data);
|
||||
}
|
||||
|
||||
const workbenchPrefix = 'role-workbench-widget/';
|
||||
|
||||
/**
|
||||
* 工作台模块可选字典(code + 中文名)
|
||||
*/
|
||||
export async function getWorkbenchWidgetOptions() {
|
||||
return requestClient.get<any>(`${workbenchPrefix}options`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按角色获取已配置的工作台模块(配置回显)
|
||||
*/
|
||||
export async function getWorkbenchWidgetByRoleId(roleId: number) {
|
||||
return requestClient.get<any>(`${workbenchPrefix}get-by-role-id`, {
|
||||
params: { role_id: roleId },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量保存角色工作台模块(按数组顺序写排序)
|
||||
*/
|
||||
export async function batchSaveWorkbenchWidget(data: {
|
||||
role_id: number;
|
||||
widget_codes: string[];
|
||||
}) {
|
||||
return requestClient.post<any>(`${workbenchPrefix}batch-save`, data);
|
||||
}
|
||||
|
||||
const wxEntryPrefix = 'wx-workbench-entry/';
|
||||
|
||||
/**
|
||||
* 小程序功能入口全量字典(角色分配抽屉可选项,含分组)
|
||||
*/
|
||||
export async function getWxWorkbenchEntryOptions() {
|
||||
return requestClient.get<any>(`${wxEntryPrefix}options`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按角色获取已分配的小程序功能入口(配置回显,按排序返回)
|
||||
*/
|
||||
export async function getWxWorkbenchEntryByRoleId(roleId: number) {
|
||||
return requestClient.get<any>(`${wxEntryPrefix}get-by-role-id`, {
|
||||
params: { role_id: roleId },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量保存角色的小程序功能入口分配(按数组顺序写排序)
|
||||
*/
|
||||
export async function batchSaveWxWorkbenchEntry(data: {
|
||||
entry_ids: number[];
|
||||
role_id: number;
|
||||
}) {
|
||||
return requestClient.post<any>(`${wxEntryPrefix}batch-save`, data);
|
||||
}
|
||||
|
||||
283
apps/web-antd/src/views/system/role/components/mp-entry.vue
Normal file
283
apps/web-antd/src/views/system/role/components/mp-entry.vue
Normal file
@@ -0,0 +1,283 @@
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* 角色小程序功能入口分配抽屉:勾选入口 + 上移/下移排序,保存调 wx-workbench-entry/batch-save
|
||||
* 交互完整复刻 workbench-widget.vue(模块少用箭头排序,不引拖拽库);
|
||||
* 与之的差异:可选项来自 xk_wx_workbench_entry 字典并按 group_name 分组展示
|
||||
*/
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenDrawer } from '@vben/common-ui';
|
||||
import { IconifyIcon as VbenIcon } from '@vben/icons';
|
||||
|
||||
import { Button, Checkbox, message } from 'ant-design-vue';
|
||||
|
||||
import {
|
||||
batchSaveWxWorkbenchEntry,
|
||||
getWxWorkbenchEntryByRoleId,
|
||||
getWxWorkbenchEntryOptions,
|
||||
} from '../api';
|
||||
|
||||
/** 字典入口项 */
|
||||
type EntryOption = {
|
||||
code: string;
|
||||
description: string;
|
||||
group_name: string;
|
||||
id: number;
|
||||
name: string;
|
||||
};
|
||||
|
||||
const record = ref<any>({});
|
||||
const options = ref<EntryOption[]>([]);
|
||||
/** 已分配的入口 id(数组顺序即小程序展示顺序) */
|
||||
const enabledIds = ref<number[]>([]);
|
||||
|
||||
const drawerTitle = computed(() => {
|
||||
const roleName = record.value?.name || '';
|
||||
return roleName ? `小程序入口 - ${roleName}` : '小程序入口';
|
||||
});
|
||||
|
||||
/** 未分配的入口,按分组聚合(供勾选添加) */
|
||||
const disabledGroups = computed(() => {
|
||||
const rest = options.value.filter(
|
||||
(item) => !enabledIds.value.includes(item.id),
|
||||
);
|
||||
const groups: { items: EntryOption[]; name: string }[] = [];
|
||||
rest.forEach((item) => {
|
||||
const name = item.group_name || '未分组';
|
||||
let group = groups.find((g) => g.name === name);
|
||||
if (!group) {
|
||||
group = { name, items: [] };
|
||||
groups.push(group);
|
||||
}
|
||||
group.items.push(item);
|
||||
});
|
||||
return groups;
|
||||
});
|
||||
|
||||
/**
|
||||
* 入口 id → 展示名(带分组前缀便于区分不同端的同名入口,如两端都有「提现管理」)
|
||||
*/
|
||||
function labelOf(id: number) {
|
||||
const item = options.value.find((option) => option.id === id);
|
||||
if (!item) return `#${id}`;
|
||||
return item.group_name ? `【${item.group_name}】${item.name}` : item.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* 勾选/取消某入口
|
||||
*/
|
||||
function toggle(id: number, checked: boolean) {
|
||||
if (checked) {
|
||||
if (!enabledIds.value.includes(id)) {
|
||||
enabledIds.value.push(id);
|
||||
}
|
||||
} else {
|
||||
enabledIds.value = enabledIds.value.filter((item) => item !== id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 上移/下移调整顺序(offset -1 上移 / 1 下移)
|
||||
*/
|
||||
function move(index: number, offset: number) {
|
||||
const target = index + offset;
|
||||
if (target < 0 || target >= enabledIds.value.length) return;
|
||||
const list = [...enabledIds.value];
|
||||
const [item] = list.splice(index, 1);
|
||||
list.splice(target, 0, item!);
|
||||
enabledIds.value = list;
|
||||
}
|
||||
|
||||
/**
|
||||
* 拉取字典 + 角色已分配入口
|
||||
*/
|
||||
async function fetchData() {
|
||||
const roleId = Number(record.value?.id || 0);
|
||||
if (!roleId) return;
|
||||
try {
|
||||
const [opts, assigned] = await Promise.all([
|
||||
getWxWorkbenchEntryOptions(),
|
||||
getWxWorkbenchEntryByRoleId(roleId),
|
||||
]);
|
||||
options.value = Array.isArray(opts) ? opts : [];
|
||||
enabledIds.value = (Array.isArray(assigned) ? assigned : []).map(
|
||||
(item: any) => Number(item.entry_id),
|
||||
);
|
||||
} catch {
|
||||
message.error('获取小程序入口配置失败');
|
||||
options.value = [];
|
||||
enabledIds.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存配置(空数组表示清空该角色全部入口,小程序端会回落硬编码菜单保可用)
|
||||
*/
|
||||
async function handleSave() {
|
||||
const roleId = Number(record.value?.id || 0);
|
||||
if (!roleId) {
|
||||
message.error('角色ID不能为空');
|
||||
return;
|
||||
}
|
||||
DrawerApi.lock();
|
||||
try {
|
||||
await batchSaveWxWorkbenchEntry({
|
||||
role_id: roleId,
|
||||
entry_ids: enabledIds.value,
|
||||
});
|
||||
message.success('保存成功');
|
||||
DrawerApi.close();
|
||||
} catch {
|
||||
message.error('保存失败');
|
||||
} finally {
|
||||
DrawerApi.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
const [Drawer, DrawerApi] = useVbenDrawer({
|
||||
onOpenChange(isOpen) {
|
||||
if (isOpen) {
|
||||
record.value = DrawerApi.getData()?.record || {};
|
||||
options.value = [];
|
||||
enabledIds.value = [];
|
||||
DrawerApi.setState({ loading: true });
|
||||
fetchData().finally(() => {
|
||||
DrawerApi.setState({ loading: false });
|
||||
});
|
||||
}
|
||||
},
|
||||
onConfirm: handleSave,
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
DrawerApi,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Drawer :title="drawerTitle" class="w-[420px]">
|
||||
<div class="entry-config">
|
||||
<div class="entry-config__section-title">已分配(自上而下为展示顺序)</div>
|
||||
<div v-if="enabledIds.length > 0" class="entry-config__list">
|
||||
<div
|
||||
v-for="(id, index) in enabledIds"
|
||||
:key="id"
|
||||
class="entry-config__row"
|
||||
>
|
||||
<span class="entry-config__label">{{ labelOf(id) }}</span>
|
||||
<span class="entry-config__ops">
|
||||
<Button
|
||||
:disabled="index === 0"
|
||||
size="small"
|
||||
type="text"
|
||||
@click="move(index, -1)"
|
||||
>
|
||||
<VbenIcon icon="lucide:arrow-up" />
|
||||
</Button>
|
||||
<Button
|
||||
:disabled="index === enabledIds.length - 1"
|
||||
size="small"
|
||||
type="text"
|
||||
@click="move(index, 1)"
|
||||
>
|
||||
<VbenIcon icon="lucide:arrow-down" />
|
||||
</Button>
|
||||
<Button danger size="small" type="text" @click="toggle(id, false)">
|
||||
<VbenIcon icon="lucide:x" />
|
||||
</Button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="entry-config__empty">
|
||||
未分配任何入口,小程序端将回落默认菜单
|
||||
</div>
|
||||
|
||||
<div class="entry-config__section-title mt-4">可添加</div>
|
||||
<template v-if="disabledGroups.length > 0">
|
||||
<div
|
||||
v-for="group in disabledGroups"
|
||||
:key="group.name"
|
||||
class="entry-config__group"
|
||||
>
|
||||
<div class="entry-config__group-title">{{ group.name }}</div>
|
||||
<div class="entry-config__list">
|
||||
<div
|
||||
v-for="item in group.items"
|
||||
:key="item.id"
|
||||
class="entry-config__row"
|
||||
>
|
||||
<Checkbox
|
||||
:checked="false"
|
||||
@update:checked="(val: boolean) => toggle(item.id, val)"
|
||||
>
|
||||
{{ item.name }}
|
||||
<span v-if="item.description" class="entry-config__desc">
|
||||
{{ item.description }}
|
||||
</span>
|
||||
</Checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="entry-config__empty">全部入口已分配</div>
|
||||
</div>
|
||||
</Drawer>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.entry-config__section-title {
|
||||
margin-bottom: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.entry-config__group {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.entry-config__group-title {
|
||||
margin-bottom: 6px;
|
||||
font-size: 12px;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.entry-config__list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.entry-config__row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 10px;
|
||||
background: hsl(var(--muted) / 0.25);
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.entry-config__label {
|
||||
font-size: 13px;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.entry-config__desc {
|
||||
margin-left: 6px;
|
||||
font-size: 12px;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.entry-config__ops {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.entry-config__empty {
|
||||
padding: 12px 0;
|
||||
font-size: 12px;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,234 @@
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* 角色工作台模块配置抽屉:勾选模块 + 上移/下移排序,保存调 batch-save
|
||||
* 为什么用上下移而不是拖拽:模块总数少(≤7 个),箭头排序更轻,无需引 SortableJS
|
||||
*/
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenDrawer } from '@vben/common-ui';
|
||||
import { IconifyIcon as VbenIcon } from '@vben/icons';
|
||||
|
||||
import { Button, Checkbox, message } from 'ant-design-vue';
|
||||
|
||||
import {
|
||||
batchSaveWorkbenchWidget,
|
||||
getWorkbenchWidgetByRoleId,
|
||||
getWorkbenchWidgetOptions,
|
||||
} from '../api';
|
||||
|
||||
/** 模块字典项 */
|
||||
type WidgetOption = {
|
||||
code: string;
|
||||
label: string;
|
||||
};
|
||||
|
||||
const record = ref<any>({});
|
||||
const options = ref<WidgetOption[]>([]);
|
||||
/** 已启用的模块 code(数组顺序即展示顺序) */
|
||||
const enabledCodes = ref<string[]>([]);
|
||||
|
||||
const drawerTitle = computed(() => {
|
||||
const roleName = record.value?.name || '';
|
||||
return roleName ? `工作台模块 - ${roleName}` : '工作台模块';
|
||||
});
|
||||
|
||||
/** 未启用的模块(供勾选添加) */
|
||||
const disabledOptions = computed(() =>
|
||||
options.value.filter((item) => !enabledCodes.value.includes(item.code)),
|
||||
);
|
||||
|
||||
/**
|
||||
* code → 中文名
|
||||
*/
|
||||
function labelOf(code: string) {
|
||||
return options.value.find((item) => item.code === code)?.label || code;
|
||||
}
|
||||
|
||||
/**
|
||||
* 勾选/取消某模块
|
||||
*/
|
||||
function toggle(code: string, checked: boolean) {
|
||||
if (checked) {
|
||||
if (!enabledCodes.value.includes(code)) {
|
||||
enabledCodes.value.push(code);
|
||||
}
|
||||
} else {
|
||||
enabledCodes.value = enabledCodes.value.filter((item) => item !== code);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 上移/下移调整顺序(offset -1 上移 / 1 下移)
|
||||
*/
|
||||
function move(index: number, offset: number) {
|
||||
const target = index + offset;
|
||||
if (target < 0 || target >= enabledCodes.value.length) return;
|
||||
const list = [...enabledCodes.value];
|
||||
const [item] = list.splice(index, 1);
|
||||
list.splice(target, 0, item!);
|
||||
enabledCodes.value = list;
|
||||
}
|
||||
|
||||
/**
|
||||
* 拉取字典 + 角色已配置模块
|
||||
*/
|
||||
async function fetchData() {
|
||||
const roleId = Number(record.value?.id || 0);
|
||||
if (!roleId) return;
|
||||
try {
|
||||
const [opts, layout] = await Promise.all([
|
||||
getWorkbenchWidgetOptions(),
|
||||
getWorkbenchWidgetByRoleId(roleId),
|
||||
]);
|
||||
options.value = Array.isArray(opts) ? opts : [];
|
||||
enabledCodes.value = (Array.isArray(layout) ? layout : []).map(
|
||||
(item: any) => item.widget_code,
|
||||
);
|
||||
} catch {
|
||||
message.error('获取工作台模块配置失败');
|
||||
options.value = [];
|
||||
enabledCodes.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存配置(空数组表示清空该角色全部模块)
|
||||
*/
|
||||
async function handleSave() {
|
||||
const roleId = Number(record.value?.id || 0);
|
||||
if (!roleId) {
|
||||
message.error('角色ID不能为空');
|
||||
return;
|
||||
}
|
||||
DrawerApi.lock();
|
||||
try {
|
||||
await batchSaveWorkbenchWidget({
|
||||
role_id: roleId,
|
||||
widget_codes: enabledCodes.value,
|
||||
});
|
||||
message.success('保存成功');
|
||||
DrawerApi.close();
|
||||
} catch {
|
||||
message.error('保存失败');
|
||||
} finally {
|
||||
DrawerApi.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
const [Drawer, DrawerApi] = useVbenDrawer({
|
||||
onOpenChange(isOpen) {
|
||||
if (isOpen) {
|
||||
record.value = DrawerApi.getData()?.record || {};
|
||||
options.value = [];
|
||||
enabledCodes.value = [];
|
||||
DrawerApi.setState({ loading: true });
|
||||
fetchData().finally(() => {
|
||||
DrawerApi.setState({ loading: false });
|
||||
});
|
||||
}
|
||||
},
|
||||
onConfirm: handleSave,
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
DrawerApi,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Drawer :title="drawerTitle" class="w-[420px]">
|
||||
<div class="widget-config">
|
||||
<div class="widget-config__section-title">已启用(自上而下为展示顺序)</div>
|
||||
<div v-if="enabledCodes.length > 0" class="widget-config__list">
|
||||
<div
|
||||
v-for="(code, index) in enabledCodes"
|
||||
:key="code"
|
||||
class="widget-config__row"
|
||||
>
|
||||
<span class="widget-config__label">{{ labelOf(code) }}</span>
|
||||
<span class="widget-config__ops">
|
||||
<Button
|
||||
:disabled="index === 0"
|
||||
size="small"
|
||||
type="text"
|
||||
@click="move(index, -1)"
|
||||
>
|
||||
<VbenIcon icon="lucide:arrow-up" />
|
||||
</Button>
|
||||
<Button
|
||||
:disabled="index === enabledCodes.length - 1"
|
||||
size="small"
|
||||
type="text"
|
||||
@click="move(index, 1)"
|
||||
>
|
||||
<VbenIcon icon="lucide:arrow-down" />
|
||||
</Button>
|
||||
<Button danger size="small" type="text" @click="toggle(code, false)">
|
||||
<VbenIcon icon="lucide:x" />
|
||||
</Button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="widget-config__empty">未启用任何模块,工作台将为空</div>
|
||||
|
||||
<div class="widget-config__section-title mt-4">可添加</div>
|
||||
<div v-if="disabledOptions.length > 0" class="widget-config__list">
|
||||
<div
|
||||
v-for="item in disabledOptions"
|
||||
:key="item.code"
|
||||
class="widget-config__row"
|
||||
>
|
||||
<Checkbox
|
||||
:checked="false"
|
||||
@update:checked="(val: boolean) => toggle(item.code, val)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</Checkbox>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="widget-config__empty">全部模块已启用</div>
|
||||
</div>
|
||||
</Drawer>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.widget-config__section-title {
|
||||
margin-bottom: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.widget-config__list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.widget-config__row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 10px;
|
||||
background: hsl(var(--muted) / 0.25);
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.widget-config__label {
|
||||
font-size: 13px;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.widget-config__ops {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.widget-config__empty {
|
||||
padding: 12px 0;
|
||||
font-size: 12px;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
</style>
|
||||
@@ -15,7 +15,12 @@ import FormModalDemo from './components/modal.vue';
|
||||
import { formOptions } from './config/search';
|
||||
import { gridOptions } from './config/table';
|
||||
import AuthMenu from "#/views/system/role/components/auth-menu.vue";
|
||||
import MpEntry from "#/views/system/role/components/mp-entry.vue";
|
||||
import QuickNav from "#/views/system/role/components/quick-nav.vue";
|
||||
import WorkbenchWidget from "#/views/system/role/components/workbench-widget.vue";
|
||||
|
||||
// 支持配置小程序工作台入口的角色(1/2平台 3/4/6业务员线 8诊所管理员 14诊所推广员)
|
||||
const MP_ENTRY_ROLE_IDS = [1, 2, 3, 4, 6, 8, 14];
|
||||
|
||||
const hasTopTableDropDownActions = ref(false);
|
||||
|
||||
@@ -77,6 +82,28 @@ const handleQuickNav = (record: any) => {
|
||||
quickNavRef.value.DrawerApi.open();
|
||||
}
|
||||
};
|
||||
|
||||
// 工作台模块配置
|
||||
const workbenchWidgetRef = ref();
|
||||
const handleWorkbenchWidget = (record: any) => {
|
||||
if (workbenchWidgetRef.value && workbenchWidgetRef.value.DrawerApi) {
|
||||
workbenchWidgetRef.value.DrawerApi.setData({
|
||||
record,
|
||||
});
|
||||
workbenchWidgetRef.value.DrawerApi.open();
|
||||
}
|
||||
};
|
||||
|
||||
// 小程序功能入口分配
|
||||
const mpEntryRef = ref();
|
||||
const handleMpEntry = (record: any) => {
|
||||
if (mpEntryRef.value && mpEntryRef.value.DrawerApi) {
|
||||
mpEntryRef.value.DrawerApi.setData({
|
||||
record,
|
||||
});
|
||||
mpEntryRef.value.DrawerApi.open();
|
||||
}
|
||||
};
|
||||
const deleteApi = (row: any) => {
|
||||
let ids = [];
|
||||
if (row) {
|
||||
@@ -96,6 +123,8 @@ const deleteApi = (row: any) => {
|
||||
<FormModal />
|
||||
<AuthMenu ref="authMenuRef" />
|
||||
<QuickNav ref="quickNavRef" />
|
||||
<WorkbenchWidget ref="workbenchWidgetRef" />
|
||||
<MpEntry ref="mpEntryRef" />
|
||||
<Grid>
|
||||
<template #toolbar-actions>
|
||||
<TableAction
|
||||
@@ -160,6 +189,22 @@ const deleteApi = (row: any) => {
|
||||
// auth: ['admin', 'sys:role:detail'],
|
||||
onClick: handleQuickNav.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: '工作台模块',
|
||||
type: 'link',
|
||||
icon: 'mdi:view-dashboard-outline',
|
||||
size: 'small',
|
||||
// auth: ['admin', 'sys:role:detail'],
|
||||
onClick: handleWorkbenchWidget.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: '小程序入口',
|
||||
type: 'link',
|
||||
icon: 'mdi:cellphone-cog',
|
||||
size: 'small',
|
||||
ifShow: MP_ENTRY_ROLE_IDS.includes(Number(row.id)),
|
||||
onClick: handleMpEntry.bind(null, row),
|
||||
},
|
||||
]"
|
||||
:drop-down-actions="[
|
||||
{
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
/** 小程序工作台功能入口字典 API */
|
||||
const prefix = 'wx-workbench-entry/';
|
||||
|
||||
export async function getWxWorkbenchEntryList(data: any) {
|
||||
return requestClient.get<any>(`${prefix}list`, { params: data });
|
||||
}
|
||||
|
||||
export async function createWxWorkbenchEntry(data: Record<string, any>) {
|
||||
return requestClient.post<any>(`${prefix}create`, data);
|
||||
}
|
||||
|
||||
export async function updateWxWorkbenchEntry(data: Record<string, any>) {
|
||||
return requestClient.post<any>(`${prefix}update`, data);
|
||||
}
|
||||
|
||||
export async function deleteWxWorkbenchEntry(data: Record<string, any>) {
|
||||
return requestClient.post<any>(`${prefix}delete`, data);
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* 小程序工作台功能入口新增/编辑弹窗
|
||||
* theme_color 为虚拟字段:回显时从存库 theme JSON 解析主色,提交时组装回 {bg,color}
|
||||
*/
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
|
||||
import { createWxWorkbenchEntry, updateWxWorkbenchEntry } from '../api';
|
||||
import { buildThemeByColor, parseThemeColor } from '../config/constants';
|
||||
import { modalFormProps } from '../config/form';
|
||||
|
||||
const isUpdate = ref(false);
|
||||
const gridApi = ref();
|
||||
const [Form, formApi] = useVbenForm(modalFormProps);
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
fullscreenButton: false,
|
||||
draggable: true,
|
||||
onCancel() {
|
||||
modalApi.close();
|
||||
},
|
||||
onConfirm: async () => {
|
||||
const e = await formApi.validate();
|
||||
if (!e.valid) return;
|
||||
const values: Record<string, any> = await formApi.getValues();
|
||||
// 虚拟字段 theme_color → 完整主题对象;未选时不覆盖已有主题
|
||||
const theme = buildThemeByColor(values.theme_color || '');
|
||||
if (theme) {
|
||||
values.theme = theme;
|
||||
}
|
||||
delete values.theme_color;
|
||||
modalApi.setState({ confirmLoading: true });
|
||||
try {
|
||||
const api = isUpdate.value ? updateWxWorkbenchEntry : createWxWorkbenchEntry;
|
||||
await api(values);
|
||||
message.success('保存成功');
|
||||
gridApi.value?.query?.();
|
||||
modalApi.close();
|
||||
} finally {
|
||||
modalApi.setState({ confirmLoading: false });
|
||||
}
|
||||
},
|
||||
onOpenChange(isOpen: boolean) {
|
||||
gridApi.value = isOpen ? modalApi.getData()?.gridApi : null;
|
||||
if (!isOpen) {
|
||||
formApi.resetForm();
|
||||
return;
|
||||
}
|
||||
const { values, update } = modalApi.getData<Record<string, any>>() || {};
|
||||
isUpdate.value = !!update;
|
||||
// 编辑时锁定 code:小程序端按编码映射行为(角标/表单预填/弹窗),改码会导致行为失效
|
||||
formApi.updateSchema([
|
||||
{
|
||||
fieldName: 'code',
|
||||
componentProps: { disabled: !!update },
|
||||
},
|
||||
]);
|
||||
if (values && update) {
|
||||
formApi.setValues({
|
||||
...values,
|
||||
theme_color: parseThemeColor(values.theme),
|
||||
});
|
||||
} else {
|
||||
formApi.resetForm();
|
||||
formApi.setValues({ sort: 0, status: 1, icon: 'grid-fill', description: '', path: '' });
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<Modal :title="`${isUpdate ? '编辑' : '新增'}功能入口`" class="w-[560px]">
|
||||
<Form />
|
||||
</Modal>
|
||||
</template>
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* 小程序工作台功能入口字典:常量定义
|
||||
* 主题预设与小程序端 xk-service-card 现用配色一一对应,避免后台随手填出不协调的颜色
|
||||
*/
|
||||
|
||||
/** 启用状态选项 */
|
||||
export const STATUS_OPTIONS = [
|
||||
{ label: '启用', value: 1 },
|
||||
{ label: '禁用', value: 0 },
|
||||
];
|
||||
|
||||
/** 主题预设:value 为图标主色,bg 为图标区渐变底(与小程序端现有配色一致) */
|
||||
export const THEME_PRESETS = [
|
||||
{ label: '橙金', value: '#F97316', bg: 'linear-gradient(135deg, #FFF7ED, #FFEDD5)' },
|
||||
{ label: '科技蓝', value: '#3B82F6', bg: 'linear-gradient(135deg, #EFF6FF, #DBEAFE)' },
|
||||
{ label: '优雅紫', value: '#A855F7', bg: 'linear-gradient(135deg, #FAF5FF, #F3E8FF)' },
|
||||
{ label: '安全绿', value: '#22C55E', bg: 'linear-gradient(135deg, #F0FDF4, #DCFCE7)' },
|
||||
{ label: '青色', value: '#06B6D4', bg: 'linear-gradient(135deg, #ECFEFF, #CFFAFE)' },
|
||||
{ label: '琥珀', value: '#D97706', bg: 'linear-gradient(135deg, #FFFBEB, #FEF3C7)' },
|
||||
{ label: '警戒红', value: '#EF4444', bg: 'linear-gradient(135deg, #FEE2E2, #FECACA)' },
|
||||
{ label: '翡翠绿', value: '#10B981', bg: 'linear-gradient(135deg, #ECFDF5, #D1FAE5)' },
|
||||
{ label: '天空蓝', value: '#0EA5E9', bg: 'linear-gradient(135deg, #F0F9FF, #E0F2FE)' },
|
||||
{ label: '中性灰', value: '#6B7280', bg: 'linear-gradient(135deg, #F3F4F6, #E5E7EB)' },
|
||||
];
|
||||
|
||||
/** 主题下拉选项(label + value) */
|
||||
export const THEME_OPTIONS = THEME_PRESETS.map((item) => ({
|
||||
label: item.label,
|
||||
value: item.value,
|
||||
}));
|
||||
|
||||
/**
|
||||
* 按主色反查完整主题对象(提交时组装 {bg,color} 存库)
|
||||
*/
|
||||
export function buildThemeByColor(color: string) {
|
||||
const preset = THEME_PRESETS.find((item) => item.value === color);
|
||||
if (!preset) return null;
|
||||
return { bg: preset.bg, color: preset.value };
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析存库的 theme(可能是 JSON 字符串或对象),返回主色用于表单回显
|
||||
*/
|
||||
export function parseThemeColor(theme: any): string {
|
||||
let parsed = theme;
|
||||
if (typeof theme === 'string' && theme) {
|
||||
try {
|
||||
parsed = JSON.parse(theme);
|
||||
} catch {
|
||||
parsed = null;
|
||||
}
|
||||
}
|
||||
return parsed?.color || '';
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import type { VbenFormProps } from '#/adapter/form';
|
||||
|
||||
import { STATUS_OPTIONS, THEME_OPTIONS } from './constants';
|
||||
|
||||
/**
|
||||
* 小程序工作台功能入口新增/编辑表单
|
||||
* theme_color 是表单虚拟字段:选主色,提交时由 modal 组装成 {bg,color} JSON 存 theme 列
|
||||
*/
|
||||
export const modalFormProps: VbenFormProps = {
|
||||
wrapperClass: 'grid-cols-12',
|
||||
commonConfig: {
|
||||
formItemClass: 'col-span-12',
|
||||
componentProps: { class: 'w-full' },
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: [
|
||||
{
|
||||
component: 'VbenInput',
|
||||
fieldName: 'id',
|
||||
label: 'ID',
|
||||
dependencies: { show: false, triggerFields: ['id'] },
|
||||
},
|
||||
{
|
||||
component: 'VbenInput',
|
||||
fieldName: 'code',
|
||||
label: '入口编码',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
placeholder: 'snake_case 全局唯一,按端加前缀:clinic_admin_ / platform_ / sp_ / cs_',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'VbenInput',
|
||||
fieldName: 'name',
|
||||
label: '入口名称',
|
||||
rules: 'required',
|
||||
componentProps: { placeholder: '卡片标题,如:提现管理' },
|
||||
},
|
||||
{
|
||||
component: 'VbenInput',
|
||||
fieldName: 'description',
|
||||
label: '入口描述',
|
||||
componentProps: { placeholder: '卡片副标题,如:提现流水查询' },
|
||||
defaultValue: '',
|
||||
},
|
||||
{
|
||||
component: 'VbenInput',
|
||||
fieldName: 'group_name',
|
||||
label: '分组标题',
|
||||
rules: 'required',
|
||||
componentProps: { placeholder: '首页分区块标题,如:财务管理' },
|
||||
},
|
||||
{
|
||||
component: 'VbenInput',
|
||||
fieldName: 'icon',
|
||||
label: '图标',
|
||||
componentProps: { placeholder: 'uView 图标名,如 red-packet-fill' },
|
||||
defaultValue: 'grid-fill',
|
||||
},
|
||||
{
|
||||
component: 'VbenSelect',
|
||||
fieldName: 'theme_color',
|
||||
label: '主题配色',
|
||||
formItemClass: 'col-span-6',
|
||||
componentProps: { options: THEME_OPTIONS, allowClear: true },
|
||||
},
|
||||
{
|
||||
component: 'InputNumber',
|
||||
fieldName: 'sort',
|
||||
label: '排序',
|
||||
formItemClass: 'col-span-6',
|
||||
componentProps: { min: 0 },
|
||||
defaultValue: 0,
|
||||
},
|
||||
{
|
||||
component: 'VbenInput',
|
||||
fieldName: 'path',
|
||||
label: '跳转路径',
|
||||
componentProps: {
|
||||
placeholder: '小程序页面路径;留空表示前端按编码特殊处理(如传方弹窗)',
|
||||
},
|
||||
defaultValue: '',
|
||||
},
|
||||
{
|
||||
component: 'VbenSelect',
|
||||
fieldName: 'status',
|
||||
label: '状态',
|
||||
componentProps: { options: STATUS_OPTIONS },
|
||||
defaultValue: 1,
|
||||
},
|
||||
],
|
||||
showDefaultActions: false,
|
||||
};
|
||||
200
apps/web-antd/src/views/system/wx-workbench-entry/index.vue
Normal file
200
apps/web-antd/src/views/system/wx-workbench-entry/index.vue
Normal file
@@ -0,0 +1,200 @@
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* 小程序工作台功能入口字典:维护四个业务角色(诊所管理员/平台管理员/业务员/诊所推广员)
|
||||
* 工作台宫格入口的展示信息(编码/名称/描述/图标/主题/路径/分组),
|
||||
* 角色可见性在「角色管理 → 小程序入口」抽屉里按角色勾选分配
|
||||
*/
|
||||
import type { VxeGridListeners } from '#/adapter/vxe-table';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { TableAction } from '#/components/table-action';
|
||||
|
||||
import { deleteWxWorkbenchEntry, getWxWorkbenchEntryList } from './api';
|
||||
import FormModal from './components/modal.vue';
|
||||
import { STATUS_OPTIONS } from './config/constants';
|
||||
|
||||
defineOptions({ name: 'WxWorkbenchEntry' });
|
||||
|
||||
const hasTopTableDropDownActions = ref(false);
|
||||
|
||||
const formOptions = {
|
||||
schema: [
|
||||
{
|
||||
component: 'VbenInput',
|
||||
fieldName: 'code',
|
||||
label: '编码',
|
||||
componentProps: { placeholder: '如 clinic_admin_withdrawal' },
|
||||
},
|
||||
{
|
||||
component: 'VbenInput',
|
||||
fieldName: 'name',
|
||||
label: '名称',
|
||||
componentProps: { placeholder: '入口名称' },
|
||||
},
|
||||
{
|
||||
component: 'VbenInput',
|
||||
fieldName: 'group_name',
|
||||
label: '分组',
|
||||
componentProps: { placeholder: '如 财务管理' },
|
||||
},
|
||||
{
|
||||
component: 'VbenSelect',
|
||||
fieldName: 'status',
|
||||
label: '状态',
|
||||
componentProps: { options: STATUS_OPTIONS, allowClear: true },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const gridOptions = {
|
||||
checkboxConfig: { highlight: true, labelField: '' },
|
||||
columns: [
|
||||
{ type: 'checkbox', width: 60 },
|
||||
{ field: 'id', title: 'ID', width: 70 },
|
||||
{ field: 'code', title: '编码', minWidth: 200 },
|
||||
{ field: 'name', title: '名称', minWidth: 120 },
|
||||
{ field: 'description', title: '描述', minWidth: 160 },
|
||||
{ field: 'group_name', title: '分组', width: 110 },
|
||||
{ field: 'icon', title: '图标', width: 140 },
|
||||
{ field: 'path', title: '跳转路径', minWidth: 240 },
|
||||
{ field: 'sort', title: '排序', width: 70 },
|
||||
{ field: 'status', title: '状态', width: 80, slots: { default: 'status' } },
|
||||
{ field: 'created_at', title: '创建时间', width: 170 },
|
||||
{ title: '操作', slots: { default: 'action' }, width: 130, fixed: 'right' },
|
||||
],
|
||||
keepSource: true,
|
||||
pagerConfig: {},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }: any, formValues: any) => {
|
||||
return await getWxWorkbenchEntryList({
|
||||
page: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
height: 'auto',
|
||||
toolbarConfig: {
|
||||
search: true,
|
||||
refresh: true,
|
||||
slots: { buttons: 'toolbar-actions' },
|
||||
},
|
||||
};
|
||||
|
||||
const gridEvents: VxeGridListeners<any> = {
|
||||
checkboxChange() {
|
||||
hasTopTableDropDownActions.value =
|
||||
gridApi.grid.getCheckboxRecords().length > 0;
|
||||
},
|
||||
checkboxAll() {
|
||||
hasTopTableDropDownActions.value =
|
||||
gridApi.grid.getCheckboxRecords().length > 0;
|
||||
},
|
||||
};
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions,
|
||||
gridOptions: gridOptions as any,
|
||||
gridEvents,
|
||||
});
|
||||
|
||||
const [FormModalComp, formModalApi] = useVbenModal({
|
||||
connectedComponent: FormModal,
|
||||
});
|
||||
|
||||
const showModal = (data: any = {}, isUpdate = false) => {
|
||||
formModalApi.setData({
|
||||
values: data,
|
||||
update: isUpdate,
|
||||
gridApi,
|
||||
});
|
||||
formModalApi.open();
|
||||
};
|
||||
|
||||
const handleDelete = (row: any) => {
|
||||
deleteWxWorkbenchEntry({ ids: [row.id] }).then(() => {
|
||||
message.success('删除成功');
|
||||
gridApi.query();
|
||||
});
|
||||
};
|
||||
|
||||
const handleBatchDelete = () => {
|
||||
const rows = gridApi.grid.getCheckboxRecords();
|
||||
if (!rows.length) return;
|
||||
deleteWxWorkbenchEntry({ ids: rows.map((r: any) => r.id) }).then(() => {
|
||||
message.success('删除成功');
|
||||
gridApi.query();
|
||||
});
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<FormModalComp />
|
||||
<Grid>
|
||||
<template #toolbar-actions>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '新增',
|
||||
type: 'primary',
|
||||
onClick: () => showModal({}, false),
|
||||
},
|
||||
]"
|
||||
:drop-down-actions="
|
||||
hasTopTableDropDownActions
|
||||
? [
|
||||
{
|
||||
label: '批量删除',
|
||||
popConfirm: {
|
||||
title: '确认删除选中入口?删除后各角色将不再展示',
|
||||
confirm: handleBatchDelete,
|
||||
},
|
||||
},
|
||||
]
|
||||
: []
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
<template #status="{ row }">
|
||||
<span :class="row.status === 1 ? 'entry-status-on' : 'entry-status-off'">
|
||||
{{ row.status === 1 ? '启用' : '禁用' }}
|
||||
</span>
|
||||
</template>
|
||||
<template #action="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '编辑',
|
||||
onClick: () => showModal(row, true),
|
||||
},
|
||||
{
|
||||
label: '删除',
|
||||
popConfirm: {
|
||||
title: '确认删除该入口?删除后各角色将不再展示',
|
||||
confirm: () => handleDelete(row),
|
||||
},
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
<style scoped>
|
||||
/* 状态文案:主题变量着色,暗色自动适配 */
|
||||
.entry-status-on {
|
||||
color: hsl(var(--success, 142 71% 45%));
|
||||
}
|
||||
|
||||
.entry-status-off {
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user