优化布局
This commit is contained in:
parent
3dcf7bbd3e
commit
0788ddfb8a
26
README.md
26
README.md
@ -1,6 +1,6 @@
|
||||
# Frontend
|
||||
|
||||
`Frontend/` 是 AppPlatform 的前端工作区,采用 `pnpm workspace + Turbo` 的 monorepo 组织方式。当前目录主要承载主业务应用、文档站、一组共享包和工程脚本。
|
||||
`Frontend/` 是 AppPlatform 的前端工作区,采用 `pnpm workspace + Turbo` 的 monorepo 组织方式。当前目录主要承载主业务应用、一组共享包和工程脚本。
|
||||
|
||||
项目整体基于 Vue 3 技术栈,并延续 `Vue Vben Admin v5` 的分层方式做业务化扩展。
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
- Vue Router
|
||||
- Turbo
|
||||
- pnpm workspace
|
||||
- VitePress
|
||||
|
||||
- Ant Design Vue / antdv-next
|
||||
|
||||
## 2. 工作区结构
|
||||
@ -21,7 +21,6 @@
|
||||
```text
|
||||
Frontend/
|
||||
├── apps/ 主业务应用(数据平台)
|
||||
├── docs/ VitePress 文档站
|
||||
├── packages/ 业务与框架共享包
|
||||
├── internal/ 内部工程配置与构建基础设施
|
||||
├── scripts/ CLI 与部署脚本
|
||||
@ -54,15 +53,9 @@ Frontend/
|
||||
- `adapter`:表单、组件等适配层
|
||||
- `locales`:国际化资源
|
||||
|
||||
### 3.2 docs
|
||||
|
||||
文档站应用,包名为 `@vben/docs`,基于 VitePress。
|
||||
|
||||
- 配置入口在 `docs/.vitepress/config/index.mts`
|
||||
- 同时提供中文与英文站点
|
||||
- 首页内容位于 `docs/src/index.md` 和 `docs/src/en/index.md`
|
||||
|
||||
### 3.3 packages
|
||||
### 3.2 packages
|
||||
|
||||
共享包分为几层:
|
||||
|
||||
@ -80,7 +73,7 @@ Frontend/
|
||||
- `packages/constants|icons|locales|preferences|stores|styles|types|utils`
|
||||
- 面向应用的共享常量、样式、类型、工具与状态能力
|
||||
|
||||
### 3.4 internal
|
||||
### 3.3 internal
|
||||
|
||||
内部工程基础设施,不直接承载业务页面:
|
||||
|
||||
@ -90,7 +83,7 @@ Frontend/
|
||||
- `internal/tailwind-config`:Tailwind 配置
|
||||
- `internal/node-utils`:Node 侧工具能力
|
||||
|
||||
### 3.5 scripts
|
||||
### 3.4 scripts
|
||||
|
||||
工程脚本目录:
|
||||
|
||||
@ -127,9 +120,6 @@ pnpm dev
|
||||
|
||||
# 启动主业务应用
|
||||
pnpm dev:antdv-next
|
||||
|
||||
# 启动文档站
|
||||
pnpm dev:docs
|
||||
```
|
||||
|
||||
### 4.3 构建命令
|
||||
@ -141,9 +131,6 @@ pnpm build
|
||||
# 构建主应用
|
||||
pnpm build:antdv-next
|
||||
|
||||
# 构建文档站
|
||||
pnpm build:docs
|
||||
|
||||
# 本地构建 Docker 镜像
|
||||
pnpm build:docker
|
||||
```
|
||||
@ -200,8 +187,7 @@ pnpm test:unit
|
||||
4. `apps/package.json`
|
||||
5. `apps/src/main.ts`
|
||||
6. `apps/src/bootstrap.ts`
|
||||
7. `docs/.vitepress/config/index.mts`
|
||||
8. `internal/vite-config/src/config/application.ts`
|
||||
7. `internal/vite-config/src/config/application.ts`
|
||||
|
||||
## 8. 补充说明
|
||||
|
||||
|
||||
@ -78,23 +78,6 @@ export async function offPropsActivityRewardGroup(
|
||||
return requestClient.get(`/props/activity/reward/group/off/shelf/${id}/${offShelf}`);
|
||||
}
|
||||
|
||||
export async function pagePropsActivityRuleConfig(params: Record<string, any>) {
|
||||
return requestClient.get<LegacyPageResult<Record<string, any>>>(
|
||||
'/props/activity/rule/config/page',
|
||||
{
|
||||
params,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export async function savePropsActivityRuleConfig(data: Record<string, any>) {
|
||||
return requestClient.post('/props/activity/rule/config/save-or-update', data);
|
||||
}
|
||||
|
||||
export async function delPropsActivityRuleConfig(id: number | string) {
|
||||
return requestClient.get(`/props/activity/rule/config/del/${id}`);
|
||||
}
|
||||
|
||||
export async function getGroupActivityList() {
|
||||
return requestClient.get<Array<Record<string, any>>>(
|
||||
'/props/activity/reward/group/get/activity/list',
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { useAntdDesignTokens } from '@vben/hooks';
|
||||
import { preferences, usePreferences } from '@vben/preferences';
|
||||
import { preferences } from '@vben/preferences';
|
||||
|
||||
import { App, ConfigProvider, theme } from 'antdv-next';
|
||||
|
||||
@ -10,13 +10,10 @@ import { antdLocale } from '#/locales';
|
||||
|
||||
defineOptions({ name: 'App' });
|
||||
|
||||
const { isDark } = usePreferences();
|
||||
const { tokens } = useAntdDesignTokens();
|
||||
|
||||
const tokenTheme = computed(() => {
|
||||
const algorithm = isDark.value
|
||||
? [theme.darkAlgorithm]
|
||||
: [theme.defaultAlgorithm];
|
||||
const algorithm = [theme.defaultAlgorithm];
|
||||
|
||||
// antd 紧凑模式算法
|
||||
if (preferences.app.compact) {
|
||||
|
||||
BIN
apps/src/assets/auth/yumi-logo.png
Normal file
BIN
apps/src/assets/auth/yumi-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 421 KiB |
@ -1,25 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { AuthPageLayout } from '@vben/layouts';
|
||||
import { preferences } from '@vben/preferences';
|
||||
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const appName = computed(() => $t(preferences.app.name));
|
||||
const logo = computed(() => preferences.logo.source);
|
||||
const logoDark = computed(() => preferences.logo.sourceDark);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AuthPageLayout
|
||||
:app-name="appName"
|
||||
:logo="logo"
|
||||
:logo-dark="logoDark"
|
||||
:page-description="$t('authentication.pageDesc')"
|
||||
:page-title="$t('authentication.pageTitle')"
|
||||
>
|
||||
<!-- 自定义工具栏 -->
|
||||
<!-- <template #toolbar></template> -->
|
||||
</AuthPageLayout>
|
||||
:copyright="false"
|
||||
:toolbar="false"
|
||||
minimal
|
||||
/>
|
||||
</template>
|
||||
|
||||
@ -1,17 +1,10 @@
|
||||
<script lang="ts" setup>
|
||||
import type { NotificationItem } from '@vben/layouts';
|
||||
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { computed, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { AuthenticationLoginExpiredModal } from '@vben/common-ui';
|
||||
import { useWatermark } from '@vben/hooks';
|
||||
import {
|
||||
BasicLayout,
|
||||
LockScreen,
|
||||
Notification,
|
||||
UserDropdown,
|
||||
} from '@vben/layouts';
|
||||
import { BasicLayout, LockScreen, UserDropdown } from '@vben/layouts';
|
||||
import { preferences } from '@vben/preferences';
|
||||
import { useAccessStore, useUserStore } from '@vben/stores';
|
||||
|
||||
@ -21,67 +14,11 @@ import DevApiSwitch from '#/components/dev-api-switch.vue';
|
||||
import LoginForm from '#/views/_core/authentication/login.vue';
|
||||
|
||||
const isDev = import.meta.env.DEV;
|
||||
const notifications = ref<NotificationItem[]>([
|
||||
{
|
||||
id: 1,
|
||||
avatar: 'https://avatar.vercel.sh/vercel.svg?text=VB',
|
||||
date: '3小时前',
|
||||
isRead: true,
|
||||
message: '描述信息描述信息描述信息',
|
||||
title: '收到了 14 份新周报',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
avatar: 'https://avatar.vercel.sh/1',
|
||||
date: '刚刚',
|
||||
isRead: false,
|
||||
message: '描述信息描述信息描述信息',
|
||||
title: '朱偏右 回复了你',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
avatar: 'https://avatar.vercel.sh/1',
|
||||
date: '2024-01-01',
|
||||
isRead: false,
|
||||
message: '描述信息描述信息描述信息',
|
||||
title: '曲丽丽 评论了你',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
avatar: 'https://avatar.vercel.sh/satori',
|
||||
date: '1天前',
|
||||
isRead: false,
|
||||
message: '描述信息描述信息描述信息',
|
||||
title: '代办提醒',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
avatar: 'https://avatar.vercel.sh/satori',
|
||||
date: '1天前',
|
||||
isRead: false,
|
||||
message: '描述信息描述信息描述信息',
|
||||
title: '跳转Workspace示例',
|
||||
link: '/workspace',
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
avatar: 'https://avatar.vercel.sh/satori',
|
||||
date: '1天前',
|
||||
isRead: false,
|
||||
message: '描述信息描述信息描述信息',
|
||||
title: '跳转外部链接示例',
|
||||
link: 'https://doc.vben.pro',
|
||||
},
|
||||
]);
|
||||
|
||||
const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
const authStore = useAuthStore();
|
||||
const accessStore = useAccessStore();
|
||||
const { destroyWatermark, updateWatermark } = useWatermark();
|
||||
const showDot = computed(() =>
|
||||
notifications.value.some((item) => !item.isRead),
|
||||
);
|
||||
|
||||
const menus = computed(() => [
|
||||
{
|
||||
@ -100,25 +37,6 @@ const avatar = computed(() => {
|
||||
async function handleLogout() {
|
||||
await authStore.logout(false);
|
||||
}
|
||||
|
||||
function handleNoticeClear() {
|
||||
notifications.value = [];
|
||||
}
|
||||
|
||||
function markRead(id: number | string) {
|
||||
const item = notifications.value.find((item) => item.id === id);
|
||||
if (item) {
|
||||
item.isRead = true;
|
||||
}
|
||||
}
|
||||
|
||||
function remove(id: number | string) {
|
||||
notifications.value = notifications.value.filter((item) => item.id !== id);
|
||||
}
|
||||
|
||||
function handleMakeAll() {
|
||||
notifications.value.forEach((item) => (item.isRead = true));
|
||||
}
|
||||
watch(
|
||||
() => ({
|
||||
enable: preferences.app.watermark,
|
||||
@ -156,16 +74,6 @@ watch(
|
||||
@logout="handleLogout"
|
||||
/>
|
||||
</template>
|
||||
<template #notification>
|
||||
<Notification
|
||||
:dot="showDot"
|
||||
:notifications="notifications"
|
||||
@clear="handleNoticeClear"
|
||||
@read="(item) => item.id && markRead(item.id)"
|
||||
@remove="(item) => item.id && remove(item.id)"
|
||||
@make-all="handleMakeAll"
|
||||
/>
|
||||
</template>
|
||||
<template #extra>
|
||||
<AuthenticationLoginExpiredModal
|
||||
v-model:open="accessStore.loginExpired"
|
||||
|
||||
@ -808,7 +808,6 @@
|
||||
"回合数(只查下注局": "Number of rounds (only check betting rounds)",
|
||||
"活动结束后不可再查看排行榜": "After the event, you can no longer view the rankings",
|
||||
"活动结束后不可再次编辑": "It cannot be edited again after the event is over.",
|
||||
"活动道具规则配置": "Event props rule configuration",
|
||||
"活动类型": "Activity type",
|
||||
"活动名称": "Activity name",
|
||||
"活动礼物": "event gifts",
|
||||
|
||||
@ -805,7 +805,6 @@
|
||||
"会员": "会员",
|
||||
"会员名": "会员名",
|
||||
"活动": "活动",
|
||||
"活动道具规则配置": "活动道具规则配置",
|
||||
"活动奖励": "活动奖励",
|
||||
"活动结束后不可再查看排行榜": "活动结束后不可再查看排行榜",
|
||||
"活动结束后不可再查看排行榜,请通过奖品记录查看获奖快照": "活动结束后不可再查看排行榜,请通过奖品记录查看获奖快照",
|
||||
|
||||
@ -11,7 +11,6 @@ const AuthPageLayout = () => import('#/layouts/auth.vue');
|
||||
const fallbackNotFoundRoute: RouteRecordRaw = {
|
||||
component: () => import('#/views/_core/fallback/not-found.vue'),
|
||||
meta: {
|
||||
hideInBreadcrumb: true,
|
||||
hideInMenu: true,
|
||||
hideInTab: true,
|
||||
title: '404',
|
||||
@ -30,7 +29,6 @@ const coreRoutes: RouteRecordRaw[] = [
|
||||
{
|
||||
component: BasicLayout,
|
||||
meta: {
|
||||
hideInBreadcrumb: true,
|
||||
title: 'Root',
|
||||
},
|
||||
name: 'Root',
|
||||
|
||||
@ -8,7 +8,6 @@ const routes: RouteRecordRaw[] = [
|
||||
name: 'LegacyCommonRoot',
|
||||
component: BasicLayout,
|
||||
meta: {
|
||||
hideInBreadcrumb: true,
|
||||
hideInMenu: true,
|
||||
hideInTab: true,
|
||||
title: 'Common',
|
||||
@ -20,7 +19,6 @@ const routes: RouteRecordRaw[] = [
|
||||
name: 'LegacyUserDetails',
|
||||
component: () => import('#/views/legacy/common/user-details.vue'),
|
||||
meta: {
|
||||
hideInBreadcrumb: true,
|
||||
hideInMenu: true,
|
||||
keepAlive: true,
|
||||
title: '用户详情',
|
||||
|
||||
@ -8,7 +8,6 @@ const routes: RouteRecordRaw[] = [
|
||||
name: 'LegacyLogin',
|
||||
redirect: LOGIN_PATH,
|
||||
meta: {
|
||||
hideInBreadcrumb: true,
|
||||
hideInMenu: true,
|
||||
hideInTab: true,
|
||||
ignoreAccess: true,
|
||||
@ -20,7 +19,6 @@ const routes: RouteRecordRaw[] = [
|
||||
name: 'LegacyForbidden',
|
||||
component: () => import('#/views/_core/fallback/forbidden.vue'),
|
||||
meta: {
|
||||
hideInBreadcrumb: true,
|
||||
hideInMenu: true,
|
||||
hideInTab: true,
|
||||
ignoreAccess: true,
|
||||
@ -32,7 +30,6 @@ const routes: RouteRecordRaw[] = [
|
||||
name: 'LegacyNotFound',
|
||||
component: () => import('#/views/_core/fallback/not-found.vue'),
|
||||
meta: {
|
||||
hideInBreadcrumb: true,
|
||||
hideInMenu: true,
|
||||
hideInTab: true,
|
||||
ignoreAccess: true,
|
||||
|
||||
@ -28,12 +28,6 @@ const routes: RouteRecordRaw[] = [
|
||||
component: () => import('#/views/props/source-group.vue'),
|
||||
meta: { title: '道具资源组配置' },
|
||||
},
|
||||
{
|
||||
name: 'PropsActivityRuleConfig',
|
||||
path: 'props-activity-rule-config',
|
||||
component: () => import('#/views/props/activity-rule-config.vue'),
|
||||
meta: { title: '活动道具规则配置' },
|
||||
},
|
||||
{
|
||||
name: 'PropsStore',
|
||||
path: 'props_store',
|
||||
|
||||
@ -5,6 +5,7 @@ import { computed } from 'vue';
|
||||
|
||||
import { AuthenticationLogin, z } from '@vben/common-ui';
|
||||
|
||||
import yumiLogo from '#/assets/auth/yumi-logo.png';
|
||||
import { useAuthStore } from '#/store';
|
||||
|
||||
defineOptions({ name: 'Login' });
|
||||
@ -16,6 +17,9 @@ const formSchema = computed((): VbenFormSchema[] => {
|
||||
{
|
||||
component: 'VbenInput',
|
||||
componentProps: {
|
||||
autocomplete: 'username',
|
||||
autofocus: true,
|
||||
class: 'h-11',
|
||||
placeholder: '请输入账号',
|
||||
},
|
||||
fieldName: 'username',
|
||||
@ -25,6 +29,8 @@ const formSchema = computed((): VbenFormSchema[] => {
|
||||
{
|
||||
component: 'VbenInputPassword',
|
||||
componentProps: {
|
||||
autocomplete: 'current-password',
|
||||
class: 'h-11',
|
||||
placeholder: '请输入密码',
|
||||
},
|
||||
fieldName: 'password',
|
||||
@ -42,11 +48,23 @@ const formSchema = computed((): VbenFormSchema[] => {
|
||||
:show-code-login="false"
|
||||
:show-forget-password="false"
|
||||
:show-qrcode-login="false"
|
||||
:show-remember-me="false"
|
||||
:show-register="false"
|
||||
:show-third-party-login="false"
|
||||
sub-title="用于处理审核、运营、配置和数据管理任务。"
|
||||
submit-button-text="进入系统"
|
||||
title="内部运营后台"
|
||||
submit-button-text="登录"
|
||||
@submit="authStore.authLogin"
|
||||
/>
|
||||
>
|
||||
<template #title>
|
||||
<img
|
||||
:src="yumiLogo"
|
||||
alt="YUMI"
|
||||
class="mx-auto mb-2 size-[72px] rounded-full border border-amber-200/80 object-cover shadow-[0_0_18px_rgba(20,184,166,0.32),0_0_0_4px_rgba(245,196,81,0.1)]"
|
||||
/>
|
||||
<h1
|
||||
class="mb-5 text-center text-[28px] font-medium tracking-[0.08em] text-[#2d3538]"
|
||||
>
|
||||
YUMI
|
||||
</h1>
|
||||
</template>
|
||||
</AuthenticationLogin>
|
||||
</template>
|
||||
|
||||
@ -1,307 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
computed,
|
||||
reactive,
|
||||
ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { useAccessStore } from '@vben/stores';
|
||||
|
||||
import {
|
||||
delPropsActivityRuleConfig,
|
||||
pagePropsActivityRuleConfig,
|
||||
savePropsActivityRuleConfig,
|
||||
} from '#/api/legacy/props';
|
||||
import InlineFilterField from '#/views/_shared/inline-filter-field.vue';
|
||||
import InlineFilterToolbar from '#/views/_shared/inline-filter-toolbar.vue';
|
||||
import { getAllowedSysOrigins } from '#/views/system/shared';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Input,
|
||||
Modal,
|
||||
Pagination,
|
||||
Row,
|
||||
Select,
|
||||
Space,
|
||||
message
|
||||
} from 'antdv-next';
|
||||
|
||||
import RewardRow from './components/reward-row.vue';
|
||||
import ActivityResourceGroupSelectDrawer from './components/activity-resource-group-select-drawer.vue';
|
||||
import ActivityRuleDrawer from './components/activity-rule-drawer.vue';
|
||||
import { PROP_ACTIVITY_TYPES } from './shared';
|
||||
|
||||
defineOptions({ name: 'PropsActivityRuleConfig' });
|
||||
|
||||
const accessStore = useAccessStore();
|
||||
const sysOriginOptions = computed(() => {
|
||||
const options = getAllowedSysOrigins(accessStore.accessCodes || []);
|
||||
return options.length > 0 ? options : getAllowedSysOrigins([]);
|
||||
});
|
||||
|
||||
const loading = ref(false);
|
||||
const total = ref(0);
|
||||
const list = ref<Array<Record<string, any>>>([]);
|
||||
const formOpen = ref(false);
|
||||
const resourceDrawerOpen = ref(false);
|
||||
const activeRow = ref<Record<string, any> | null>(null);
|
||||
|
||||
const query = reactive({
|
||||
activityType: 'STAR',
|
||||
cursor: 1,
|
||||
id: '',
|
||||
limit: 20,
|
||||
resourceGroupId: '',
|
||||
ruleDescription: '',
|
||||
sysOrigin: sysOriginOptions.value[0]?.value ?? 'LIKEI',
|
||||
});
|
||||
|
||||
async function loadData(reset = false) {
|
||||
if (reset) {
|
||||
query.cursor = 1;
|
||||
}
|
||||
loading.value = true;
|
||||
try {
|
||||
const result = await pagePropsActivityRuleConfig({ ...query });
|
||||
list.value = [...(result.records || [])].sort(
|
||||
(left, right) => Number(left.sort || 0) - Number(right.sort || 0),
|
||||
);
|
||||
total.value = result.total || 0;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handlePageChange(page: number, pageSize: number) {
|
||||
query.cursor = page;
|
||||
query.limit = pageSize;
|
||||
void loadData();
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
activeRow.value = null;
|
||||
formOpen.value = true;
|
||||
}
|
||||
|
||||
function openEdit(record: Record<string, any>) {
|
||||
activeRow.value = { ...record };
|
||||
formOpen.value = true;
|
||||
}
|
||||
|
||||
function openResourceDrawer(record: Record<string, any>) {
|
||||
activeRow.value = { ...record };
|
||||
resourceDrawerOpen.value = true;
|
||||
}
|
||||
|
||||
function formatJson(value: string) {
|
||||
if (!value) {
|
||||
return '{}';
|
||||
}
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(value), null, 2);
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(record: Record<string, any>) {
|
||||
Modal.confirm({
|
||||
async onOk() {
|
||||
await delPropsActivityRuleConfig(record.id);
|
||||
message.success('删除成功');
|
||||
await loadData();
|
||||
},
|
||||
title: '确认删除吗?',
|
||||
});
|
||||
}
|
||||
|
||||
async function handleResourceSelect(row: Record<string, any>) {
|
||||
if (!activeRow.value) {
|
||||
return;
|
||||
}
|
||||
await savePropsActivityRuleConfig({
|
||||
...activeRow.value,
|
||||
resourceGroupId: row.id,
|
||||
});
|
||||
message.success('资源组已关联');
|
||||
resourceDrawerOpen.value = false;
|
||||
await loadData();
|
||||
}
|
||||
|
||||
loadData(true);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page title="活动道具规则配置">
|
||||
<Card>
|
||||
<InlineFilterToolbar class="toolbar">
|
||||
<InlineFilterField label="系统">
|
||||
<SysOriginSelect
|
||||
v-model:value="query.sysOrigin"
|
||||
style="width: 140px"
|
||||
@change="loadData(true)"
|
||||
|
||||
:options="sysOriginOptions"
|
||||
></SysOriginSelect>
|
||||
</InlineFilterField>
|
||||
<InlineFilterField label="活动类型">
|
||||
<Select option-label-prop="label"
|
||||
v-model:value="query.activityType"
|
||||
allow-clear
|
||||
style="width: 180px"
|
||||
@change="loadData(true)"
|
||||
|
||||
:options="PROP_ACTIVITY_TYPES.map((item) => ({ label: `${item.name}`, value: item.value as any }))"
|
||||
/>
|
||||
</InlineFilterField>
|
||||
<InlineFilterField label="ID">
|
||||
<Input v-model:value="query.id" allow-clear style="width: 160px" />
|
||||
</InlineFilterField>
|
||||
<InlineFilterField label="规则描述">
|
||||
<Input
|
||||
v-model:value="query.ruleDescription"
|
||||
allow-clear
|
||||
style="width: 200px"
|
||||
/>
|
||||
</InlineFilterField>
|
||||
<InlineFilterField label="资源组ID" :label-width="88">
|
||||
<Input
|
||||
v-model:value="query.resourceGroupId"
|
||||
allow-clear
|
||||
style="width: 180px"
|
||||
/>
|
||||
</InlineFilterField>
|
||||
<Button :loading="loading" type="primary" @click="loadData(true)">
|
||||
搜索
|
||||
</Button>
|
||||
<Button @click="openCreate">新增</Button>
|
||||
</InlineFilterToolbar>
|
||||
|
||||
<Row :gutter="[16, 16]">
|
||||
<Col
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
:lg="8"
|
||||
:md="12"
|
||||
:sm="24"
|
||||
:xs="24"
|
||||
>
|
||||
<Card :title="item.activityTypeName || item.activityType">
|
||||
<template #extra>
|
||||
<Space size="small">
|
||||
<Button size="small" type="link" @click="openEdit(item)">
|
||||
编辑规则
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
@click="openResourceDrawer(item)"
|
||||
>
|
||||
关联资源
|
||||
</Button>
|
||||
<Button danger size="small" type="link" @click="handleDelete(item)">
|
||||
删除
|
||||
</Button>
|
||||
</Space>
|
||||
</template>
|
||||
|
||||
<div class="rule-meta">
|
||||
<div>ID:{{ item.id }}</div>
|
||||
<div>排序:{{ item.sort }}</div>
|
||||
<div class="rule-meta__full">描述:{{ item.ruleDescription || '-' }}</div>
|
||||
<div class="rule-meta__full">资源组:{{ item.resourceGroupId || '-' }}</div>
|
||||
</div>
|
||||
|
||||
<div class="rule-section">
|
||||
<div class="rule-section__title">规则</div>
|
||||
<pre class="rule-json">{{ formatJson(item.jsonData) }}</pre>
|
||||
</div>
|
||||
|
||||
<div class="rule-section">
|
||||
<div class="rule-section__title">
|
||||
资源信息({{ item.resourceGroupId || '-' }})
|
||||
</div>
|
||||
<RewardRow :list="item.activityRewards" />
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<div class="pager">
|
||||
<Pagination
|
||||
:current="query.cursor"
|
||||
:page-size="query.limit"
|
||||
:total="total"
|
||||
show-size-changer
|
||||
@change="handlePageChange"
|
||||
@showSizeChange="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<ActivityRuleDrawer
|
||||
:open="formOpen"
|
||||
:record="activeRow"
|
||||
:sys-origin-options="sysOriginOptions"
|
||||
@close="formOpen = false"
|
||||
@success="loadData()"
|
||||
/>
|
||||
|
||||
<ActivityResourceGroupSelectDrawer
|
||||
:open="resourceDrawerOpen"
|
||||
:sys-origin="activeRow?.sysOrigin || query.sysOrigin"
|
||||
@close="resourceDrawerOpen = false"
|
||||
@select="handleResourceSelect"
|
||||
/>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.pager {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.rule-json {
|
||||
background: rgb(248 250 252);
|
||||
border: 1px solid rgb(226 232 240);
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
margin: 0;
|
||||
max-height: 180px;
|
||||
overflow: auto;
|
||||
padding: 12px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.rule-meta {
|
||||
color: rgb(71 85 105);
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.rule-meta__full {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.rule-section {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.rule-section__title {
|
||||
color: rgb(15 23 42);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
@ -1,568 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref, watch } from 'vue';
|
||||
|
||||
import {
|
||||
OSS_FILE_BUCKETS,
|
||||
getAccessImgUrl,
|
||||
simpleUploadFile,
|
||||
} from '#/api/legacy/oss';
|
||||
import { listFirstChargeRewards } from '#/api/legacy/product';
|
||||
import { savePropsActivityRuleConfig } from '#/api/legacy/props';
|
||||
import { flowGameMatchConfig } from '#/api/legacy/system';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Drawer,
|
||||
Form,
|
||||
FormItem,
|
||||
Image,
|
||||
Input,
|
||||
Select,
|
||||
Space,
|
||||
Switch,
|
||||
message,
|
||||
} from 'antdv-next';
|
||||
|
||||
import {
|
||||
PROP_ACTIVITY_TYPE_HELP,
|
||||
PROP_ACTIVITY_TYPES,
|
||||
createActivityRuleDraft,
|
||||
} from '../shared';
|
||||
|
||||
const props = defineProps<{
|
||||
open: boolean;
|
||||
record: null | Record<string, any>;
|
||||
sysOriginOptions: Array<{ label: string; value: string }>;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: [];
|
||||
success: [];
|
||||
}>();
|
||||
|
||||
const saving = ref(false);
|
||||
const productsLoading = ref(false);
|
||||
const gamesLoading = ref(false);
|
||||
const smallIconUploading = ref(false);
|
||||
const mediumIconUploading = ref(false);
|
||||
const sourceUploading = ref(false);
|
||||
|
||||
const products = ref<Array<Record<string, any>>>([]);
|
||||
const games = ref<Array<Record<string, any>>>([]);
|
||||
|
||||
const smallIconInputRef = ref<HTMLInputElement>();
|
||||
const mediumIconInputRef = ref<HTMLInputElement>();
|
||||
const sourceInputRef = ref<HTMLInputElement>();
|
||||
|
||||
const form = reactive<Record<string, any>>({
|
||||
activityType: '',
|
||||
id: '',
|
||||
jsonData: '',
|
||||
ruleDescription: '',
|
||||
sort: '',
|
||||
sysOrigin: '',
|
||||
});
|
||||
|
||||
const ruleState = ref<any>(createActivityRuleDraft());
|
||||
|
||||
const isUpdate = computed(() => Boolean(form.id));
|
||||
const isCrystal = computed(() => form.activityType === 'CRYSTAL');
|
||||
const isFirstChargeReward = computed(() => form.activityType === 'FIRST_CHARGE_REWARD');
|
||||
const isLuckyBox = computed(() => form.activityType === 'LUCKY_BOX');
|
||||
const isQuantityMark = computed(() => form.activityType === 'SVIP_REWARD');
|
||||
const isWeeklyGameTask = computed(() => form.activityType === 'WEEKLY_GAME_TASKS');
|
||||
const isGameFruit = computed(() =>
|
||||
['GAME_FRUIT_BOX_REWARD_WIN', 'GAME_FRUIT_BOX_REWARD_TIMES'].includes(
|
||||
form.activityType,
|
||||
),
|
||||
);
|
||||
const luckyBoxRules = computed<Array<Record<string, any>>>(() =>
|
||||
Array.isArray(ruleState.value) ? ruleState.value : [],
|
||||
);
|
||||
const tips = computed(() => PROP_ACTIVITY_TYPE_HELP[form.activityType] || {});
|
||||
const title = computed(() => (isUpdate.value ? '修改规则' : '新增规则'));
|
||||
const activityTypeOptions = PROP_ACTIVITY_TYPES.map((item) => ({
|
||||
label: item.name,
|
||||
value: item.value as any,
|
||||
}));
|
||||
const productOptions = computed(() =>
|
||||
products.value.map((item) => ({
|
||||
label: `${item.unitPrice || '-'} / ${item.productId} / ${item.description || '-'}`,
|
||||
value: item.productId as any,
|
||||
})),
|
||||
);
|
||||
const crystalLevelOptions = [1, 2, 3, 4, 5].map((level) => ({
|
||||
label: `${level}`,
|
||||
value: level as any,
|
||||
}));
|
||||
const gameOptions = computed(() =>
|
||||
games.value.map((item) => ({
|
||||
label: item.name || '-',
|
||||
value: item.id as any,
|
||||
})),
|
||||
);
|
||||
|
||||
function resetForm() {
|
||||
form.activityType = '';
|
||||
form.id = '';
|
||||
form.jsonData = '';
|
||||
form.ruleDescription = '';
|
||||
form.sort = '';
|
||||
form.sysOrigin = props.sysOriginOptions[0]?.value ?? 'LIKEI';
|
||||
ruleState.value = createActivityRuleDraft();
|
||||
}
|
||||
|
||||
function syncRuleDraft(value: string) {
|
||||
ruleState.value = createActivityRuleDraft(value);
|
||||
}
|
||||
|
||||
async function ensureProducts() {
|
||||
if (!isFirstChargeReward.value || !form.sysOrigin || products.value.length > 0) {
|
||||
return;
|
||||
}
|
||||
productsLoading.value = true;
|
||||
try {
|
||||
products.value = await listFirstChargeRewards(form.sysOrigin);
|
||||
} finally {
|
||||
productsLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureGames() {
|
||||
if (!isWeeklyGameTask.value || !form.sysOrigin || games.value.length > 0) {
|
||||
return;
|
||||
}
|
||||
gamesLoading.value = true;
|
||||
try {
|
||||
games.value = await flowGameMatchConfig({ limit: 50, sysOrigin: form.sysOrigin });
|
||||
} finally {
|
||||
gamesLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleActivityTypeChange(value: string) {
|
||||
syncRuleDraft(value);
|
||||
if (value === 'FIRST_CHARGE_REWARD') {
|
||||
await ensureProducts();
|
||||
}
|
||||
if (value === 'WEEKLY_GAME_TASKS') {
|
||||
await ensureGames();
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.open,
|
||||
(open) => {
|
||||
if (!open) {
|
||||
return;
|
||||
}
|
||||
const record = props.record;
|
||||
products.value = [];
|
||||
games.value = [];
|
||||
if (!record) {
|
||||
resetForm();
|
||||
return;
|
||||
}
|
||||
form.activityType = record.activityType ?? '';
|
||||
form.id = record.id ?? '';
|
||||
form.jsonData = record.jsonData ?? '';
|
||||
form.ruleDescription = record.ruleDescription ?? '';
|
||||
form.sort = record.sort ?? '';
|
||||
form.sysOrigin = record.sysOrigin || props.sysOriginOptions[0]?.value || 'LIKEI';
|
||||
try {
|
||||
ruleState.value = form.jsonData
|
||||
? JSON.parse(form.jsonData)
|
||||
: createActivityRuleDraft(form.activityType);
|
||||
} catch {
|
||||
ruleState.value = createActivityRuleDraft(form.activityType);
|
||||
}
|
||||
if (form.activityType === 'FIRST_CHARGE_REWARD') {
|
||||
void ensureProducts();
|
||||
}
|
||||
if (form.activityType === 'WEEKLY_GAME_TASKS') {
|
||||
void ensureGames();
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
function validateRule() {
|
||||
if (!form.sysOrigin) {
|
||||
message.warning('请选择系统');
|
||||
return false;
|
||||
}
|
||||
if (!form.activityType) {
|
||||
message.warning('请选择类型');
|
||||
return false;
|
||||
}
|
||||
if (!String(form.ruleDescription || '').trim()) {
|
||||
message.warning('请输入规则描述');
|
||||
return false;
|
||||
}
|
||||
if (!isCrystal.value && (form.sort === '' || Number.isNaN(Number(form.sort)))) {
|
||||
message.warning('请输入排序');
|
||||
return false;
|
||||
}
|
||||
if (isQuantityMark.value) {
|
||||
if (!String(ruleState.value.mark || '').trim()) {
|
||||
message.warning('请输入标识');
|
||||
return false;
|
||||
}
|
||||
if (ruleState.value.quantity === '' || Number.isNaN(Number(ruleState.value.quantity))) {
|
||||
message.warning('请输入数量');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (isFirstChargeReward.value) {
|
||||
if (!ruleState.value.productId) {
|
||||
message.warning('请选择内购产品');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (isLuckyBox.value) {
|
||||
const valid = Array.isArray(ruleState.value)
|
||||
&& ruleState.value.every(
|
||||
(item) =>
|
||||
item.quantity !== '' &&
|
||||
!Number.isNaN(Number(item.quantity)) &&
|
||||
item.opportunityNumber !== '' &&
|
||||
!Number.isNaN(Number(item.opportunityNumber)),
|
||||
);
|
||||
if (!valid) {
|
||||
message.warning('请完整填写 LuckyBox 规则');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (isCrystal.value) {
|
||||
const { level, mediumIcon, milestone, smallIcon, sourceUrl } = ruleState.value || {};
|
||||
if (!level || milestone === '' || !smallIcon || !mediumIcon || !sourceUrl) {
|
||||
message.warning('请完整填写爆水晶规则');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (isWeeklyGameTask.value) {
|
||||
if (!ruleState.value.gameConfId || ruleState.value.target === '') {
|
||||
message.warning('请完整填写游戏任务规则');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (isGameFruit.value) {
|
||||
if (ruleState.value.quantity === '' || Number.isNaN(Number(ruleState.value.quantity))) {
|
||||
message.warning('请输入数量');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (ruleState.value.quantity === '' || ruleState.value.quantity === undefined) {
|
||||
message.warning('请输入数量');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function uploadAsset(
|
||||
event: Event,
|
||||
key: 'mediumIcon' | 'smallIcon' | 'sourceUrl',
|
||||
loadingRef: typeof smallIconUploading,
|
||||
imageBucket = true,
|
||||
) {
|
||||
const target = event.target as HTMLInputElement;
|
||||
const file = target.files?.[0];
|
||||
target.value = '';
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
loadingRef.value = true;
|
||||
try {
|
||||
const result = await simpleUploadFile(
|
||||
file,
|
||||
imageBucket ? OSS_FILE_BUCKETS.svgaCover : OSS_FILE_BUCKETS.svgasource,
|
||||
);
|
||||
ruleState.value[key] = getAccessImgUrl(result.name);
|
||||
message.success('上传成功');
|
||||
} finally {
|
||||
loadingRef.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleSmallIconChange(event: Event) {
|
||||
void uploadAsset(event, 'smallIcon', smallIconUploading);
|
||||
}
|
||||
|
||||
function handleMediumIconChange(event: Event) {
|
||||
void uploadAsset(event, 'mediumIcon', mediumIconUploading);
|
||||
}
|
||||
|
||||
function handleSourceAssetChange(event: Event) {
|
||||
void uploadAsset(event, 'sourceUrl', sourceUploading, false);
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!validateRule()) {
|
||||
return;
|
||||
}
|
||||
saving.value = true;
|
||||
try {
|
||||
const payload = {
|
||||
activityType: form.activityType,
|
||||
id: form.id || undefined,
|
||||
jsonData: JSON.stringify(ruleState.value),
|
||||
ruleDescription: form.ruleDescription.trim(),
|
||||
sort: isCrystal.value ? Number(ruleState.value.level) : Number(form.sort),
|
||||
sysOrigin: form.sysOrigin,
|
||||
};
|
||||
await savePropsActivityRuleConfig(payload);
|
||||
message.success('保存成功');
|
||||
emit('success');
|
||||
emit('close');
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Drawer
|
||||
:open="open"
|
||||
:title="title"
|
||||
width="760"
|
||||
@close="emit('close')"
|
||||
>
|
||||
<Form layout="vertical">
|
||||
<FormItem label="系统">
|
||||
<SysOriginSelect
|
||||
v-model:value="form.sysOrigin"
|
||||
:disabled="isUpdate"
|
||||
|
||||
:options="sysOriginOptions"
|
||||
></SysOriginSelect>
|
||||
</FormItem>
|
||||
<FormItem label="类型">
|
||||
<Select option-label-prop="label"
|
||||
v-model:value="form.activityType"
|
||||
:disabled="isUpdate"
|
||||
:options="activityTypeOptions"
|
||||
@change="handleActivityTypeChange"
|
||||
/>
|
||||
</FormItem>
|
||||
|
||||
<template v-if="form.activityType">
|
||||
<FormItem v-if="isQuantityMark" :label="tips.mark || '标识'">
|
||||
<Input v-model:value="ruleState.mark" />
|
||||
</FormItem>
|
||||
|
||||
<template v-if="isFirstChargeReward">
|
||||
<FormItem label="内购产品">
|
||||
<Select option-label-prop="label"
|
||||
v-model:value="ruleState.productId"
|
||||
:loading="productsLoading"
|
||||
:options="productOptions"
|
||||
option-filter-prop="label"
|
||||
show-search
|
||||
/>
|
||||
</FormItem>
|
||||
</template>
|
||||
|
||||
<template v-else-if="isLuckyBox">
|
||||
<div class="rule-array">
|
||||
<div
|
||||
v-for="(item, index) in luckyBoxRules"
|
||||
:key="item.id || index"
|
||||
class="rule-array__item"
|
||||
>
|
||||
<div class="rule-array__title">第 {{ index + 1 }} 档</div>
|
||||
<FormItem label="消耗糖果">
|
||||
<Input v-model:value="item.quantity" />
|
||||
</FormItem>
|
||||
<FormItem label="抽奖次数">
|
||||
<Input v-model:value="item.opportunityNumber" />
|
||||
</FormItem>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="isCrystal">
|
||||
<FormItem :label="tips.level || '等级'">
|
||||
<Select
|
||||
option-label-prop="label"
|
||||
v-model:value="ruleState.level"
|
||||
:options="crystalLevelOptions"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem :label="tips.milestone || '里程碑'">
|
||||
<Input v-model:value="ruleState.milestone" />
|
||||
</FormItem>
|
||||
<div class="upload-grid">
|
||||
<FormItem :label="tips.smallIcon || '小号图标'">
|
||||
<input
|
||||
ref="smallIconInputRef"
|
||||
accept="image/*"
|
||||
class="hidden-input"
|
||||
type="file"
|
||||
@change="handleSmallIconChange"
|
||||
/>
|
||||
<div class="upload-box">
|
||||
<Image v-if="ruleState.smallIcon" class="upload-box__image" :src="ruleState.smallIcon" />
|
||||
<div v-else class="upload-box__empty">请上传小图标</div>
|
||||
<Button :loading="smallIconUploading" @click="smallIconInputRef?.click()">
|
||||
{{ ruleState.smallIcon ? '重新上传' : '上传图片' }}
|
||||
</Button>
|
||||
</div>
|
||||
</FormItem>
|
||||
<FormItem :label="tips.mediumIcon || '中号图标'">
|
||||
<input
|
||||
ref="mediumIconInputRef"
|
||||
accept="image/*"
|
||||
class="hidden-input"
|
||||
type="file"
|
||||
@change="handleMediumIconChange"
|
||||
/>
|
||||
<div class="upload-box">
|
||||
<Image v-if="ruleState.mediumIcon" class="upload-box__image" :src="ruleState.mediumIcon" />
|
||||
<div v-else class="upload-box__empty">请上传中图标</div>
|
||||
<Button :loading="mediumIconUploading" @click="mediumIconInputRef?.click()">
|
||||
{{ ruleState.mediumIcon ? '重新上传' : '上传图片' }}
|
||||
</Button>
|
||||
</div>
|
||||
</FormItem>
|
||||
</div>
|
||||
<FormItem :label="tips.sourceUrl || '动画资源图'">
|
||||
<input
|
||||
ref="sourceInputRef"
|
||||
accept=".svga,.pag"
|
||||
class="hidden-input"
|
||||
type="file"
|
||||
@change="handleSourceAssetChange"
|
||||
/>
|
||||
<div class="upload-box">
|
||||
<div v-if="ruleState.sourceUrl" class="upload-box__file">
|
||||
{{ ruleState.sourceUrl }}
|
||||
</div>
|
||||
<div v-else class="upload-box__empty">请上传 svga/pag 资源</div>
|
||||
<Button :loading="sourceUploading" @click="sourceInputRef?.click()">
|
||||
{{ ruleState.sourceUrl ? '重新上传' : '上传资源' }}
|
||||
</Button>
|
||||
</div>
|
||||
</FormItem>
|
||||
</template>
|
||||
|
||||
<template v-else-if="isWeeklyGameTask">
|
||||
<FormItem label="游戏">
|
||||
<Select option-label-prop="label"
|
||||
v-model:value="ruleState.gameConfId"
|
||||
:loading="gamesLoading"
|
||||
:options="gameOptions"
|
||||
option-filter-prop="label"
|
||||
show-search
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="目标">
|
||||
<Input v-model:value="ruleState.target" />
|
||||
</FormItem>
|
||||
</template>
|
||||
|
||||
<template v-else-if="isGameFruit">
|
||||
<FormItem label="上架状态">
|
||||
<Switch
|
||||
v-model:checked="ruleState.status"
|
||||
checked-children="上架"
|
||||
un-checked-children="下架"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="数量">
|
||||
<Input v-model:value="ruleState.quantity" />
|
||||
</FormItem>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<FormItem :label="tips.quantity || '数量'">
|
||||
<Input v-model:value="ruleState.quantity" />
|
||||
</FormItem>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<FormItem label="规则描述">
|
||||
<Input
|
||||
v-model:value="form.ruleDescription"
|
||||
:maxlength="200"
|
||||
show-count
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem v-if="!isCrystal" label="排序">
|
||||
<Input v-model:value="form.sort" />
|
||||
</FormItem>
|
||||
</Form>
|
||||
|
||||
<template #footer>
|
||||
<Space>
|
||||
<Button @click="emit('close')">取消</Button>
|
||||
<Button :loading="saving" type="primary" @click="handleSubmit">
|
||||
保存
|
||||
</Button>
|
||||
</Space>
|
||||
</template>
|
||||
</Drawer>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.hidden-input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.rule-array {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.rule-array__item {
|
||||
background: rgb(248 250 252);
|
||||
border: 1px solid rgb(226 232 240);
|
||||
border-radius: 16px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.rule-array__title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.upload-box {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.upload-box__empty,
|
||||
.upload-box__file {
|
||||
align-items: center;
|
||||
background: rgb(248 250 252);
|
||||
border: 1px dashed rgb(148 163 184 / 35%);
|
||||
border-radius: 16px;
|
||||
color: rgb(148 163 184);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
min-height: 120px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.upload-box__file {
|
||||
color: rgb(51 65 85);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.upload-box__image {
|
||||
height: 120px;
|
||||
object-fit: contain;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.upload-grid {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
</style>
|
||||
1390
apps/src/views/props/components/resource-batch-drawer.vue
Normal file
1390
apps/src/views/props/components/resource-batch-drawer.vue
Normal file
File diff suppressed because it is too large
Load Diff
@ -7,8 +7,8 @@ import {
|
||||
FormItem,
|
||||
Image,
|
||||
Input,
|
||||
InputNumber,
|
||||
message,
|
||||
Modal,
|
||||
Select,
|
||||
Switch,
|
||||
} from 'antdv-next';
|
||||
@ -241,6 +241,18 @@ function handleNotSelectInput(event: Event) {
|
||||
void uploadFile(event, 'notSelectUrl', notSelectUploading, true);
|
||||
}
|
||||
|
||||
function clearUpload(targetKey: 'cover' | 'expand' | 'notSelectUrl' | 'sourceUrl') {
|
||||
form[targetKey] = '';
|
||||
}
|
||||
|
||||
function getAssetName(value: unknown) {
|
||||
const text = String(value || '').trim();
|
||||
if (!text) {
|
||||
return '';
|
||||
}
|
||||
return decodeURIComponent(text.split('/').pop() || text);
|
||||
}
|
||||
|
||||
async function uploadFile(
|
||||
event: Event,
|
||||
targetKey: 'cover' | 'expand' | 'notSelectUrl' | 'sourceUrl',
|
||||
@ -299,7 +311,13 @@ function validateForm() {
|
||||
message.warning('请输入编码');
|
||||
return false;
|
||||
}
|
||||
if (isAmountRequired.value && (form.amount === '' || Number.isNaN(Number(form.amount)))) {
|
||||
if (
|
||||
isAmountRequired.value &&
|
||||
(form.amount === '' ||
|
||||
form.amount === null ||
|
||||
form.amount === undefined ||
|
||||
Number.isNaN(Number(form.amount)))
|
||||
) {
|
||||
message.warning('请输入底价');
|
||||
return false;
|
||||
}
|
||||
@ -335,141 +353,259 @@ async function handleSubmit() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal
|
||||
:confirm-loading="saving"
|
||||
:open="open"
|
||||
destroy-on-close
|
||||
:title="title"
|
||||
width="720"
|
||||
@cancel="emit('close')"
|
||||
@ok="handleSubmit"
|
||||
>
|
||||
<Form layout="vertical">
|
||||
<FormItem label="系统">
|
||||
<SysOriginSelect
|
||||
v-model:value="form.sysOrigin"
|
||||
:options="sysOriginOptions"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="类型">
|
||||
<Select
|
||||
option-label-prop="label"
|
||||
v-model:value="form.type"
|
||||
:options="propsTypeOptions"
|
||||
/>
|
||||
</FormItem>
|
||||
<template v-if="form.type === 'BADGE'">
|
||||
<FormItem label="展示形态">
|
||||
<Select
|
||||
v-model:value="form.badgeDisplayType"
|
||||
:options="badgeDisplayTypeOptions"
|
||||
option-label-prop="label"
|
||||
/>
|
||||
</FormItem>
|
||||
</template>
|
||||
<FormItem label="封面">
|
||||
<input
|
||||
ref="coverInputRef"
|
||||
accept="image/*"
|
||||
class="hidden-input"
|
||||
type="file"
|
||||
@change="handleCoverInput"
|
||||
/>
|
||||
<div class="upload-field">
|
||||
<div v-if="form.cover" class="upload-preview">
|
||||
<Image class="upload-preview__image" :src="form.cover" />
|
||||
</div>
|
||||
<div v-else class="upload-empty">请上传封面</div>
|
||||
<Button :loading="coverUploading" @click="openCoverPicker">
|
||||
{{ form.cover ? '重新上传' : '上传封面' }}
|
||||
</Button>
|
||||
</div>
|
||||
</FormItem>
|
||||
<FormItem v-if="form.type === 'BADGE'" label="未选中图(选填)">
|
||||
<input
|
||||
ref="notSelectInputRef"
|
||||
accept="image/*"
|
||||
class="hidden-input"
|
||||
type="file"
|
||||
@change="handleNotSelectInput"
|
||||
/>
|
||||
<div class="upload-field">
|
||||
<div v-if="form.notSelectUrl" class="upload-preview">
|
||||
<Image class="upload-preview__image" :src="form.notSelectUrl" />
|
||||
</div>
|
||||
<div v-else class="upload-empty">未选中图可不上传</div>
|
||||
<Button :loading="notSelectUploading" @click="openNotSelectPicker">
|
||||
{{ form.notSelectUrl ? '重新上传' : '上传未选中图' }}
|
||||
</Button>
|
||||
</div>
|
||||
</FormItem>
|
||||
<FormItem v-if="showSourceUpload" :label="sourceUploadLabel">
|
||||
<input
|
||||
ref="sourceInputRef"
|
||||
:accept="isImageSourceType ? 'image/*' : '.svga,.pag,.mp4'"
|
||||
class="hidden-input"
|
||||
type="file"
|
||||
@change="handleSourceInput"
|
||||
/>
|
||||
<div class="upload-field">
|
||||
<div v-if="form.sourceUrl" class="upload-preview">
|
||||
<Image
|
||||
v-if="isImageSourceType"
|
||||
class="upload-preview__image"
|
||||
:src="form.sourceUrl"
|
||||
<section v-if="open" class="quick-editor">
|
||||
<input
|
||||
ref="coverInputRef"
|
||||
accept="image/*"
|
||||
class="hidden-input"
|
||||
type="file"
|
||||
@change="handleCoverInput"
|
||||
/>
|
||||
<input
|
||||
ref="notSelectInputRef"
|
||||
accept="image/*"
|
||||
class="hidden-input"
|
||||
type="file"
|
||||
@change="handleNotSelectInput"
|
||||
/>
|
||||
<input
|
||||
ref="sourceInputRef"
|
||||
:accept="isImageSourceType ? 'image/*' : '.svga,.pag,.mp4'"
|
||||
class="hidden-input"
|
||||
type="file"
|
||||
@change="handleSourceInput"
|
||||
/>
|
||||
<input
|
||||
ref="expandInputRef"
|
||||
accept="image/*"
|
||||
class="hidden-input"
|
||||
type="file"
|
||||
@change="handleExpandInput"
|
||||
/>
|
||||
|
||||
<div class="quick-editor__bar">
|
||||
<div class="quick-editor__title">{{ title }}</div>
|
||||
<div class="quick-editor__actions">
|
||||
<Button @click="emit('close')">取消</Button>
|
||||
<Button :loading="saving" type="primary" @click="handleSubmit">
|
||||
保存
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Form class="resource-form" layout="vertical">
|
||||
<section class="compact-section">
|
||||
<div class="section-title">基础配置</div>
|
||||
<div class="form-grid">
|
||||
<FormItem label="系统">
|
||||
<SysOriginSelect
|
||||
v-model:value="form.sysOrigin"
|
||||
:options="sysOriginOptions"
|
||||
/>
|
||||
<div v-else class="upload-file">
|
||||
{{ form.sourceUrl }}
|
||||
</FormItem>
|
||||
<FormItem label="类型">
|
||||
<Select
|
||||
v-model:value="form.type"
|
||||
option-label-prop="label"
|
||||
:options="propsTypeOptions"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem v-if="form.type === 'BADGE'" label="展示形态">
|
||||
<Select
|
||||
v-model:value="form.badgeDisplayType"
|
||||
option-label-prop="label"
|
||||
:options="badgeDisplayTypeOptions"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem v-if="isAmountRequired" label="底价">
|
||||
<InputNumber
|
||||
v-model:value="form.amount"
|
||||
:min="0"
|
||||
placeholder="请输入底价"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="Admin Free">
|
||||
<div class="switch-line">
|
||||
<Switch
|
||||
v-model:checked="form.adminFree"
|
||||
checked-children="是"
|
||||
un-checked-children="否"
|
||||
/>
|
||||
</div>
|
||||
</FormItem>
|
||||
<FormItem class="form-grid__wide" label="名称">
|
||||
<Select
|
||||
v-if="form.type === 'NOBLE_VIP'"
|
||||
v-model:value="form.name"
|
||||
option-label-prop="label"
|
||||
:options="nobleVipNameOptions"
|
||||
/>
|
||||
<Input v-else v-model:value="form.name" />
|
||||
</FormItem>
|
||||
<FormItem v-if="!isUpdate" label="编码">
|
||||
<Input v-model:value="form.code" placeholder="添加后不可修改" />
|
||||
</FormItem>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="compact-section">
|
||||
<div class="section-title">资源文件</div>
|
||||
<div class="asset-grid">
|
||||
<div class="asset-card">
|
||||
<div class="asset-preview">
|
||||
<Image
|
||||
v-if="form.cover"
|
||||
class="asset-preview__image"
|
||||
:src="form.cover"
|
||||
/>
|
||||
<div v-else class="asset-empty">未上传</div>
|
||||
</div>
|
||||
<div class="asset-detail">
|
||||
<div class="asset-card__head">
|
||||
<span>封面</span>
|
||||
<span
|
||||
class="asset-status"
|
||||
:class="{ 'asset-status--ready': form.cover }"
|
||||
>
|
||||
{{ form.cover ? '已上传' : '必填' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="asset-meta">{{ getAssetName(form.cover) || '-' }}</div>
|
||||
</div>
|
||||
<div class="asset-actions">
|
||||
<Button :loading="coverUploading" @click="openCoverPicker">
|
||||
{{ form.cover ? '替换' : '上传' }}
|
||||
</Button>
|
||||
<Button
|
||||
v-if="form.cover"
|
||||
danger
|
||||
type="text"
|
||||
@click="clearUpload('cover')"
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="upload-empty">请上传资源</div>
|
||||
<Button :loading="sourceUploading" @click="openSourcePicker">
|
||||
{{ form.sourceUrl ? '重新上传' : '上传资源' }}
|
||||
</Button>
|
||||
</div>
|
||||
</FormItem>
|
||||
<FormItem v-if="showExpandUpload" label="Android资源">
|
||||
<input
|
||||
ref="expandInputRef"
|
||||
accept="image/*"
|
||||
class="hidden-input"
|
||||
type="file"
|
||||
@change="handleExpandInput"
|
||||
/>
|
||||
<div class="upload-field">
|
||||
<div v-if="form.expand" class="upload-preview">
|
||||
<Image class="upload-preview__image" :src="form.expand" />
|
||||
|
||||
<div v-if="showSourceUpload" class="asset-card">
|
||||
<div class="asset-preview">
|
||||
<Image
|
||||
v-if="form.sourceUrl && isImageSourceType"
|
||||
class="asset-preview__image"
|
||||
:src="form.sourceUrl"
|
||||
/>
|
||||
<div v-else-if="form.sourceUrl" class="asset-file">
|
||||
{{ getAssetName(form.sourceUrl) }}
|
||||
</div>
|
||||
<div v-else class="asset-empty">未上传</div>
|
||||
</div>
|
||||
<div class="asset-detail">
|
||||
<div class="asset-card__head">
|
||||
<span>{{ sourceUploadLabel }}</span>
|
||||
<span
|
||||
class="asset-status"
|
||||
:class="{ 'asset-status--ready': form.sourceUrl }"
|
||||
>
|
||||
{{ form.sourceUrl ? '已上传' : isSourceRequired ? '必填' : '选填' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="asset-meta">
|
||||
{{ getAssetName(form.sourceUrl) || '-' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="asset-actions">
|
||||
<Button :loading="sourceUploading" @click="openSourcePicker">
|
||||
{{ form.sourceUrl ? '替换' : '上传' }}
|
||||
</Button>
|
||||
<Button
|
||||
v-if="form.sourceUrl"
|
||||
danger
|
||||
type="text"
|
||||
@click="clearUpload('sourceUrl')"
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="showExpandUpload" class="asset-card">
|
||||
<div class="asset-preview">
|
||||
<Image
|
||||
v-if="form.expand"
|
||||
class="asset-preview__image"
|
||||
:src="form.expand"
|
||||
/>
|
||||
<div v-else class="asset-empty">未上传</div>
|
||||
</div>
|
||||
<div class="asset-detail">
|
||||
<div class="asset-card__head">
|
||||
<span>Android资源</span>
|
||||
<span
|
||||
class="asset-status"
|
||||
:class="{ 'asset-status--ready': form.expand }"
|
||||
>
|
||||
{{ form.expand ? '已上传' : '必填' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="asset-meta">{{ getAssetName(form.expand) || '-' }}</div>
|
||||
</div>
|
||||
<div class="asset-actions">
|
||||
<Button :loading="expandUploading" @click="openExpandPicker">
|
||||
{{ form.expand ? '替换' : '上传' }}
|
||||
</Button>
|
||||
<Button
|
||||
v-if="form.expand"
|
||||
danger
|
||||
type="text"
|
||||
@click="clearUpload('expand')"
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="form.type === 'BADGE'" class="asset-card">
|
||||
<div class="asset-preview">
|
||||
<Image
|
||||
v-if="form.notSelectUrl"
|
||||
class="asset-preview__image"
|
||||
:src="form.notSelectUrl"
|
||||
/>
|
||||
<div v-else class="asset-empty">未上传</div>
|
||||
</div>
|
||||
<div class="asset-detail">
|
||||
<div class="asset-card__head">
|
||||
<span>未选中图</span>
|
||||
<span
|
||||
class="asset-status"
|
||||
:class="{ 'asset-status--ready': form.notSelectUrl }"
|
||||
>
|
||||
{{ form.notSelectUrl ? '已上传' : '选填' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="asset-meta">
|
||||
{{ getAssetName(form.notSelectUrl) || '-' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="asset-actions">
|
||||
<Button :loading="notSelectUploading" @click="openNotSelectPicker">
|
||||
{{ form.notSelectUrl ? '替换' : '上传' }}
|
||||
</Button>
|
||||
<Button
|
||||
v-if="form.notSelectUrl"
|
||||
danger
|
||||
type="text"
|
||||
@click="clearUpload('notSelectUrl')"
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="upload-empty">请上传 Android 资源</div>
|
||||
<Button :loading="expandUploading" @click="openExpandPicker">
|
||||
{{ form.expand ? '重新上传' : '上传资源' }}
|
||||
</Button>
|
||||
</div>
|
||||
</FormItem>
|
||||
<FormItem label="名称">
|
||||
<Select
|
||||
v-if="form.type === 'NOBLE_VIP'"
|
||||
option-label-prop="label"
|
||||
v-model:value="form.name"
|
||||
:options="nobleVipNameOptions"
|
||||
/>
|
||||
<Input v-else v-model:value="form.name" />
|
||||
</FormItem>
|
||||
<FormItem v-if="!isUpdate" label="编码">
|
||||
<Input v-model:value="form.code" placeholder="编码添加后不可修改" />
|
||||
</FormItem>
|
||||
<FormItem v-if="isAmountRequired" label="底价">
|
||||
<Input v-model:value="form.amount" />
|
||||
</FormItem>
|
||||
<FormItem label="Admin Free">
|
||||
<Switch
|
||||
v-model:checked="form.adminFree"
|
||||
checked-children="是"
|
||||
un-checked-children="否"
|
||||
/>
|
||||
</FormItem>
|
||||
</section>
|
||||
</Form>
|
||||
</Modal>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@ -477,37 +613,221 @@ async function handleSubmit() {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.upload-field {
|
||||
.quick-editor {
|
||||
background: #fff;
|
||||
border: 1px solid #d8dee8;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 2px rgb(16 24 40 / 4%);
|
||||
margin-bottom: 8px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.quick-editor__bar {
|
||||
align-items: center;
|
||||
border-bottom: 1px solid #eef0f3;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.quick-editor__title {
|
||||
color: #111827;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.quick-editor__actions {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
color: #111827;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.resource-form {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.upload-preview,
|
||||
.upload-empty {
|
||||
.resource-form :deep(.ant-form-item) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.resource-form :deep(.ant-form-item-label) {
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
|
||||
.resource-form :deep(.ant-form-item-label > label) {
|
||||
color: #475467;
|
||||
font-size: 13px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.compact-section {
|
||||
border-bottom: 1px solid #eef0f3;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.compact-section:last-child {
|
||||
border-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
grid-template-columns: repeat(4, minmax(170px, 1fr));
|
||||
}
|
||||
|
||||
.form-grid__wide {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
.switch-line {
|
||||
align-items: center;
|
||||
background: rgb(248 250 252);
|
||||
border: 1px dashed rgb(148 163 184 / 35%);
|
||||
border-radius: 16px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
min-height: 180px;
|
||||
overflow: hidden;
|
||||
gap: 8px;
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
.asset-grid {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
grid-template-columns: repeat(2, minmax(320px, 1fr));
|
||||
}
|
||||
|
||||
.asset-card {
|
||||
align-items: center;
|
||||
background: #fbfdff;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 6px;
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
grid-template-columns: 96px minmax(0, 1fr) auto;
|
||||
min-width: 0;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.upload-empty {
|
||||
color: rgb(148 163 184);
|
||||
.asset-card__head,
|
||||
.asset-actions {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.upload-preview__image {
|
||||
height: 180px;
|
||||
.asset-card__head {
|
||||
color: #344054;
|
||||
gap: 8px;
|
||||
font-weight: 600;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.asset-card__head span:first-child {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.asset-status {
|
||||
background: #f3f4f6;
|
||||
border-radius: 999px;
|
||||
color: #667085;
|
||||
flex: 0 0 auto;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.asset-status--ready {
|
||||
background: #e8f3ff;
|
||||
color: #1677ff;
|
||||
}
|
||||
|
||||
.asset-preview {
|
||||
align-items: center;
|
||||
background: #f8fafc;
|
||||
border: 1px dashed #cbd5e1;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
height: 76px;
|
||||
justify-content: center;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.asset-preview__image {
|
||||
height: 64px;
|
||||
object-fit: contain;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.upload-file {
|
||||
color: rgb(51 65 85);
|
||||
line-height: 1.5;
|
||||
word-break: break-all;
|
||||
.asset-empty {
|
||||
color: #98a2b3;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.asset-file {
|
||||
color: #344054;
|
||||
font-size: 12px;
|
||||
line-height: 1.35;
|
||||
max-width: 100%;
|
||||
overflow-wrap: anywhere;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.asset-meta {
|
||||
color: #667085;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.asset-actions {
|
||||
gap: 6px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
@media (max-width: 1120px) {
|
||||
.form-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.form-grid__wide {
|
||||
grid-column: span 2;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.asset-grid,
|
||||
.form-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.form-grid__wide {
|
||||
grid-column: span 1;
|
||||
}
|
||||
|
||||
.asset-card {
|
||||
grid-template-columns: 80px minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.asset-actions {
|
||||
grid-column: 1 / -1;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -77,10 +77,20 @@ const tokenClassName = computed(() => {
|
||||
align-items: center;
|
||||
background: #fff;
|
||||
border: 1px solid rgb(226 232 240);
|
||||
border-radius: 14px;
|
||||
border-radius: 8px;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
transition:
|
||||
border-color 0.18s ease,
|
||||
box-shadow 0.18s ease,
|
||||
transform 0.18s ease;
|
||||
}
|
||||
|
||||
.reward-icon:hover {
|
||||
border-color: rgb(191 219 254);
|
||||
box-shadow: 0 6px 18px rgb(15 23 42 / 10%);
|
||||
transform: scale(1.06);
|
||||
}
|
||||
|
||||
.reward-icon__image {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -328,40 +328,3 @@ export function formatRewardText(item: Record<string, any>) {
|
||||
const quantity = Number(item.quantity || 0);
|
||||
return `天数:${quantity <= 0 ? '永久' : quantity}`;
|
||||
}
|
||||
|
||||
export function createActivityRuleDraft(type = '') {
|
||||
switch (type) {
|
||||
case 'CRYSTAL': {
|
||||
return {
|
||||
level: '',
|
||||
mediumIcon: '',
|
||||
milestone: '',
|
||||
smallIcon: '',
|
||||
sourceUrl: '',
|
||||
};
|
||||
}
|
||||
case 'FIRST_CHARGE_REWARD': {
|
||||
return { productId: '' };
|
||||
}
|
||||
case 'GAME_FRUIT_BOX_REWARD_TIMES':
|
||||
case 'GAME_FRUIT_BOX_REWARD_WIN': {
|
||||
return { quantity: '', status: true };
|
||||
}
|
||||
case 'LUCKY_BOX': {
|
||||
return [
|
||||
{ id: 1, opportunityNumber: '', quantity: '' },
|
||||
{ id: 2, opportunityNumber: '', quantity: '' },
|
||||
{ id: 3, opportunityNumber: '', quantity: '' },
|
||||
];
|
||||
}
|
||||
case 'SVIP_REWARD': {
|
||||
return { mark: '', quantity: '' };
|
||||
}
|
||||
case 'WEEKLY_GAME_TASKS': {
|
||||
return { gameConfId: '', target: '' };
|
||||
}
|
||||
default: {
|
||||
return { quantity: '' };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,45 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
import PreviewGroup from './preview-group.vue';
|
||||
|
||||
interface Props {
|
||||
files?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), { files: '() => []' });
|
||||
|
||||
const parsedFiles = computed(() => {
|
||||
try {
|
||||
return JSON.parse(decodeURIComponent(props.files ?? ''));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative rounded-xl border border-border shadow-float">
|
||||
<div
|
||||
class="not-prose relative w-full overflow-x-auto rounded-t-lg px-4 py-6"
|
||||
>
|
||||
<div class="flex w-full max-w-[700px] px-2">
|
||||
<ClientOnly>
|
||||
<slot v-if="parsedFiles.length > 0"></slot>
|
||||
<div v-else class="text-sm text-destructive">
|
||||
<span class="rounded-sm bg-destructive p-1 text-foreground">
|
||||
ERROR:
|
||||
</span>
|
||||
The preview directory does not exist. Please check the 'dir'
|
||||
parameter.
|
||||
</div>
|
||||
</ClientOnly>
|
||||
</div>
|
||||
</div>
|
||||
<PreviewGroup v-if="parsedFiles.length > 0" :files="parsedFiles">
|
||||
<template v-for="file in parsedFiles" #[file]>
|
||||
<slot :name="file"></slot>
|
||||
</template>
|
||||
</PreviewGroup>
|
||||
</div>
|
||||
</template>
|
||||
@ -1 +0,0 @@
|
||||
export { default as DemoPreview } from './demo-preview.vue';
|
||||
@ -1,110 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { SetupContext } from 'vue';
|
||||
|
||||
import { computed, ref, useSlots } from 'vue';
|
||||
|
||||
import { VbenTooltip } from '@vben-core/shadcn-ui';
|
||||
|
||||
import { Code } from 'lucide-vue-next';
|
||||
import {
|
||||
TabsContent,
|
||||
TabsIndicator,
|
||||
TabsList,
|
||||
TabsRoot,
|
||||
TabsTrigger,
|
||||
} from 'reka-ui';
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
files?: string[];
|
||||
}>(),
|
||||
{ files: () => [] },
|
||||
);
|
||||
|
||||
const open = ref(false);
|
||||
|
||||
const slots: SetupContext['slots'] = useSlots();
|
||||
|
||||
const tabs = computed(() => {
|
||||
return props.files.map((file) => {
|
||||
return {
|
||||
component: slots[file],
|
||||
label: file,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
const currentTab = ref('index.vue');
|
||||
|
||||
const toggleOpen = () => {
|
||||
open.value = !open.value;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TabsRoot
|
||||
v-model="currentTab"
|
||||
class="overflow-hidden rounded-b-xl border-t border-border bg-background-deep"
|
||||
@update:model-value="open = true"
|
||||
>
|
||||
<div class="flex border-b-2 border-border bg-background pr-2">
|
||||
<div class="flex w-full items-center justify-between text-[13px]">
|
||||
<TabsList class="relative flex">
|
||||
<template v-if="open">
|
||||
<TabsIndicator
|
||||
class="absolute bottom-0 left-0 h-[2px] w-(--reka-tabs-indicator-size) translate-x-(--reka-tabs-indicator-position) rounded-full transition-[width,transform] duration-300"
|
||||
>
|
||||
<div class="size-full bg-(--vp-c-indigo-1)"></div>
|
||||
</TabsIndicator>
|
||||
<TabsTrigger
|
||||
v-for="(tab, index) in tabs"
|
||||
:key="index"
|
||||
:value="tab.label"
|
||||
class="border-box px-4 py-3 text-foreground data-[state=active]:text-(--vp-c-indigo-1)"
|
||||
tabindex="-1"
|
||||
>
|
||||
{{ tab.label }}
|
||||
</TabsTrigger>
|
||||
</template>
|
||||
</TabsList>
|
||||
|
||||
<div
|
||||
:class="{
|
||||
'py-2': !open,
|
||||
}"
|
||||
class="flex items-center"
|
||||
>
|
||||
<VbenTooltip side="top">
|
||||
<template #trigger>
|
||||
<Code
|
||||
class="size-7 cursor-pointer rounded-full p-1.5 hover:bg-accent"
|
||||
@click="toggleOpen"
|
||||
/>
|
||||
</template>
|
||||
{{ open ? 'Collapse code' : 'Expand code' }}
|
||||
</VbenTooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
:class="`${open ? 'h-[unset] max-h-[80vh]' : 'h-0'}`"
|
||||
class="block overflow-y-scroll bg-(--vp-code-block-bg) transition-all duration-300"
|
||||
>
|
||||
<TabsContent
|
||||
v-for="tab in tabs"
|
||||
:key="tab.label"
|
||||
:value="tab.label"
|
||||
as-child
|
||||
class="rounded-xl"
|
||||
>
|
||||
<div class="relative rounded-xl text-foreground">
|
||||
<component :is="tab.component" class="border-0" />
|
||||
</div>
|
||||
</TabsContent>
|
||||
</div>
|
||||
</TabsRoot>
|
||||
</template>
|
||||
@ -1,302 +0,0 @@
|
||||
import type { DefaultTheme } from 'vitepress';
|
||||
|
||||
import { defineConfig } from 'vitepress';
|
||||
|
||||
import { version } from '../../../package.json';
|
||||
|
||||
export const en = defineConfig({
|
||||
description: 'Vben Admin & Enterprise level management system framework',
|
||||
lang: 'en-US',
|
||||
themeConfig: {
|
||||
darkModeSwitchLabel: 'Theme',
|
||||
darkModeSwitchTitle: 'Switch to Dark Mode',
|
||||
docFooter: {
|
||||
next: 'Next Page',
|
||||
prev: 'Previous Page',
|
||||
},
|
||||
editLink: {
|
||||
pattern:
|
||||
'https://github.com/vbenjs/vue-vben-admin/edit/main/docs/src/:path',
|
||||
text: 'Edit this page on GitHub',
|
||||
},
|
||||
footer: {
|
||||
copyright: `Copyright © 2020-${new Date().getFullYear()} Vben`,
|
||||
message: 'Released under the MIT License.',
|
||||
},
|
||||
langMenuLabel: 'Language',
|
||||
lastUpdated: {
|
||||
formatOptions: {
|
||||
dateStyle: 'short',
|
||||
timeStyle: 'medium',
|
||||
},
|
||||
text: 'Last updated on',
|
||||
},
|
||||
lightModeSwitchTitle: 'Switch to Light Mode',
|
||||
nav: nav(),
|
||||
outline: {
|
||||
label: 'Navigate',
|
||||
},
|
||||
returnToTopLabel: 'Back to top',
|
||||
sidebar: {
|
||||
'/en/commercial/': {
|
||||
base: '/en/commercial/',
|
||||
items: sidebarCommercial(),
|
||||
},
|
||||
'/en/components/': {
|
||||
base: '/en/components/',
|
||||
items: sidebarComponents(),
|
||||
},
|
||||
'/en/guide/': { base: '/en/guide/', items: sidebarGuide() },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
function sidebarGuide(): DefaultTheme.SidebarItem[] {
|
||||
return [
|
||||
{
|
||||
collapsed: false,
|
||||
text: 'Introduction',
|
||||
items: [
|
||||
{
|
||||
link: 'introduction/vben',
|
||||
text: 'About Vben Admin',
|
||||
},
|
||||
{
|
||||
link: 'introduction/why',
|
||||
text: 'Why Choose Us?',
|
||||
},
|
||||
{ link: 'introduction/quick-start', text: 'Quick Start' },
|
||||
{ link: 'introduction/thin', text: 'Lite Version' },
|
||||
{
|
||||
base: '/',
|
||||
link: 'components/introduction',
|
||||
text: 'Components',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'Basics',
|
||||
items: [
|
||||
{ link: 'essentials/concept', text: 'Basic Concepts' },
|
||||
{ link: 'essentials/development', text: 'Local Development' },
|
||||
{ link: 'essentials/route', text: 'Routing and Menu' },
|
||||
{ link: 'essentials/settings', text: 'Configuration' },
|
||||
{ link: 'essentials/icons', text: 'Icons' },
|
||||
{ link: 'essentials/styles', text: 'Styles' },
|
||||
{ link: 'essentials/external-module', text: 'External Modules' },
|
||||
{ link: 'essentials/build', text: 'Build and Deployment' },
|
||||
{ link: 'essentials/server', text: 'Server Interaction and Data Mock' },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'Advanced',
|
||||
items: [
|
||||
{ link: 'in-depth/login', text: 'Login' },
|
||||
{ link: 'in-depth/theme', text: 'Theme' },
|
||||
{ link: 'in-depth/access', text: 'Access Control' },
|
||||
{ link: 'in-depth/locale', text: 'Internationalization' },
|
||||
{ link: 'in-depth/features', text: 'Common Features' },
|
||||
{ link: 'in-depth/check-updates', text: 'Check Updates' },
|
||||
{ link: 'in-depth/loading', text: 'Global Loading' },
|
||||
{ link: 'in-depth/ui-framework', text: 'UI Framework Switching' },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'Engineering',
|
||||
items: [
|
||||
{ link: 'project/standard', text: 'Standards' },
|
||||
{ link: 'project/cli', text: 'CLI' },
|
||||
{ link: 'project/dir', text: 'Directory Explanation' },
|
||||
{ link: 'project/test', text: 'Unit Testing' },
|
||||
{ link: 'project/tailwindcss', text: 'Tailwind CSS' },
|
||||
{ link: 'project/changeset', text: 'Changeset' },
|
||||
{ link: 'project/vite', text: 'Vite Config' },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'Others',
|
||||
items: [
|
||||
{ link: 'other/project-update', text: 'Project Update' },
|
||||
{ link: 'other/remove-code', text: 'Remove Code' },
|
||||
{ link: 'other/faq', text: 'FAQ' },
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function sidebarCommercial(): DefaultTheme.SidebarItem[] {
|
||||
return [
|
||||
{
|
||||
link: 'community',
|
||||
text: 'Community',
|
||||
},
|
||||
{
|
||||
link: 'technical-support',
|
||||
text: 'Technical-support',
|
||||
},
|
||||
{
|
||||
link: 'customized',
|
||||
text: 'Customized',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function sidebarComponents(): DefaultTheme.SidebarItem[] {
|
||||
return [
|
||||
{
|
||||
text: 'Components',
|
||||
items: [
|
||||
{
|
||||
link: 'introduction',
|
||||
text: 'Introduction',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
collapsed: false,
|
||||
text: 'Layout UI',
|
||||
items: [
|
||||
{
|
||||
link: 'layout-ui/page',
|
||||
text: 'Page',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
collapsed: false,
|
||||
text: 'Common UI',
|
||||
items: [
|
||||
{
|
||||
link: 'common-ui/vben-api-component',
|
||||
text: 'ApiComponent',
|
||||
},
|
||||
{
|
||||
link: 'common-ui/vben-alert',
|
||||
text: 'Alert',
|
||||
},
|
||||
{
|
||||
link: 'common-ui/vben-modal',
|
||||
text: 'Modal',
|
||||
},
|
||||
{
|
||||
link: 'common-ui/vben-drawer',
|
||||
text: 'Drawer',
|
||||
},
|
||||
{
|
||||
link: 'common-ui/vben-form',
|
||||
text: 'Form',
|
||||
},
|
||||
{
|
||||
link: 'common-ui/vben-vxe-table',
|
||||
text: 'Vxe Table',
|
||||
},
|
||||
{
|
||||
link: 'common-ui/vben-count-to-animator',
|
||||
text: 'CountToAnimator',
|
||||
},
|
||||
{
|
||||
link: 'common-ui/vben-ellipsis-text',
|
||||
text: 'EllipsisText',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function nav(): DefaultTheme.NavItem[] {
|
||||
return [
|
||||
{
|
||||
activeMatch: '^/en/(guide|components)/',
|
||||
text: 'Doc',
|
||||
items: [
|
||||
{
|
||||
activeMatch: '^/en/guide/',
|
||||
link: '/en/guide/introduction/vben',
|
||||
text: 'Guide',
|
||||
},
|
||||
{
|
||||
activeMatch: '^/en/components/',
|
||||
link: '/en/components/introduction',
|
||||
text: 'Components',
|
||||
},
|
||||
{
|
||||
text: 'Historical Versions',
|
||||
items: [
|
||||
{
|
||||
link: 'https://doc.vvbin.cn',
|
||||
text: '2.x Version Documentation',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'Demo',
|
||||
items: [
|
||||
{
|
||||
text: 'Vben Admin',
|
||||
items: [
|
||||
{
|
||||
link: 'https://www.vben.pro',
|
||||
text: 'Demo Version',
|
||||
},
|
||||
{
|
||||
link: 'https://ant.vben.pro',
|
||||
text: 'Ant Design Vue Version',
|
||||
},
|
||||
{
|
||||
link: 'https://naive.vben.pro',
|
||||
text: 'Naive Version',
|
||||
},
|
||||
{
|
||||
link: 'https://ele.vben.pro',
|
||||
text: 'Element Plus Version',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'Others',
|
||||
items: [
|
||||
{
|
||||
link: 'https://vben.vvbin.cn',
|
||||
text: 'Vben Admin 2.x',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: version,
|
||||
items: [
|
||||
{
|
||||
link: 'https://github.com/vbenjs/vue-vben-admin/releases',
|
||||
text: 'Changelog',
|
||||
},
|
||||
{
|
||||
link: 'https://github.com/orgs/vbenjs/projects/5',
|
||||
text: 'Roadmap',
|
||||
},
|
||||
{
|
||||
link: 'https://github.com/vbenjs/vue-vben-admin/blob/main/.github/contributing.md',
|
||||
text: 'Contribution',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
link: '/commercial/technical-support',
|
||||
text: '🦄 Tech Support',
|
||||
},
|
||||
{
|
||||
link: '/sponsor/personal',
|
||||
text: '✨ Sponsor',
|
||||
},
|
||||
{
|
||||
link: '/commercial/community',
|
||||
text: '👨👦👦 Community',
|
||||
},
|
||||
// {
|
||||
// link: '/friend-links/',
|
||||
// text: '🤝 Friend Links',
|
||||
// },
|
||||
];
|
||||
}
|
||||
@ -1,25 +0,0 @@
|
||||
import { withPwa } from '@vite-pwa/vitepress';
|
||||
import { defineConfigWithTheme } from 'vitepress';
|
||||
|
||||
import { en } from './en.mts';
|
||||
import { shared } from './shared.mts';
|
||||
import { zh } from './zh.mts';
|
||||
|
||||
export default withPwa(
|
||||
defineConfigWithTheme({
|
||||
...shared,
|
||||
locales: {
|
||||
en: {
|
||||
label: 'English',
|
||||
lang: 'en',
|
||||
link: '/en/',
|
||||
...en,
|
||||
},
|
||||
root: {
|
||||
label: '简体中文',
|
||||
lang: 'zh-CN',
|
||||
...zh,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
@ -1,142 +0,0 @@
|
||||
import type { MarkdownEnv, MarkdownRenderer } from 'vitepress';
|
||||
|
||||
import crypto from 'node:crypto';
|
||||
import { readdirSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
|
||||
export const rawPathRegexp =
|
||||
/^(.+?(?:\.([\da-z]+))?)(#[\w-]+)?(?: ?{(\d+(?:[,-]\d+)*)? ?(\S+)?})? ?(?:\[(.+)])?$/;
|
||||
|
||||
function rawPathToToken(rawPath: string) {
|
||||
const [
|
||||
filepath = '',
|
||||
extension = '',
|
||||
region = '',
|
||||
lines = '',
|
||||
lang = '',
|
||||
rawTitle = '',
|
||||
] = (rawPathRegexp.exec(rawPath) || []).slice(1);
|
||||
|
||||
const title = rawTitle || filepath.split('/').pop() || '';
|
||||
|
||||
return { extension, filepath, lang, lines, region, title };
|
||||
}
|
||||
|
||||
export const demoPreviewPlugin = (md: MarkdownRenderer) => {
|
||||
md.core.ruler.after('inline', 'demo-preview', (state) => {
|
||||
const insertComponentImport = (importString: string) => {
|
||||
const index = state.tokens.findIndex(
|
||||
(i) => i.type === 'html_block' && i.content.match(/<script setup>/g),
|
||||
);
|
||||
if (index === -1) {
|
||||
const importComponent = new state.Token('html_block', '', 0);
|
||||
importComponent.content = `<script setup>\n${importString}\n</script>\n`;
|
||||
state.tokens.splice(0, 0, importComponent);
|
||||
} else {
|
||||
if (state.tokens[index]) {
|
||||
const content = state.tokens[index].content;
|
||||
state.tokens[index].content = content.replace(
|
||||
'</script>',
|
||||
`${importString}\n</script>`,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
// Define the regular expression to match the desired pattern
|
||||
const regex = /<DemoPreview[^>]*\sdir="([^"]*)"/g;
|
||||
// Iterate through the Markdown content and replace the pattern
|
||||
state.src = state.src.replaceAll(regex, (_match, dir) => {
|
||||
const componentDir = join(process.cwd(), 'src', dir).replaceAll(
|
||||
'\\',
|
||||
'/',
|
||||
);
|
||||
|
||||
let childFiles: string[] = [];
|
||||
let dirExists = true;
|
||||
|
||||
try {
|
||||
childFiles =
|
||||
readdirSync(componentDir, {
|
||||
encoding: 'utf8',
|
||||
recursive: false,
|
||||
withFileTypes: false,
|
||||
}) || [];
|
||||
} catch {
|
||||
dirExists = false;
|
||||
}
|
||||
|
||||
if (!dirExists) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const uniqueWord = generateContentHash(componentDir);
|
||||
|
||||
const ComponentName = `DemoComponent_${uniqueWord}`;
|
||||
insertComponentImport(
|
||||
`import ${ComponentName} from '${componentDir}/index.vue'`,
|
||||
);
|
||||
const { path: _path } = state.env as MarkdownEnv;
|
||||
|
||||
const index = state.tokens.findIndex((i) => i.content.match(regex));
|
||||
|
||||
if (!state.tokens[index]) {
|
||||
return '';
|
||||
}
|
||||
const firstString = 'index.vue';
|
||||
childFiles = childFiles.toSorted((a, b) => {
|
||||
if (a === firstString) return -1;
|
||||
if (b === firstString) return 1;
|
||||
return a.localeCompare(b, 'en', { sensitivity: 'base' });
|
||||
});
|
||||
state.tokens[index].content =
|
||||
`<DemoPreview files="${encodeURIComponent(JSON.stringify(childFiles))}" ><${ComponentName}/>
|
||||
`;
|
||||
|
||||
const _dummyToken = new state.Token('', '', 0);
|
||||
const tokenArray: Array<typeof _dummyToken> = [];
|
||||
childFiles.forEach((filename) => {
|
||||
// const slotName = filename.replace(extname(filename), '');
|
||||
|
||||
const templateStart = new state.Token('html_inline', '', 0);
|
||||
templateStart.content = `<template #${filename}>`;
|
||||
tokenArray.push(templateStart);
|
||||
|
||||
const resolvedPath = join(componentDir, filename);
|
||||
|
||||
const { extension, filepath, lang, lines, title } =
|
||||
rawPathToToken(resolvedPath);
|
||||
// Add code tokens for each line
|
||||
const token = new state.Token('fence', 'code', 0);
|
||||
token.info = `${lang || extension}${lines ? `{${lines}}` : ''}${
|
||||
title ? `[${title}]` : ''
|
||||
}`;
|
||||
|
||||
token.content = `<<< ${filepath}`;
|
||||
(token as any).src = [resolvedPath];
|
||||
tokenArray.push(token);
|
||||
|
||||
const templateEnd = new state.Token('html_inline', '', 0);
|
||||
templateEnd.content = '</template>';
|
||||
tokenArray.push(templateEnd);
|
||||
});
|
||||
const endTag = new state.Token('html_inline', '', 0);
|
||||
endTag.content = '</DemoPreview>';
|
||||
tokenArray.push(endTag);
|
||||
|
||||
state.tokens.splice(index + 1, 0, ...tokenArray);
|
||||
|
||||
// console.log(
|
||||
// state.md.renderer.render(state.tokens, state?.options ?? [], state.env),
|
||||
// );
|
||||
return '';
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
function generateContentHash(input: string, length: number = 10): string {
|
||||
// 使用 SHA-256 生成哈希值
|
||||
const hash = crypto.createHash('sha256').update(input).digest('hex');
|
||||
|
||||
// 将哈希值转换为 Base36 编码,并取指定长度的字符作为结果
|
||||
return Number.parseInt(hash, 16).toString(36).slice(0, length);
|
||||
}
|
||||
@ -1,170 +0,0 @@
|
||||
import type { PwaOptions } from '@vite-pwa/vitepress';
|
||||
import type { HeadConfig } from 'vitepress';
|
||||
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
import {
|
||||
viteArchiverPlugin,
|
||||
viteVxeTableImportsPlugin,
|
||||
} from '@vben/vite-config';
|
||||
|
||||
import {
|
||||
GitChangelog,
|
||||
GitChangelogMarkdownSection,
|
||||
} from '@nolebase/vitepress-plugin-git-changelog/vite';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import { defineConfig, postcssIsolateStyles } from 'vitepress';
|
||||
import {
|
||||
groupIconMdPlugin,
|
||||
groupIconVitePlugin,
|
||||
} from 'vitepress-plugin-group-icons';
|
||||
|
||||
import { demoPreviewPlugin } from './plugins/demo-preview';
|
||||
import { search as zhSearch } from './zh.mts';
|
||||
|
||||
export const shared = defineConfig({
|
||||
appearance: 'dark',
|
||||
head: head(),
|
||||
markdown: {
|
||||
preConfig(md) {
|
||||
md.use(demoPreviewPlugin);
|
||||
md.use(groupIconMdPlugin);
|
||||
},
|
||||
},
|
||||
pwa: pwa(),
|
||||
srcDir: 'src',
|
||||
themeConfig: {
|
||||
i18nRouting: true,
|
||||
logo: 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp',
|
||||
search: {
|
||||
options: {
|
||||
locales: {
|
||||
...zhSearch,
|
||||
},
|
||||
},
|
||||
provider: 'local',
|
||||
},
|
||||
siteTitle: 'Vben Admin',
|
||||
socialLinks: [
|
||||
{ icon: 'github', link: 'https://github.com/vbenjs/vue-vben-admin' },
|
||||
],
|
||||
},
|
||||
title: 'Vben Admin',
|
||||
vite: {
|
||||
build: {
|
||||
chunkSizeWarningLimit: Infinity,
|
||||
minify: 'terser',
|
||||
},
|
||||
css: {
|
||||
postcss: {
|
||||
plugins: [postcssIsolateStyles({ includeFiles: [/vp-doc\.css/] })],
|
||||
},
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
api: 'modern',
|
||||
},
|
||||
},
|
||||
},
|
||||
json: {
|
||||
stringify: true,
|
||||
},
|
||||
plugins: [
|
||||
tailwindcss(),
|
||||
GitChangelog({
|
||||
mapAuthors: [
|
||||
{
|
||||
mapByNameAliases: ['Vben'],
|
||||
name: 'vben',
|
||||
username: 'anncwb',
|
||||
},
|
||||
{
|
||||
name: 'vince',
|
||||
username: 'vince292007',
|
||||
},
|
||||
{
|
||||
name: 'Li Kui',
|
||||
username: 'likui628',
|
||||
},
|
||||
],
|
||||
repoURL: () => 'https://github.com/vbenjs/vue-vben-admin',
|
||||
}),
|
||||
GitChangelogMarkdownSection(),
|
||||
viteArchiverPlugin({ outputDir: '.vitepress' }),
|
||||
groupIconVitePlugin(),
|
||||
await viteVxeTableImportsPlugin(),
|
||||
],
|
||||
server: {
|
||||
fs: {
|
||||
allow: ['../..'],
|
||||
},
|
||||
host: true,
|
||||
port: 6173,
|
||||
},
|
||||
|
||||
ssr: {
|
||||
external: ['@vue/repl'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
function head(): HeadConfig[] {
|
||||
return [
|
||||
['meta', { content: 'Vbenjs Team', name: 'author' }],
|
||||
[
|
||||
'meta',
|
||||
{
|
||||
content: 'vben, vitejs, vite, shacdn-ui, vue',
|
||||
name: 'keywords',
|
||||
},
|
||||
],
|
||||
['link', { href: '/favicon.ico', rel: 'icon', type: 'image/svg+xml' }],
|
||||
[
|
||||
'meta',
|
||||
{
|
||||
content:
|
||||
'width=device-width,initial-scale=1,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no',
|
||||
name: 'viewport',
|
||||
},
|
||||
],
|
||||
['meta', { content: 'vben admin docs', name: 'keywords' }],
|
||||
['link', { href: '/favicon.ico', rel: 'icon' }],
|
||||
// [
|
||||
// 'script',
|
||||
// {
|
||||
// src: 'https://cdn.tailwindcss.com',
|
||||
// },
|
||||
// ],
|
||||
];
|
||||
}
|
||||
|
||||
function pwa(): PwaOptions {
|
||||
return {
|
||||
includeManifestIcons: false,
|
||||
manifest: {
|
||||
description:
|
||||
'Vben Admin is a modern admin dashboard template based on Vue 3. ',
|
||||
icons: [
|
||||
{
|
||||
sizes: '192x192',
|
||||
src: 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/pwa-icon-192.png',
|
||||
type: 'image/png',
|
||||
},
|
||||
{
|
||||
sizes: '512x512',
|
||||
src: 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/pwa-icon-512.png',
|
||||
type: 'image/png',
|
||||
},
|
||||
],
|
||||
id: '/',
|
||||
name: 'Vben Admin Doc',
|
||||
short_name: 'vben_admin_doc',
|
||||
theme_color: '#ffffff',
|
||||
},
|
||||
outDir: resolve(process.cwd(), '.vitepress/dist'),
|
||||
registerType: 'autoUpdate',
|
||||
workbox: {
|
||||
globPatterns: ['**/*.{css,js,html,svg,png,ico,txt,woff2}'],
|
||||
maximumFileSizeToCacheInBytes: 5 * 1024 * 1024,
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -1,358 +0,0 @@
|
||||
import type { DefaultTheme } from 'vitepress';
|
||||
|
||||
import { defineConfig } from 'vitepress';
|
||||
|
||||
import { version } from '../../../package.json';
|
||||
|
||||
export const zh = defineConfig({
|
||||
description: 'Vben Admin & 企业级管理系统框架',
|
||||
lang: 'zh-Hans',
|
||||
themeConfig: {
|
||||
darkModeSwitchLabel: '主题',
|
||||
darkModeSwitchTitle: '切换到深色模式',
|
||||
docFooter: {
|
||||
next: '下一页',
|
||||
prev: '上一页',
|
||||
},
|
||||
editLink: {
|
||||
pattern:
|
||||
'https://github.com/vbenjs/vue-vben-admin/edit/main/docs/src/:path',
|
||||
text: '在 GitHub 上编辑此页面',
|
||||
},
|
||||
footer: {
|
||||
copyright: `Copyright © 2020-${new Date().getFullYear()} Vben`,
|
||||
message: '基于 MIT 许可发布.',
|
||||
},
|
||||
langMenuLabel: '多语言',
|
||||
lastUpdated: {
|
||||
formatOptions: {
|
||||
dateStyle: 'short',
|
||||
timeStyle: 'medium',
|
||||
},
|
||||
text: '最后更新于',
|
||||
},
|
||||
lightModeSwitchTitle: '切换到浅色模式',
|
||||
nav: nav(),
|
||||
|
||||
outline: {
|
||||
label: '页面导航',
|
||||
},
|
||||
returnToTopLabel: '回到顶部',
|
||||
|
||||
sidebar: {
|
||||
'/commercial/': { base: '/commercial/', items: sidebarCommercial() },
|
||||
'/components/': { base: '/components/', items: sidebarComponents() },
|
||||
'/guide/': { base: '/guide/', items: sidebarGuide() },
|
||||
},
|
||||
sidebarMenuLabel: '菜单',
|
||||
},
|
||||
});
|
||||
|
||||
function sidebarGuide(): DefaultTheme.SidebarItem[] {
|
||||
return [
|
||||
{
|
||||
collapsed: false,
|
||||
text: '简介',
|
||||
items: [
|
||||
{
|
||||
link: 'introduction/vben',
|
||||
text: '关于 Vben Admin',
|
||||
},
|
||||
{
|
||||
link: 'introduction/why',
|
||||
text: '为什么选择我们?',
|
||||
},
|
||||
{ link: 'introduction/quick-start', text: '快速开始' },
|
||||
{ link: 'introduction/thin', text: '精简版本' },
|
||||
{
|
||||
base: '/',
|
||||
link: 'components/introduction',
|
||||
text: '组件文档',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '基础',
|
||||
items: [
|
||||
{ link: 'essentials/concept', text: '基础概念' },
|
||||
{ link: 'essentials/development', text: '本地开发' },
|
||||
{ link: 'essentials/route', text: '路由和菜单' },
|
||||
{ link: 'essentials/settings', text: '配置' },
|
||||
{ link: 'essentials/icons', text: '图标' },
|
||||
{ link: 'essentials/styles', text: '样式' },
|
||||
{ link: 'essentials/external-module', text: '外部模块' },
|
||||
{ link: 'essentials/build', text: '构建与部署' },
|
||||
{ link: 'essentials/server', text: '服务端交互与数据Mock' },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '深入',
|
||||
items: [
|
||||
{ link: 'in-depth/login', text: '登录' },
|
||||
// { link: 'in-depth/layout', text: '布局' },
|
||||
{ link: 'in-depth/theme', text: '主题' },
|
||||
{ link: 'in-depth/access', text: '权限' },
|
||||
{ link: 'in-depth/locale', text: '国际化' },
|
||||
{ link: 'in-depth/features', text: '常用功能' },
|
||||
{ link: 'in-depth/check-updates', text: '检查更新' },
|
||||
{ link: 'in-depth/loading', text: '全局loading' },
|
||||
{ link: 'in-depth/ui-framework', text: '组件库切换' },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '工程',
|
||||
items: [
|
||||
{ link: 'project/standard', text: '规范' },
|
||||
{ link: 'project/cli', text: 'CLI' },
|
||||
{ link: 'project/dir', text: '目录说明' },
|
||||
{ link: 'project/test', text: '单元测试' },
|
||||
{ link: 'project/tailwindcss', text: 'Tailwind CSS' },
|
||||
{ link: 'project/changeset', text: 'Changeset' },
|
||||
{ link: 'project/vite', text: 'Vite Config' },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '其他',
|
||||
items: [
|
||||
{ link: 'other/project-update', text: '项目更新' },
|
||||
{ link: 'other/remove-code', text: '移除代码' },
|
||||
{ link: 'other/faq', text: '常见问题' },
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function sidebarCommercial(): DefaultTheme.SidebarItem[] {
|
||||
return [
|
||||
{
|
||||
link: 'community',
|
||||
text: '交流群',
|
||||
},
|
||||
{
|
||||
link: 'technical-support',
|
||||
text: '技术支持',
|
||||
},
|
||||
{
|
||||
link: 'customized',
|
||||
text: '定制开发',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function sidebarComponents(): DefaultTheme.SidebarItem[] {
|
||||
return [
|
||||
{
|
||||
text: '组件',
|
||||
items: [
|
||||
{
|
||||
link: 'introduction',
|
||||
text: '介绍',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
collapsed: false,
|
||||
text: '布局组件',
|
||||
items: [
|
||||
{
|
||||
link: 'layout-ui/page',
|
||||
text: 'Page 页面',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
collapsed: false,
|
||||
text: '通用组件',
|
||||
items: [
|
||||
{
|
||||
link: 'common-ui/vben-api-component',
|
||||
text: 'ApiComponent Api组件包装器',
|
||||
},
|
||||
{
|
||||
link: 'common-ui/vben-alert',
|
||||
text: 'Alert 轻量提示框',
|
||||
},
|
||||
{
|
||||
link: 'common-ui/vben-modal',
|
||||
text: 'Modal 模态框',
|
||||
},
|
||||
{
|
||||
link: 'common-ui/vben-drawer',
|
||||
text: 'Drawer 抽屉',
|
||||
},
|
||||
{
|
||||
link: 'common-ui/vben-form',
|
||||
text: 'Form 表单',
|
||||
},
|
||||
{
|
||||
link: 'common-ui/vben-vxe-table',
|
||||
text: 'Vxe Table 表格',
|
||||
},
|
||||
{
|
||||
link: 'common-ui/vben-count-to-animator',
|
||||
text: 'CountToAnimator 数字动画',
|
||||
},
|
||||
{
|
||||
link: 'common-ui/vben-ellipsis-text',
|
||||
text: 'EllipsisText 省略文本',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function nav(): DefaultTheme.NavItem[] {
|
||||
return [
|
||||
{
|
||||
activeMatch: '^/(guide|components)/',
|
||||
text: '文档',
|
||||
items: [
|
||||
{
|
||||
activeMatch: '^/guide/',
|
||||
link: '/guide/introduction/vben',
|
||||
text: '指南',
|
||||
},
|
||||
{
|
||||
activeMatch: '^/components/',
|
||||
link: '/components/introduction',
|
||||
text: '组件',
|
||||
},
|
||||
{
|
||||
text: '历史版本',
|
||||
items: [
|
||||
{
|
||||
link: 'https://doc.vvbin.cn',
|
||||
text: '2.x版本文档',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '演示',
|
||||
items: [
|
||||
{
|
||||
text: 'Vben Admin',
|
||||
items: [
|
||||
{
|
||||
link: 'https://www.vben.pro',
|
||||
text: '演示版本',
|
||||
},
|
||||
{
|
||||
link: 'https://ant.vben.pro',
|
||||
text: 'Ant Design Vue 版本',
|
||||
},
|
||||
{
|
||||
link: 'https://naive.vben.pro',
|
||||
text: 'Naive 版本',
|
||||
},
|
||||
{
|
||||
link: 'https://ele.vben.pro',
|
||||
text: 'Element Plus版本',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '其他',
|
||||
items: [
|
||||
{
|
||||
link: 'https://vben.vvbin.cn',
|
||||
text: 'Vben Admin 2.x',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: version,
|
||||
items: [
|
||||
{
|
||||
link: 'https://github.com/vbenjs/vue-vben-admin/releases',
|
||||
text: '更新日志',
|
||||
},
|
||||
{
|
||||
link: 'https://github.com/orgs/vbenjs/projects/5',
|
||||
text: '路线图',
|
||||
},
|
||||
{
|
||||
link: 'https://github.com/vbenjs/vue-vben-admin/blob/main/.github/contributing.md',
|
||||
text: '贡献',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
link: '/commercial/technical-support',
|
||||
text: '🦄 技术支持',
|
||||
},
|
||||
{
|
||||
link: '/sponsor/personal',
|
||||
text: '✨ 赞助',
|
||||
},
|
||||
{
|
||||
link: '/commercial/community',
|
||||
text: '👨👦👦 交流群',
|
||||
// items: [
|
||||
// {
|
||||
// link: 'https://qun.qq.com/qqweb/qunpro/share?_wv=3&_wwv=128&appChannel=share&inviteCode=22ySzj7pKiw&businessType=9&from=246610&biz=ka&mainSourceId=share&subSourceId=others&jumpsource=shorturl#/pc',
|
||||
// text: 'QQ频道',
|
||||
// },
|
||||
// {
|
||||
// link: 'https://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=mjZmlhgVzzUxvdxllB6C1vHpX8O8QRL0&authKey=DBdFbBwERmfaKY95JvRWqLCJIRGJAmKyZbrpzZ41EKDMZ5SR6MfbjOBaaNRN73fr&noverify=0&group_code=4286109',
|
||||
// text: 'QQ群',
|
||||
// },
|
||||
// {
|
||||
// link: 'https://discord.gg/VU62jTecad',
|
||||
// text: 'Discord',
|
||||
// },
|
||||
// ],
|
||||
},
|
||||
// {
|
||||
// link: '/friend-links/',
|
||||
// text: '🤝 友情链接',
|
||||
// },
|
||||
];
|
||||
}
|
||||
|
||||
export const search: DefaultTheme.AlgoliaSearchOptions['locales'] = {
|
||||
root: {
|
||||
placeholder: '搜索文档',
|
||||
translations: {
|
||||
button: {
|
||||
buttonAriaLabel: '搜索文档',
|
||||
buttonText: '搜索文档',
|
||||
},
|
||||
modal: {
|
||||
errorScreen: {
|
||||
helpText: '你可能需要检查你的网络连接',
|
||||
titleText: '无法获取结果',
|
||||
},
|
||||
footer: {
|
||||
closeText: '关闭',
|
||||
navigateText: '切换',
|
||||
searchByText: '搜索提供者',
|
||||
selectText: '选择',
|
||||
},
|
||||
noResultsScreen: {
|
||||
noResultsText: '无法找到相关结果',
|
||||
reportMissingResultsLinkText: '点击反馈',
|
||||
reportMissingResultsText: '你认为该查询应该有结果?',
|
||||
suggestedQueryText: '你可以尝试查询',
|
||||
},
|
||||
searchBox: {
|
||||
cancelButtonAriaLabel: '取消',
|
||||
cancelButtonText: '取消',
|
||||
resetButtonAriaLabel: '清除查询条件',
|
||||
resetButtonTitle: '清除查询条件',
|
||||
},
|
||||
startScreen: {
|
||||
favoriteSearchesTitle: '收藏',
|
||||
noRecentSearchesText: '没有搜索历史',
|
||||
recentSearchesTitle: '搜索历史',
|
||||
removeFavoriteSearchButtonTitle: '从收藏中移除',
|
||||
removeRecentSearchButtonTitle: '从搜索历史中移除',
|
||||
saveRecentSearchButtonTitle: '保存至搜索历史',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@ -1,96 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
computed,
|
||||
nextTick,
|
||||
onBeforeUnmount,
|
||||
onMounted,
|
||||
ref,
|
||||
watch,
|
||||
} from 'vue';
|
||||
|
||||
// import { useAntdDesignTokens } from '@vben/hooks';
|
||||
// import { initPreferences } from '@vben/preferences';
|
||||
import { ConfigProvider, theme } from 'ant-design-vue';
|
||||
import mediumZoom from 'medium-zoom';
|
||||
import { useRoute } from 'vitepress';
|
||||
import DefaultTheme from 'vitepress/theme';
|
||||
|
||||
const { Layout } = DefaultTheme;
|
||||
const route = useRoute();
|
||||
// const { tokens } = useAntdDesignTokens();
|
||||
|
||||
const initZoom = () => {
|
||||
// mediumZoom('[data-zoomable]', { background: 'var(--vp-c-bg)' });
|
||||
mediumZoom('.VPContent img', { background: 'var(--vp-c-bg)' });
|
||||
};
|
||||
|
||||
const isDark = ref(true);
|
||||
|
||||
watch(
|
||||
() => route.path,
|
||||
() => nextTick(() => initZoom()),
|
||||
);
|
||||
|
||||
// initPreferences({
|
||||
// namespace: 'docs',
|
||||
// });
|
||||
|
||||
onMounted(() => {
|
||||
initZoom();
|
||||
});
|
||||
|
||||
// 使用该函数
|
||||
const observer = watchDarkModeChange((dark) => {
|
||||
isDark.value = dark;
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
observer?.disconnect();
|
||||
});
|
||||
|
||||
function watchDarkModeChange(callback: (isDark: boolean) => void) {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
const htmlElement = document.documentElement;
|
||||
|
||||
const observer = new MutationObserver(() => {
|
||||
const isDark = htmlElement.classList.contains('dark');
|
||||
callback(isDark);
|
||||
});
|
||||
|
||||
observer.observe(htmlElement, {
|
||||
attributeFilter: ['class'],
|
||||
attributes: true,
|
||||
});
|
||||
|
||||
const initialIsDark = htmlElement.classList.contains('dark');
|
||||
callback(initialIsDark);
|
||||
|
||||
return observer;
|
||||
}
|
||||
|
||||
const tokenTheme = computed(() => {
|
||||
const algorithm = isDark.value
|
||||
? [theme.darkAlgorithm]
|
||||
: [theme.defaultAlgorithm];
|
||||
|
||||
return {
|
||||
algorithm,
|
||||
// token: tokens,
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ConfigProvider :theme="tokenTheme">
|
||||
<Layout />
|
||||
</ConfigProvider>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.medium-zoom-overlay,
|
||||
.medium-zoom-image--opened {
|
||||
z-index: 2147483647;
|
||||
}
|
||||
</style>
|
||||
@ -1,29 +0,0 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<div class="vben-contributors vp-doc">
|
||||
<p>Contributors</p>
|
||||
<a href="https://github.com/vbenjs/vue-vben-admin/graphs/contributors">
|
||||
<img
|
||||
alt="Contributors"
|
||||
src="https://opencollective.com/vbenjs/contributors.svg?button=false"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.vben-contributors {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-top: 60px;
|
||||
|
||||
p {
|
||||
margin-bottom: 50px;
|
||||
font-size: 30px;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,29 +0,0 @@
|
||||
// https://vitepress.dev/guide/custom-theme
|
||||
import type { EnhanceAppContext, Theme } from 'vitepress';
|
||||
|
||||
import { NolebaseGitChangelogPlugin } from '@nolebase/vitepress-plugin-git-changelog/client';
|
||||
import DefaultTheme from 'vitepress/theme';
|
||||
|
||||
import { DemoPreview } from '../components';
|
||||
import SiteLayout from './components/site-layout.vue';
|
||||
import VbenContributors from './components/vben-contributors.vue';
|
||||
import { initHmPlugin } from './plugins/hm';
|
||||
|
||||
import './styles';
|
||||
|
||||
import 'virtual:group-icons.css';
|
||||
import '@nolebase/vitepress-plugin-git-changelog/client/style.css';
|
||||
|
||||
export default {
|
||||
async enhanceApp(ctx: EnhanceAppContext) {
|
||||
const { app } = ctx;
|
||||
app.component('VbenContributors', VbenContributors);
|
||||
app.component('DemoPreview', DemoPreview);
|
||||
app.use(NolebaseGitChangelogPlugin);
|
||||
|
||||
// 百度统计
|
||||
initHmPlugin();
|
||||
},
|
||||
extends: DefaultTheme,
|
||||
Layout: SiteLayout,
|
||||
} satisfies Theme;
|
||||
@ -1,28 +0,0 @@
|
||||
import { inBrowser } from 'vitepress';
|
||||
|
||||
const SITE_ID = '2e443a834727c065877c01d89921545e';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
_hmt: any;
|
||||
}
|
||||
}
|
||||
|
||||
function registerAnalytics() {
|
||||
window._hmt = window._hmt || [];
|
||||
const script = document.createElement('script');
|
||||
script.innerHTML = `var _hmt = _hmt || [];
|
||||
(function() {
|
||||
var hm = document.createElement("script");
|
||||
hm.src = "https://hm.baidu.com/hm.js?${SITE_ID}";
|
||||
var s = document.getElementsByTagName("script")[0];
|
||||
s.parentNode.insertBefore(hm, s);
|
||||
})()`;
|
||||
document.querySelector('head')?.append(script);
|
||||
}
|
||||
|
||||
export function initHmPlugin() {
|
||||
if (inBrowser && import.meta.env.PROD) {
|
||||
registerAnalytics();
|
||||
}
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
html.dark {
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
.dark .VPContent {
|
||||
/* background-color: #14161a; */
|
||||
}
|
||||
|
||||
.form-valid-error p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 顶部导航栏选中项样式 */
|
||||
.VPNavBarMenuLink,
|
||||
.VPNavBarMenuGroup {
|
||||
border-bottom: 1px solid transparent;
|
||||
}
|
||||
|
||||
.VPNavBarMenuLink.active,
|
||||
.VPNavBarMenuGroup.active {
|
||||
border-bottom-color: var(--vp-c-brand-1);
|
||||
}
|
||||
@ -1,4 +0,0 @@
|
||||
import '@vben/styles';
|
||||
|
||||
import './variables.css';
|
||||
import './base.css';
|
||||
@ -1,132 +0,0 @@
|
||||
/**
|
||||
* Customize default theme styling by overriding CSS variables:
|
||||
* https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/styles/vars.css
|
||||
*/
|
||||
|
||||
/**
|
||||
* Colors
|
||||
*
|
||||
* Each colors have exact same color scale system with 3 levels of solid
|
||||
* colors with different brightness, and 1 soft color.
|
||||
*
|
||||
* - `XXX-1`: The most solid color used mainly for colored text. It must
|
||||
* satisfy the contrast ratio against when used on top of `XXX-soft`.
|
||||
*
|
||||
* - `XXX-2`: The color used mainly for hover state of the button.
|
||||
*
|
||||
* - `XXX-3`: The color for solid background, such as bg color of the button.
|
||||
* It must satisfy the contrast ratio with pure white (#ffffff) text on
|
||||
* top of it.
|
||||
*
|
||||
* - `XXX-soft`: The color used for subtle background such as custom container
|
||||
* or badges. It must satisfy the contrast ratio when putting `XXX-1` colors
|
||||
* on top of it.
|
||||
*
|
||||
* The soft color must be semi transparent alpha channel. This is crucial
|
||||
* because it allows adding multiple "soft" colors on top of each other
|
||||
* to create a accent, such as when having inline code block inside
|
||||
* custom containers.
|
||||
*
|
||||
* - `default`: The color used purely for subtle indication without any
|
||||
* special meanings attched to it such as bg color for menu hover state.
|
||||
*
|
||||
* - `brand`: Used for primary brand colors, such as link text, button with
|
||||
* brand theme, etc.
|
||||
*
|
||||
* - `tip`: Used to indicate useful information. The default theme uses the
|
||||
* brand color for this by default.
|
||||
*
|
||||
* - `warning`: Used to indicate warning to the users. Used in custom
|
||||
* container, badges, etc.
|
||||
*
|
||||
* - `danger`: Used to show error, or dangerous message to the users. Used
|
||||
* in custom container, badges, etc.
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
:root {
|
||||
/* --vp-c-indigo-1: #4f69fd; */
|
||||
--vp-c-default-1: var(--vp-c-gray-1);
|
||||
--vp-c-default-2: var(--vp-c-gray-2);
|
||||
--vp-c-default-3: var(--vp-c-gray-3);
|
||||
--vp-c-default-soft: var(--vp-c-gray-soft);
|
||||
--vp-c-brand-1: var(--vp-c-indigo-1);
|
||||
--vp-c-brand-2: var(--vp-c-indigo-2);
|
||||
--vp-c-brand-3: var(--vp-c-indigo-3);
|
||||
--vp-c-brand-soft: var(--vp-c-indigo-soft);
|
||||
--vp-c-tip-1: var(--vp-c-brand-1);
|
||||
--vp-c-tip-2: var(--vp-c-brand-2);
|
||||
--vp-c-tip-3: var(--vp-c-brand-3);
|
||||
--vp-c-tip-soft: var(--vp-c-brand-soft);
|
||||
--vp-c-warning-1: var(--vp-c-yellow-1);
|
||||
--vp-c-warning-2: var(--vp-c-yellow-2);
|
||||
--vp-c-warning-3: var(--vp-c-yellow-3);
|
||||
--vp-c-warning-soft: var(--vp-c-yellow-soft);
|
||||
--vp-c-danger-1: var(--vp-c-red-1);
|
||||
--vp-c-danger-2: var(--vp-c-red-2);
|
||||
--vp-c-danger-3: var(--vp-c-red-3);
|
||||
--vp-c-danger-soft: var(--vp-c-red-soft);
|
||||
|
||||
/**
|
||||
* Component: Button
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
--vp-button-brand-border: transparent;
|
||||
--vp-button-brand-text: var(--vp-c-white);
|
||||
--vp-button-brand-bg: var(--vp-c-brand-3);
|
||||
--vp-button-brand-hover-border: transparent;
|
||||
--vp-button-brand-hover-text: var(--vp-c-white);
|
||||
--vp-button-brand-hover-bg: var(--vp-c-brand-2);
|
||||
--vp-button-brand-active-border: transparent;
|
||||
--vp-button-brand-active-text: var(--vp-c-white);
|
||||
--vp-button-brand-active-bg: var(--vp-c-brand-1);
|
||||
|
||||
/**
|
||||
* Component: Home
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
--vp-home-hero-name-color: transparent;
|
||||
--vp-home-hero-name-background: linear-gradient(
|
||||
120deg,
|
||||
var(--vp-c-indigo-1) 30%,
|
||||
#18cefe
|
||||
);
|
||||
--vp-home-hero-image-background-image: linear-gradient(
|
||||
-45deg,
|
||||
#18cefe 50%,
|
||||
#c279ed 50%
|
||||
);
|
||||
--vp-home-hero-image-filter: blur(44px);
|
||||
|
||||
/**
|
||||
* Component: Custom Block
|
||||
* -------------------------------------------------------------------------- */
|
||||
--vp-custom-block-tip-border: transparent;
|
||||
--vp-custom-block-tip-text: var(--vp-c-text-1);
|
||||
--vp-custom-block-tip-bg: var(--vp-c-brand-soft);
|
||||
--vp-custom-block-tip-code-bg: var(--vp-c-brand-soft);
|
||||
|
||||
/**
|
||||
* modal zIndex
|
||||
*/
|
||||
--popup-z-index: 1000;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
:root {
|
||||
--vp-home-hero-image-filter: blur(56px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
:root {
|
||||
--vp-home-hero-image-filter: blur(68px);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Component: Algolia
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
.DocSearch {
|
||||
--docsearch-primary-color: var(--vp-c-brand-1) !important;
|
||||
}
|
||||
@ -1,37 +0,0 @@
|
||||
{
|
||||
"name": "@vben/docs",
|
||||
"version": "5.7.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "vitepress build",
|
||||
"dev": "vitepress dev",
|
||||
"docs:preview": "vitepress preview"
|
||||
},
|
||||
"imports": {
|
||||
"#/*": {
|
||||
"node": "./src/_env/node/*",
|
||||
"default": "./src/_env/*"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@vben-core/shadcn-ui": "workspace:*",
|
||||
"@vben/common-ui": "workspace:*",
|
||||
"@vben/locales": "workspace:*",
|
||||
"@vben/plugins": "workspace:*",
|
||||
"@vben/styles": "workspace:*",
|
||||
"ant-design-vue": "catalog:",
|
||||
"lucide-vue-next": "catalog:",
|
||||
"medium-zoom": "catalog:",
|
||||
"reka-ui": "catalog:",
|
||||
"vitepress-plugin-group-icons": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nolebase/vitepress-plugin-git-changelog": "catalog:",
|
||||
"@tailwindcss/vite": "catalog:",
|
||||
"@vben/tailwind-config": "workspace:*",
|
||||
"@vben/vite-config": "workspace:*",
|
||||
"@vite-pwa/vitepress": "catalog:",
|
||||
"vitepress": "catalog:",
|
||||
"vue": "catalog:"
|
||||
}
|
||||
}
|
||||
@ -1,128 +0,0 @@
|
||||
/**
|
||||
* 通用组件共同的使用的基础组件,原先放在 adapter/form 内部,限制了使用范围,这里提取出来,方便其他地方使用
|
||||
* 可用于 vben-form、vben-modal、vben-drawer 等组件使用,
|
||||
*/
|
||||
|
||||
import type { Component, SetupContext } from 'vue';
|
||||
|
||||
import type { BaseFormComponentType } from '@vben/common-ui';
|
||||
|
||||
import { h } from 'vue';
|
||||
|
||||
import { globalShareState } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import {
|
||||
AutoComplete,
|
||||
Button,
|
||||
Checkbox,
|
||||
CheckboxGroup,
|
||||
DatePicker,
|
||||
Divider,
|
||||
Input,
|
||||
InputNumber,
|
||||
InputPassword,
|
||||
Mentions,
|
||||
notification,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
RangePicker,
|
||||
Rate,
|
||||
Select,
|
||||
Space,
|
||||
Switch,
|
||||
Textarea,
|
||||
TimePicker,
|
||||
TreeSelect,
|
||||
Upload,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
const withDefaultPlaceholder = <T extends Component>(
|
||||
component: T,
|
||||
type: 'input' | 'select',
|
||||
) => {
|
||||
return (props: any, { attrs, slots }: Omit<SetupContext, 'expose'>) => {
|
||||
const placeholder = props?.placeholder || $t(`ui.placeholder.${type}`);
|
||||
return h(component, { ...props, ...attrs, placeholder }, slots);
|
||||
};
|
||||
};
|
||||
|
||||
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
|
||||
export type ComponentType =
|
||||
| 'AutoComplete'
|
||||
| 'Checkbox'
|
||||
| 'CheckboxGroup'
|
||||
| 'DatePicker'
|
||||
| 'DefaultButton'
|
||||
| 'Divider'
|
||||
| 'Input'
|
||||
| 'InputNumber'
|
||||
| 'InputPassword'
|
||||
| 'Mentions'
|
||||
| 'PrimaryButton'
|
||||
| 'Radio'
|
||||
| 'RadioGroup'
|
||||
| 'RangePicker'
|
||||
| 'Rate'
|
||||
| 'Select'
|
||||
| 'Space'
|
||||
| 'Switch'
|
||||
| 'Textarea'
|
||||
| 'TimePicker'
|
||||
| 'TreeSelect'
|
||||
| 'Upload'
|
||||
| BaseFormComponentType;
|
||||
|
||||
async function initComponentAdapter() {
|
||||
const components: Partial<Record<ComponentType, Component>> = {
|
||||
// 如果你的组件体积比较大,可以使用异步加载
|
||||
// Button: () =>
|
||||
// import('xxx').then((res) => res.Button),
|
||||
|
||||
AutoComplete,
|
||||
Checkbox,
|
||||
CheckboxGroup,
|
||||
DatePicker,
|
||||
// 自定义默认按钮
|
||||
DefaultButton: (props, { attrs, slots }) => {
|
||||
return h(Button, { ...props, attrs, type: 'default' }, slots);
|
||||
},
|
||||
Divider,
|
||||
Input: withDefaultPlaceholder(Input, 'input'),
|
||||
InputNumber: withDefaultPlaceholder(InputNumber, 'input'),
|
||||
InputPassword: withDefaultPlaceholder(InputPassword, 'input'),
|
||||
Mentions: withDefaultPlaceholder(Mentions, 'input'),
|
||||
// 自定义主要按钮
|
||||
PrimaryButton: (props, { attrs, slots }) => {
|
||||
return h(Button, { ...props, attrs, type: 'primary' }, slots);
|
||||
},
|
||||
Radio,
|
||||
RadioGroup,
|
||||
RangePicker,
|
||||
Rate,
|
||||
Select: withDefaultPlaceholder(Select, 'select'),
|
||||
Space,
|
||||
Switch,
|
||||
Textarea: withDefaultPlaceholder(Textarea, 'input'),
|
||||
TimePicker,
|
||||
TreeSelect: withDefaultPlaceholder(TreeSelect, 'select'),
|
||||
Upload,
|
||||
};
|
||||
|
||||
// 将组件注册到全局共享状态中
|
||||
globalShareState.setComponents(components);
|
||||
|
||||
// 定义全局共享状态中的消息提示
|
||||
globalShareState.defineMessage({
|
||||
// 复制成功消息提示
|
||||
copyPreferencesSuccess: (title, content) => {
|
||||
notification.success({
|
||||
description: content,
|
||||
message: title,
|
||||
placement: 'bottomRight',
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export { initComponentAdapter };
|
||||
@ -1,48 +0,0 @@
|
||||
import type {
|
||||
VbenFormSchema as FormSchema,
|
||||
VbenFormProps,
|
||||
} from '@vben/common-ui';
|
||||
|
||||
import type { ComponentType } from './component';
|
||||
|
||||
import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { initComponentAdapter } from './component';
|
||||
|
||||
initComponentAdapter();
|
||||
|
||||
setupVbenForm<ComponentType>({
|
||||
config: {
|
||||
baseModelPropName: 'value',
|
||||
// naive-ui组件的空值为null,不能是undefined,否则重置表单时不生效
|
||||
emptyStateValue: null,
|
||||
modelPropNameMap: {
|
||||
Checkbox: 'checked',
|
||||
Radio: 'checked',
|
||||
Switch: 'checked',
|
||||
Upload: 'fileList',
|
||||
},
|
||||
},
|
||||
defineRules: {
|
||||
required: (value, _params, ctx) => {
|
||||
if (value === undefined || value === null || value.length === 0) {
|
||||
return $t('ui.formRules.required', [ctx.label]);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
selectRequired: (value, _params, ctx) => {
|
||||
if (value === undefined || value === null) {
|
||||
return $t('ui.formRules.selectRequired', [ctx.label]);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const useVbenForm = useForm<ComponentType>;
|
||||
|
||||
export { useVbenForm, z };
|
||||
|
||||
export type VbenFormSchema = FormSchema<ComponentType>;
|
||||
export type { VbenFormProps };
|
||||
@ -1,71 +0,0 @@
|
||||
import { h } from 'vue';
|
||||
|
||||
import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table';
|
||||
|
||||
import { Button, Image } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm } from './form';
|
||||
|
||||
if (!import.meta.env.SSR) {
|
||||
setupVbenVxeTable({
|
||||
configVxeTable: (vxeUI) => {
|
||||
vxeUI.setConfig({
|
||||
grid: {
|
||||
align: 'center',
|
||||
border: false,
|
||||
columnConfig: {
|
||||
resizable: true,
|
||||
},
|
||||
|
||||
formConfig: {
|
||||
// 全局禁用vxe-table的表单配置,使用formOptions
|
||||
enabled: false,
|
||||
},
|
||||
minHeight: 180,
|
||||
proxyConfig: {
|
||||
autoLoad: true,
|
||||
response: {
|
||||
result: 'items',
|
||||
total: 'total',
|
||||
list: 'items',
|
||||
},
|
||||
showActiveMsg: true,
|
||||
showResponseMsg: false,
|
||||
},
|
||||
round: true,
|
||||
showOverflow: true,
|
||||
size: 'small',
|
||||
},
|
||||
});
|
||||
|
||||
// 表格配置项可以用 cellRender: { name: 'CellImage' },
|
||||
vxeUI.renderer.add('CellImage', {
|
||||
renderTableDefault(renderOpts, params) {
|
||||
const { props } = renderOpts;
|
||||
const { column, row } = params;
|
||||
return h(Image, { src: row[column.field], ...props });
|
||||
},
|
||||
});
|
||||
|
||||
// 表格配置项可以用 cellRender: { name: 'CellLink' },
|
||||
vxeUI.renderer.add('CellLink', {
|
||||
renderTableDefault(renderOpts) {
|
||||
const { props } = renderOpts;
|
||||
return h(
|
||||
Button,
|
||||
{ size: 'small', type: 'link' },
|
||||
{ default: () => props?.text },
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
// 这里可以自行扩展 vxe-table 的全局配置,比如自定义格式化
|
||||
// vxeUI.formats.add
|
||||
},
|
||||
useVbenForm,
|
||||
});
|
||||
}
|
||||
|
||||
export { useVbenVxeGrid };
|
||||
|
||||
export type * from '@vben/plugins/vxe-table';
|
||||
@ -1,4 +0,0 @@
|
||||
export const useVbenForm = () => {};
|
||||
export const z = {};
|
||||
export type VbenFormSchema = any;
|
||||
export type VbenFormProps = any;
|
||||
@ -1,3 +0,0 @@
|
||||
export type * from '@vben/plugins/vxe-table';
|
||||
|
||||
export const useVbenVxeGrid = () => {};
|
||||
@ -1,30 +0,0 @@
|
||||
# 社区交流
|
||||
|
||||
社区交流群主要是为了方便大家交流,提问,解答问题,分享经验等。偏自助方式,如果你有问题,可以通过以下方式加入社区交流群:
|
||||
|
||||
- [QQ频道](https://pd.qq.com/s/16p8lvvob):推荐!!!主要提供问题解答,分享经验等。
|
||||
- QQ群:[大群](https://qm.qq.com/q/MEmHoCLbG0),[1群](https://qm.qq.com/q/YacMHPYAMu)、[2群](https://qm.qq.com/q/ajVKZvFICk)、[3群](https://qm.qq.com/q/36zdwThP2E),[4群](https://qm.qq.com/q/sCzSlm3504),[5群](https://qm.qq.com/q/ya9XrtbS6s),主要的使用者交流群。
|
||||
- [Discord](https://discord.com/invite/VU62jTecad): 主要提供问题解答,分享经验等。
|
||||
|
||||
::: tip
|
||||
|
||||
免费QQ群人数上限200,将会不定期清理。推荐加入QQ频道进行交流
|
||||
|
||||
:::
|
||||
|
||||
## 微信群
|
||||
|
||||
作者主要通过微信群提供帮助,如果你有问题,可以通过以下方式加入微信群。
|
||||
|
||||
通过微信联系作者,注明加群来意:
|
||||
|
||||
::: tip
|
||||
|
||||
因为微信群人数有限制,加微信群要求:
|
||||
|
||||
- 通过[赞助](../sponsor/personal.md)任意金额。
|
||||
- 发送赞助`截图`,备注`加入微信群`即可。
|
||||
|
||||
:::
|
||||
|
||||
<img src="https://unpkg.com/@vbenjs/static-source@0.1.7/source/wechat.jpg" style="width: 300px;"/>
|
||||
@ -1,12 +0,0 @@
|
||||
# 定制开发
|
||||
|
||||
我们提供基于 Vben Admin 的技术支持服务及定制开发,基本需求我们都可以满足。
|
||||
|
||||
详细需求可添加作者了解,并注明来意:
|
||||
|
||||
- 通过邮箱联系开发者: [ann.vben@gmail.com](mailto:ann.vben@gmail.com)
|
||||
- 通过微信联系开发者:
|
||||
|
||||
<img src="https://unpkg.com/@vbenjs/static-source@0.1.7/source/wechat.jpg" style="width: 300px;"/>
|
||||
|
||||
我们会在第一时间回复您,定制费用根据需求而定。
|
||||
@ -1,8 +0,0 @@
|
||||
# 技术支持
|
||||
|
||||
## 问题反馈
|
||||
|
||||
在使用项目的过程中,如果遇到问题,你可以先详细阅读本文档,未找到解决方案时,可以通过以下方式获取技术支持:
|
||||
|
||||
- 通过 [GitHub Issues](https://github.com/vbenjs/vue-vben-admin/issues)
|
||||
- 通过 [GitHub Discussions](https://github.com/vbenjs/vue-vben-admin/discussions)
|
||||
@ -1,115 +0,0 @@
|
||||
---
|
||||
outline: deep
|
||||
---
|
||||
|
||||
# Vben Alert 轻量提示框
|
||||
|
||||
`Alert` 提供了一组纯 JavaScript 调用的轻量提示框能力,适合快速创建 `alert`、`confirm`、`prompt` 这类简单交互。
|
||||
|
||||
::: info 适用场景
|
||||
|
||||
`Alert` 与 `Modal` 的能力有部分重叠,但更适合临时确认、简单提示和轻量输入场景。复杂弹窗仍然建议使用 `Vben Modal`。:::
|
||||
|
||||
::: tip 注意
|
||||
|
||||
通过 `alert`、`confirm`、`prompt` 动态创建的弹窗,在已经打开的情况下不支持 HMR 热更新。修改相关代码后,需要关闭后重新打开。:::
|
||||
|
||||
## 基础用法
|
||||
|
||||
使用 `alert` 创建只有确认按钮的提示框。
|
||||
|
||||
<DemoPreview dir="demos/vben-alert/alert" />
|
||||
|
||||
使用 `confirm` 创建带确认和取消按钮的提示框。
|
||||
|
||||
<DemoPreview dir="demos/vben-alert/confirm" />
|
||||
|
||||
使用 `prompt` 创建可接收用户输入的提示框。
|
||||
|
||||
<DemoPreview dir="demos/vben-alert/prompt" />
|
||||
|
||||
## useAlertContext
|
||||
|
||||
当 `content`、`footer` 或 `icon` 使用的是自定义组件时,可以在这些组件内部通过 `useAlertContext()` 获取当前弹窗上下文,并主动触发确认或取消。
|
||||
|
||||
::: tip 注意
|
||||
|
||||
`useAlertContext` 只能在 `setup` 或函数式组件中使用。:::
|
||||
|
||||
### Methods
|
||||
|
||||
| 方法 | 描述 | 类型 | 版本要求 |
|
||||
| --------- | ---------------------- | ------------ | -------- |
|
||||
| doConfirm | 触发当前弹窗的确认操作 | `() => void` | `>5.5.4` |
|
||||
| doCancel | 触发当前弹窗的取消操作 | `() => void` | `>5.5.4` |
|
||||
|
||||
## 类型说明
|
||||
|
||||
```ts
|
||||
export type IconType = 'error' | 'info' | 'question' | 'success' | 'warning';
|
||||
|
||||
export type BeforeCloseScope = {
|
||||
isConfirm: boolean;
|
||||
};
|
||||
|
||||
export type AlertProps = {
|
||||
beforeClose?: (
|
||||
scope: BeforeCloseScope,
|
||||
) => boolean | Promise<boolean | undefined> | undefined;
|
||||
bordered?: boolean;
|
||||
buttonAlign?: 'center' | 'end' | 'start';
|
||||
cancelText?: string;
|
||||
centered?: boolean;
|
||||
confirmText?: string;
|
||||
containerClass?: string;
|
||||
content: Component | string;
|
||||
contentClass?: string;
|
||||
contentMasking?: boolean;
|
||||
footer?: Component | string;
|
||||
icon?: Component | IconType;
|
||||
overlayBlur?: number;
|
||||
showCancel?: boolean;
|
||||
title?: string;
|
||||
};
|
||||
|
||||
export type PromptProps<T = any> = {
|
||||
beforeClose?: (scope: {
|
||||
isConfirm: boolean;
|
||||
value: T | undefined;
|
||||
}) => boolean | Promise<boolean | undefined> | undefined;
|
||||
component?: Component;
|
||||
componentProps?: Recordable<any>;
|
||||
componentSlots?:
|
||||
| (() => any)
|
||||
| Recordable<unknown>
|
||||
| VNode
|
||||
| VNodeArrayChildren;
|
||||
defaultValue?: T;
|
||||
modelPropName?: string;
|
||||
} & Omit<AlertProps, 'beforeClose'>;
|
||||
|
||||
export function alert(options: AlertProps): Promise<void>;
|
||||
export function alert(
|
||||
message: string,
|
||||
options?: Partial<AlertProps>,
|
||||
): Promise<void>;
|
||||
export function alert(
|
||||
message: string,
|
||||
title?: string,
|
||||
options?: Partial<AlertProps>,
|
||||
): Promise<void>;
|
||||
|
||||
export async function prompt<T = any>(
|
||||
options: Omit<AlertProps, 'beforeClose'> & {
|
||||
beforeClose?: (
|
||||
scope: BeforeCloseScope & {
|
||||
value: T;
|
||||
},
|
||||
) => boolean | Promise<boolean | undefined> | undefined;
|
||||
component?: Component;
|
||||
componentProps?: Recordable<any>;
|
||||
defaultValue?: T;
|
||||
modelPropName?: string;
|
||||
},
|
||||
): Promise<T | undefined>;
|
||||
```
|
||||
@ -1,173 +0,0 @@
|
||||
---
|
||||
outline: deep
|
||||
---
|
||||
|
||||
# Vben ApiComponent Api组件包装器
|
||||
|
||||
框架提供的API“包装器”,它一般不独立使用,主要用于包装其它组件,为目标组件提供自动获取远程数据的能力,但仍然保持了目标组件的原始用法。
|
||||
|
||||
::: info 写在前面
|
||||
|
||||
我们在各个应用的组件适配器中,使用ApiComponent包装了Select、TreeSelect组件,使得这些组件可以自动获取远程数据并生成选项。其它类似的组件(比如Cascader)如有需要也可以参考示例代码自行进行包装。
|
||||
|
||||
:::
|
||||
|
||||
## 基础用法
|
||||
|
||||
通过 `component` 传入其它组件的定义,并配置相关的其它属性(主要是一些名称映射)。包装组件将通过 `api` 获取数据(`beforeFetch`、`afterFetch` 将分别在 `api` 运行前、运行后被调用),使用 `resultField` 从中提取数组,使用 `valueField`、`labelField` 等来从数据中提取 value 和 label(如果提供了 `childrenField`,会将其作为树形结构递归处理每一级数据),之后将处理好的数据通过 `optionsPropName` 指定的属性传递给目标组件。
|
||||
|
||||
::: details 包装级联选择器,点击下拉时开始加载远程数据
|
||||
|
||||
```vue
|
||||
<script lang="ts" setup>
|
||||
import { ApiComponent } from '@vben/common-ui';
|
||||
|
||||
import { Cascader } from 'ant-design-vue';
|
||||
|
||||
const treeData: Record<string, any> = [
|
||||
{
|
||||
label: '浙江',
|
||||
value: 'zhejiang',
|
||||
children: [
|
||||
{
|
||||
value: 'hangzhou',
|
||||
label: '杭州',
|
||||
children: [
|
||||
{
|
||||
value: 'xihu',
|
||||
label: '西湖',
|
||||
},
|
||||
{
|
||||
value: 'sudi',
|
||||
label: '苏堤',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'jiaxing',
|
||||
label: '嘉兴',
|
||||
children: [
|
||||
{
|
||||
value: 'wuzhen',
|
||||
label: '乌镇',
|
||||
},
|
||||
{
|
||||
value: 'meihuazhou',
|
||||
label: '梅花洲',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'zhoushan',
|
||||
label: '舟山',
|
||||
children: [
|
||||
{
|
||||
value: 'putuoshan',
|
||||
label: '普陀山',
|
||||
},
|
||||
{
|
||||
value: 'taohuadao',
|
||||
label: '桃花岛',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '江苏',
|
||||
value: 'jiangsu',
|
||||
children: [
|
||||
{
|
||||
value: 'nanjing',
|
||||
label: '南京',
|
||||
children: [
|
||||
{
|
||||
value: 'zhonghuamen',
|
||||
label: '中华门',
|
||||
},
|
||||
{
|
||||
value: 'zijinshan',
|
||||
label: '紫金山',
|
||||
},
|
||||
{
|
||||
value: 'yuhuatai',
|
||||
label: '雨花台',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
/**
|
||||
* 模拟请求接口
|
||||
*/
|
||||
function fetchApi(): Promise<Record<string, any>> {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve(treeData);
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<ApiComponent
|
||||
:api="fetchApi"
|
||||
:component="Cascader"
|
||||
:immediate="false"
|
||||
children-field="children"
|
||||
loading-slot="suffixIcon"
|
||||
visible-event="onDropdownVisibleChange"
|
||||
/>
|
||||
</template>
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
## 并发和缓存
|
||||
|
||||
有些场景下可能需要使用多个ApiComponent,它们使用了相同的远程数据源(例如用在可编辑的表格中)。如果直接将请求后端接口的函数传递给api属性,则每一个实例都会访问一次接口,这会造成资源浪费,是完全没有必要的。Tanstack Query提供了并发控制、缓存、重试等诸多特性,我们可以将接口请求函数用useQuery包装一下再传递给ApiComponent,这样的话无论页面有多少个使用相同数据源的ApiComponent实例,都只会发起一次远程请求。
|
||||
|
||||
## API
|
||||
|
||||
### Props
|
||||
|
||||
| 属性名 | 描述 | 类型 | 默认值 | 版本要求 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| modelValue(v-model) | 当前值 | `any` | - | - |
|
||||
| component | 欲包装的组件(以下称为目标组件) | `Component` | - | - |
|
||||
| numberToString | 是否将value从数字转为string | `boolean` | `false` | - |
|
||||
| api | 获取数据的函数 | `(arg?: any) => Promise<OptionsItem[] \| Record<string, any>>` | - | - |
|
||||
| params | 传递给api的参数 | `Record<string, any>` | - | - |
|
||||
| resultField | 从api返回的结果中提取options数组的字段名 | `string` | - | - |
|
||||
| labelField | label字段名 | `string` | `label` | - |
|
||||
| childrenField | 子级数据字段名,需要层级数据的组件可用 | `string` | `` | - |
|
||||
| valueField | value字段名 | `string` | `value` | - |
|
||||
| optionsPropName | 目标组件接收options数据的属性名称 | `string` | `options` | - |
|
||||
| modelPropName | 目标组件的双向绑定属性名,默认为modelValue。部分组件可能为value | `string` | `modelValue` | - |
|
||||
| immediate | 是否立即调用api | `boolean` | `true` | - |
|
||||
| alwaysLoad | 每次`visibleEvent`事件发生时都重新请求数据 | `boolean` | `false` | - |
|
||||
| beforeFetch | 在api请求之前的回调函数 | `AnyPromiseFunction<any, any>` | - | - |
|
||||
| afterFetch | 在api请求之后的回调函数 | `AnyPromiseFunction<any, any>` | - | - |
|
||||
| options | 直接传入选项数据,也作为api返回空数据时的后备数据 | `OptionsItem[]` | - | - |
|
||||
| visibleEvent | 触发重新请求数据的事件名 | `string` | - | - |
|
||||
| loadingSlot | 目标组件的插槽名称,用来显示一个"加载中"的图标 | `string` | - | - |
|
||||
| autoSelect | 自动设置选项 | `'first' \| 'last' \| 'one'\| ((item: OptionsItem[]) => OptionsItem) \| false` | `false` | >5.5.4 |
|
||||
|
||||
#### autoSelect 自动设置选项
|
||||
|
||||
如果当前值为undefined,在选项数据成功加载之后,自动从备选项中选择一个作为当前值。默认值为`false`,即不自动选择选项。注意:该属性不应用于多选组件。可选值有:
|
||||
|
||||
- `"first"`:自动选择第一个选项
|
||||
- `"last"`:自动选择最后一个选项
|
||||
- `"one"`:有且仅有一个选项时,自动选择它
|
||||
- `自定义函数`:自定义选择逻辑,函数的参数为options,返回值为选择的选项
|
||||
- `false`:不自动选择选项
|
||||
|
||||
### Methods
|
||||
|
||||
| 方法 | 描述 | 类型 | 版本要求 |
|
||||
| --- | --- | --- | --- |
|
||||
| getComponentRef | 获取被包装的组件的实例 | ()=>T | >5.5.4 |
|
||||
| updateParam | 设置接口请求参数(将与params属性合并) | (newParams: Record<string, any>)=>void | >5.5.4 |
|
||||
| getOptions | 获取已加载的选项数据 | ()=>OptionsItem[] | >5.5.4 |
|
||||
| getValue | 获取当前值 | ()=>any | >5.5.4 |
|
||||
@ -1,57 +0,0 @@
|
||||
---
|
||||
outline: deep
|
||||
---
|
||||
|
||||
# Vben CountToAnimator 数字动画
|
||||
|
||||
`CountToAnimator` 用于展示数字滚动动画效果。
|
||||
|
||||
> 如果文档内没有覆盖到你需要的细节,可以结合在线示例一起查看。
|
||||
|
||||
::: info 写在前面
|
||||
|
||||
这是一个轻量数字动画组件。如果你需要完全不同的过渡控制方式,也可以直接使用原生动画方案或自行封装。:::
|
||||
|
||||
## 基础用法
|
||||
|
||||
通过 `start-val` 和 `end-val` 设置数字动画的起始值和结束值,配合 `duration` 控制动画时长。
|
||||
|
||||
<DemoPreview dir="demos/vben-count-to-animator/basic" />
|
||||
|
||||
## 自定义前缀与分隔符
|
||||
|
||||
通过 `prefix`、`suffix`、`separator` 和 `decimal` 可以控制展示格式。
|
||||
|
||||
<DemoPreview dir="demos/vben-count-to-animator/custom" />
|
||||
|
||||
### Props
|
||||
|
||||
| 属性名 | 描述 | 类型 | 默认值 |
|
||||
| --- | --- | --- | --- |
|
||||
| startVal | 起始值 | `number` | `0` |
|
||||
| endVal | 结束值 | `number` | `2021` |
|
||||
| duration | 动画持续时间 | `number` | `1500` |
|
||||
| autoplay | 是否自动播放 | `boolean` | `true` |
|
||||
| prefix | 前缀 | `string` | `''` |
|
||||
| suffix | 后缀 | `string` | `''` |
|
||||
| separator | 千分位分隔符 | `string` | `','` |
|
||||
| decimal | 小数点分隔符 | `string` | `'.'` |
|
||||
| color | 文本颜色 | `string` | `''` |
|
||||
| useEasing | 是否启用过渡预设 | `boolean` | `true` |
|
||||
| transition | 过渡预设名称 | `keyof typeof TransitionPresets` | `'linear'` |
|
||||
| decimals | 保留小数位数 | `number` | `0` |
|
||||
|
||||
### Events
|
||||
|
||||
| 事件名 | 描述 | 类型 |
|
||||
| -------------- | ----------------------------- | ---------------- |
|
||||
| started | 动画开始时触发 | `() => void` |
|
||||
| finished | 动画结束时触发 | `() => void` |
|
||||
| ~~onStarted~~ | ~~已废弃,请改用 `started`~~ | ~~`() => void`~~ |
|
||||
| ~~onFinished~~ | ~~已废弃,请改用 `finished`~~ | ~~`() => void`~~ |
|
||||
|
||||
### Methods
|
||||
|
||||
| 方法名 | 描述 | 类型 |
|
||||
| ------ | -------------------------------- | ------------ |
|
||||
| reset | 重置为 `startVal` 并重新执行动画 | `() => void` |
|
||||
@ -1,156 +0,0 @@
|
||||
---
|
||||
outline: deep
|
||||
---
|
||||
|
||||
# Vben Drawer 抽屉
|
||||
|
||||
框架提供的抽屉组件,支持`自动高度`、`loading`等功能。
|
||||
|
||||
> 如果文档内没有参数说明,可以尝试在在线示例内寻找
|
||||
|
||||
::: info 写在前面
|
||||
|
||||
如果你觉得现有组件的封装不够理想,或者不完全符合你的需求,大可以直接使用原生组件,亦或亲手封装一个适合的组件。框架提供的组件并非束缚,使用与否,完全取决于你的需求与自由。
|
||||
|
||||
:::
|
||||
|
||||
::: tip README
|
||||
|
||||
下方示例代码中的,存在一些国际化、主题色未适配问题,这些问题只在文档内会出现,实际使用并不会有这些问题,可忽略,不必纠结。
|
||||
|
||||
:::
|
||||
|
||||
## 基础用法
|
||||
|
||||
使用 `useVbenDrawer` 创建最基础的抽屉。
|
||||
|
||||
<DemoPreview dir="demos/vben-drawer/basic" />
|
||||
|
||||
## 组件抽离
|
||||
|
||||
Drawer 内的内容一般业务中,会比较复杂,所以我们可以将 drawer 内的内容抽离出来,也方便复用。通过 `connectedComponent` 参数,可以将内外组件进行连接,而不用其他任何操作。
|
||||
|
||||
<DemoPreview dir="demos/vben-drawer/extra" />
|
||||
|
||||
## 自动计算高度
|
||||
|
||||
弹窗会自动计算内容高度,超过一定高度会出现滚动条,同时结合 `loading` 效果以及使用 `prepend-footer` 插槽。
|
||||
|
||||
<DemoPreview dir="demos/vben-drawer/auto-height" />
|
||||
|
||||
## 使用 Api
|
||||
|
||||
通过 `drawerApi` 可以调用 drawer 的方法以及使用 `setState` 更新 drawer 的状态。
|
||||
|
||||
<DemoPreview dir="demos/vben-drawer/dynamic" />
|
||||
|
||||
## 数据共享
|
||||
|
||||
如果你使用了 `connectedComponent` 参数,那么内外组件会共享数据,比如一些表单回填等操作。可以用 `drawerApi` 来获取数据和设置数据,配合 `onOpenChange`,可以满足大部分的需求。
|
||||
|
||||
<DemoPreview dir="demos/vben-drawer/shared-data" />
|
||||
|
||||
::: info 注意
|
||||
|
||||
- `VbenDrawer` 组件对于参数的处理优先级是 `slot` > `props` > `state`(通过api更新的状态以及useVbenDrawer参数)。如果你已经传入了 `slot` 或者 `props`,那么 `setState` 将不会生效,这种情况下你可以通过 `slot` 或者 `props` 来更新状态。
|
||||
- 如果你使用到了 `connectedComponent` 参数,那么会存在 2 个`useVbenDrawer`, 此时,如果同时设置了相同的参数,那么以内部为准(也就是没有设置 connectedComponent 的代码)。比如 同时设置了 `onConfirm`,那么以内部的 `onConfirm` 为准。`onOpenChange`事件除外,内外都会触发。
|
||||
- 使用了`connectedComponent`参数时,可以配置`destroyOnClose`属性来决定当关闭弹窗时,是否要销毁`connectedComponent`组件(重新创建`connectedComponent`组件,这将会把其内部所有的变量、状态、数据等恢复到初始状态。)。
|
||||
- 如果抽屉的默认行为不符合你的预期,可以在对应应用的 `apps/<app>/src/bootstrap.ts` 中修改 `setDefaultDrawerProps` 的参数来设置默认属性,例如修改默认 `zIndex` 等。
|
||||
|
||||
:::
|
||||
|
||||
## API
|
||||
|
||||
```ts
|
||||
// Drawer 为弹窗组件
|
||||
// drawerApi 为弹窗的方法
|
||||
const [Drawer, drawerApi] = useVbenDrawer({
|
||||
// 属性
|
||||
// 事件
|
||||
});
|
||||
```
|
||||
|
||||
### Props
|
||||
|
||||
所有属性都可以传入 `useVbenDrawer` 的第一个参数中。
|
||||
|
||||
| 属性名 | 描述 | 类型 | 默认值 |
|
||||
| --- | --- | --- | --- |
|
||||
| appendToMain | 是否挂载到内容区域(默认挂载到body) | `boolean` | `false` |
|
||||
| connectedComponent | 连接另一个Drawer组件 | `Component` | - |
|
||||
| destroyOnClose | 关闭时销毁 | `boolean` | `false` |
|
||||
| title | 标题 | `string\|slot` | - |
|
||||
| titleTooltip | 标题提示信息 | `string\|slot` | - |
|
||||
| description | 描述信息 | `string\|slot` | - |
|
||||
| isOpen | 弹窗打开状态 | `boolean` | `false` |
|
||||
| loading | 弹窗加载状态 | `boolean` | `false` |
|
||||
| closable | 显示关闭按钮 | `boolean` | `true` |
|
||||
| closeIconPlacement | 关闭按钮位置 | `'left'\|'right'` | `right` |
|
||||
| modal | 显示遮罩 | `boolean` | `true` |
|
||||
| header | 显示header | `boolean` | `true` |
|
||||
| footer | 显示footer | `boolean\|slot` | `true` |
|
||||
| confirmLoading | 确认按钮loading状态 | `boolean` | `false` |
|
||||
| closeOnClickModal | 点击遮罩关闭弹窗 | `boolean` | `true` |
|
||||
| closeOnPressEscape | esc 关闭弹窗 | `boolean` | `true` |
|
||||
| confirmText | 确认按钮文本 | `string\|slot` | `确认` |
|
||||
| cancelText | 取消按钮文本 | `string\|slot` | `取消` |
|
||||
| placement | 抽屉弹出位置 | `'left'\|'right'\|'top'\|'bottom'` | `right` |
|
||||
| showCancelButton | 显示取消按钮 | `boolean` | `true` |
|
||||
| showConfirmButton | 显示确认按钮 | `boolean` | `true` |
|
||||
| class | modal的class,宽度通过这个配置 | `string` | - |
|
||||
| contentClass | modal内容区域的class | `string` | - |
|
||||
| footerClass | modal底部区域的class | `string` | - |
|
||||
| headerClass | modal顶部区域的class | `string` | - |
|
||||
| zIndex | 抽屉的ZIndex层级 | `number` | `1000` |
|
||||
| overlayBlur | 遮罩模糊度 | `number` | - |
|
||||
|
||||
::: info appendToMain
|
||||
|
||||
`appendToMain`可以指定将抽屉挂载到内容区域,打开抽屉时,内容区域以外的部分(标签栏、导航菜单等等)不会被遮挡。默认情况下,抽屉会挂载到body上。但是:挂载到内容区域时,作为页面根容器的`Page`组件,需要设置`auto-content-height`属性,以便抽屉能够正确计算高度。
|
||||
|
||||
:::
|
||||
|
||||
### Event
|
||||
|
||||
以下事件,只有在 `useVbenDrawer({onCancel:()=>{}})` 中传入才会生效。
|
||||
|
||||
| 事件名 | 描述 | 类型 | 版本限制 |
|
||||
| --- | --- | --- | --- |
|
||||
| onBeforeClose | 关闭前触发,返回 `false` 或 Promise reject 则禁止关闭 | `()=>Promise<boolean \| undefined>\|boolean\|undefined` | >5.5.2 支持 Promise |
|
||||
| onCancel | 点击取消按钮触发 | `()=>void` | --- |
|
||||
| onClosed | 关闭动画播放完毕时触发 | `()=>void` | >5.5.2 |
|
||||
| onConfirm | 点击确认按钮触发 | `()=>void` | --- |
|
||||
| onOpenChange | 关闭或者打开弹窗时触发 | `(isOpen:boolean)=>void` | --- |
|
||||
| onOpened | 打开动画播放完毕时触发 | `()=>void` | >5.5.2 |
|
||||
|
||||
### Slots
|
||||
|
||||
除了上面的属性类型包含`slot`,还可以通过插槽来自定义弹窗的内容。
|
||||
|
||||
| 插槽名 | 描述 |
|
||||
| -------------- | -------------------------------------------------- |
|
||||
| default | 默认插槽 - 弹窗内容 |
|
||||
| prepend-footer | 取消按钮左侧 |
|
||||
| center-footer | 取消按钮和确认按钮中间(不使用 footer 插槽时有效) |
|
||||
| append-footer | 确认按钮右侧 |
|
||||
| close-icon | 关闭按钮图标 |
|
||||
| extra | 额外内容(标题右侧) |
|
||||
|
||||
### drawerApi
|
||||
|
||||
| 方法 | 描述 | 类型 | 版本限制 |
|
||||
| --- | --- | --- | --- |
|
||||
| setState | 动态设置抽屉状态属性 | `(((prev: DrawerState) => Partial<DrawerState>)\| Partial<DrawerState>)=>drawerApi` |
|
||||
| open | 打开弹窗 | `()=>void` | --- |
|
||||
| close | 关闭弹窗 | `()=>void` | --- |
|
||||
| setData | 设置共享数据 | `<T>(data:T)=>drawerApi` | --- |
|
||||
| getData | 获取共享数据 | `<T>()=>T` | --- |
|
||||
| useStore | 获取可响应式状态 | - | --- |
|
||||
| lock | 将抽屉标记为提交中,锁定当前状态 | `(isLock:boolean)=>drawerApi` | >5.5.3 |
|
||||
| unlock | lock方法的反操作,解除抽屉的锁定状态,也是lock(false)的别名 | `()=>drawerApi` | >5.5.3 |
|
||||
|
||||
::: info lock
|
||||
|
||||
`lock`方法用于锁定抽屉的状态,一般用于提交数据的过程中防止用户重复提交或者抽屉被意外关闭、表单数据被改变等等。当处于锁定状态时,抽屉的确认按钮会变为loading状态,同时禁用取消按钮和关闭按钮、禁止ESC或者点击遮罩等方式关闭抽屉、开启抽屉的spinner动画以遮挡弹窗内容。调用`close`方法关闭处于锁定状态的抽屉时,会自动解锁。要主动解除这种状态,可以调用`unlock`方法或者再次调用lock方法并传入false参数。
|
||||
|
||||
:::
|
||||
@ -1,64 +0,0 @@
|
||||
---
|
||||
outline: deep
|
||||
---
|
||||
|
||||
# Vben EllipsisText 省略文本
|
||||
|
||||
`EllipsisText` 用于展示超长文本,支持省略、Tooltip 提示以及点击展开收起。
|
||||
|
||||
> 如果文档内没有覆盖到你需要的细节,可以结合在线示例一起查看。
|
||||
|
||||
## 基础用法
|
||||
|
||||
通过默认插槽提供文本内容,`maxWidth` 用于限制文本区域宽度。
|
||||
|
||||
<DemoPreview dir="demos/vben-ellipsis-text/line" />
|
||||
|
||||
## 可折叠文本
|
||||
|
||||
通过 `line` 设置折叠后的最大行数,通过 `expand` 开启点击展开与收起。
|
||||
|
||||
<DemoPreview dir="demos/vben-ellipsis-text/expand" />
|
||||
|
||||
## 自定义提示浮层
|
||||
|
||||
通过 `tooltip` 插槽自定义提示内容。
|
||||
|
||||
<DemoPreview dir="demos/vben-ellipsis-text/tooltip" />
|
||||
|
||||
## 仅在省略时显示 Tooltip
|
||||
|
||||
通过 `tooltip-when-ellipsis` 控制仅在文本被截断时显示 Tooltip。
|
||||
|
||||
<DemoPreview dir="demos/vben-ellipsis-text/auto-display" />
|
||||
|
||||
## API
|
||||
|
||||
### Props
|
||||
|
||||
| 属性名 | 描述 | 类型 | 默认值 |
|
||||
| --- | --- | --- | --- |
|
||||
| expand | 是否支持点击展开或收起 | `boolean` | `false` |
|
||||
| line | 文本最大显示行数 | `number` | `1` |
|
||||
| maxWidth | 文本区域最大宽度 | `number \| string` | `'100%'` |
|
||||
| placement | 提示浮层位置 | `'bottom' \| 'left' \| 'right' \| 'top'` | `'top'` |
|
||||
| tooltip | 是否启用文本提示 | `boolean` | `true` |
|
||||
| tooltipWhenEllipsis | 是否仅在文本被截断时显示提示 | `boolean` | `false` |
|
||||
| ellipsisThreshold | 文本截断检测阈值,值越大判定越严格 | `number` | `3` |
|
||||
| tooltipBackgroundColor | 提示背景色 | `string` | `''` |
|
||||
| tooltipColor | 提示文字颜色 | `string` | `''` |
|
||||
| tooltipFontSize | 提示文字大小,单位 `px` | `number` | `14` |
|
||||
| tooltipMaxWidth | 提示内容最大宽度,单位 `px` | `number` | - |
|
||||
| tooltipOverlayStyle | 提示内容区域样式 | `CSSProperties` | `{ textAlign: 'justify' }` |
|
||||
|
||||
### Events
|
||||
|
||||
| 事件名 | 描述 | 类型 |
|
||||
| ------------ | ------------------ | ----------------------------- |
|
||||
| expandChange | 展开状态变化时触发 | `(isExpand: boolean) => void` |
|
||||
|
||||
### Slots
|
||||
|
||||
| 插槽名 | 描述 |
|
||||
| ------- | ---------------------------------- |
|
||||
| tooltip | 开启文本提示时,用于自定义提示内容 |
|
||||
@ -1,568 +0,0 @@
|
||||
---
|
||||
outline: deep
|
||||
---
|
||||
|
||||
# Vben Form 表单
|
||||
|
||||
框架提供的表单组件,可适配 `Element Plus`、`Ant Design Vue`、`Naive UI` 等框架。
|
||||
|
||||
> 如果文档内没有参数说明,可以尝试在在线示例内寻找
|
||||
|
||||
::: info 写在前面
|
||||
|
||||
如果你觉得现有组件的封装不够理想,或者不完全符合你的需求,大可以直接使用原生组件,亦或亲手封装一个适合的组件。框架提供的组件并非束缚,使用与否,完全取决于你的需求与自由。
|
||||
|
||||
:::
|
||||
|
||||
## 适配器
|
||||
|
||||
表单底层使用 [vee-validate](https://vee-validate.logaretm.com/v4/) 进行表单验证,所以你可以使用 `vee-validate` 的所有功能。对于不同的 UI 框架,我们提供了适配器,以便更好的适配不同的 UI 框架。
|
||||
|
||||
### 适配器说明
|
||||
|
||||
每个应用都有不同的 UI 框架,所以在应用的 `src/adapter/form` 和 `src/adapter/component` 内部,你可以根据自己的需求,进行组件适配。下面是 `Ant Design Vue` 的适配器示例代码,可根据注释查看说明:
|
||||
|
||||
::: details ant design vue 表单适配器
|
||||
|
||||
```ts
|
||||
import type {
|
||||
VbenFormSchema as FormSchema,
|
||||
VbenFormProps,
|
||||
} from '@vben/common-ui';
|
||||
|
||||
import type { ComponentType } from './component';
|
||||
|
||||
import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { initComponentAdapter } from './component';
|
||||
|
||||
initComponentAdapter();
|
||||
setupVbenForm<ComponentType>({
|
||||
config: {
|
||||
// ant design vue组件库默认都是 v-model:value
|
||||
baseModelPropName: 'value',
|
||||
// 一些组件库空值为 null,重置表单时需要和实际组件行为保持一致
|
||||
emptyStateValue: null,
|
||||
// 一些组件是 v-model:checked 或者 v-model:fileList
|
||||
modelPropNameMap: {
|
||||
Checkbox: 'checked',
|
||||
Radio: 'checked',
|
||||
Switch: 'checked',
|
||||
Upload: 'fileList',
|
||||
},
|
||||
},
|
||||
defineRules: {
|
||||
// 输入项目必填国际化适配
|
||||
required: (value, _params, ctx) => {
|
||||
if (value === undefined || value === null || value.length === 0) {
|
||||
return $t('ui.formRules.required', [ctx.label]);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
// 选择项目必填国际化适配
|
||||
selectRequired: (value, _params, ctx) => {
|
||||
if (value === undefined || value === null) {
|
||||
return $t('ui.formRules.selectRequired', [ctx.label]);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const useVbenForm = useForm<ComponentType>;
|
||||
|
||||
export { useVbenForm, z };
|
||||
export type VbenFormSchema = FormSchema<ComponentType>;
|
||||
export type { VbenFormProps };
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
::: details ant design vue 组件适配器
|
||||
|
||||
```ts
|
||||
/**
|
||||
* 通用组件共同的使用的基础组件,原先放在 adapter/form 内部,限制了使用范围,这里提取出来,方便其他地方使用
|
||||
* 可用于 vben-form、vben-modal、vben-drawer 等组件使用,
|
||||
*/
|
||||
|
||||
import type { BaseFormComponentType } from '@vben/common-ui';
|
||||
|
||||
import type { Component, SetupContext } from 'vue';
|
||||
import { h } from 'vue';
|
||||
|
||||
import { globalShareState } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
import {
|
||||
AutoComplete,
|
||||
Button,
|
||||
Checkbox,
|
||||
CheckboxGroup,
|
||||
DatePicker,
|
||||
Divider,
|
||||
Input,
|
||||
InputNumber,
|
||||
InputPassword,
|
||||
Mentions,
|
||||
notification,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
RangePicker,
|
||||
Rate,
|
||||
Select,
|
||||
Space,
|
||||
Switch,
|
||||
Textarea,
|
||||
TimePicker,
|
||||
TreeSelect,
|
||||
Upload,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
const withDefaultPlaceholder = <T extends Component>(
|
||||
component: T,
|
||||
type: 'input' | 'select',
|
||||
) => {
|
||||
return (props: any, { attrs, slots }: Omit<SetupContext, 'expose'>) => {
|
||||
const placeholder = props?.placeholder || $t(`ui.placeholder.${type}`);
|
||||
return h(component, { ...props, ...attrs, placeholder }, slots);
|
||||
};
|
||||
};
|
||||
|
||||
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
|
||||
export type ComponentType =
|
||||
| 'AutoComplete'
|
||||
| 'Checkbox'
|
||||
| 'CheckboxGroup'
|
||||
| 'DatePicker'
|
||||
| 'DefaultButton'
|
||||
| 'Divider'
|
||||
| 'Input'
|
||||
| 'InputNumber'
|
||||
| 'InputPassword'
|
||||
| 'Mentions'
|
||||
| 'PrimaryButton'
|
||||
| 'Radio'
|
||||
| 'RadioGroup'
|
||||
| 'RangePicker'
|
||||
| 'Rate'
|
||||
| 'Select'
|
||||
| 'Space'
|
||||
| 'Switch'
|
||||
| 'Textarea'
|
||||
| 'TimePicker'
|
||||
| 'TreeSelect'
|
||||
| 'Upload'
|
||||
| BaseFormComponentType;
|
||||
|
||||
async function initComponentAdapter() {
|
||||
const components: Partial<Record<ComponentType, Component>> = {
|
||||
// 如果你的组件体积比较大,可以使用异步加载
|
||||
// Button: () =>
|
||||
// import('xxx').then((res) => res.Button),
|
||||
|
||||
AutoComplete,
|
||||
Checkbox,
|
||||
CheckboxGroup,
|
||||
DatePicker,
|
||||
// 自定义默认按钮
|
||||
DefaultButton: (props, { attrs, slots }) => {
|
||||
return h(Button, { ...props, attrs, type: 'default' }, slots);
|
||||
},
|
||||
Divider,
|
||||
Input: withDefaultPlaceholder(Input, 'input'),
|
||||
InputNumber: withDefaultPlaceholder(InputNumber, 'input'),
|
||||
InputPassword: withDefaultPlaceholder(InputPassword, 'input'),
|
||||
Mentions: withDefaultPlaceholder(Mentions, 'input'),
|
||||
// 自定义主要按钮
|
||||
PrimaryButton: (props, { attrs, slots }) => {
|
||||
return h(Button, { ...props, attrs, type: 'primary' }, slots);
|
||||
},
|
||||
Radio,
|
||||
RadioGroup,
|
||||
RangePicker,
|
||||
Rate,
|
||||
Select: withDefaultPlaceholder(Select, 'select'),
|
||||
Space,
|
||||
Switch,
|
||||
Textarea: withDefaultPlaceholder(Textarea, 'input'),
|
||||
TimePicker,
|
||||
TreeSelect: withDefaultPlaceholder(TreeSelect, 'select'),
|
||||
Upload,
|
||||
};
|
||||
|
||||
// 将组件注册到全局共享状态中
|
||||
globalShareState.setComponents(components);
|
||||
|
||||
// 定义全局共享状态中的消息提示
|
||||
globalShareState.defineMessage({
|
||||
// 复制成功消息提示
|
||||
copyPreferencesSuccess: (title, content) => {
|
||||
notification.success({
|
||||
description: content,
|
||||
message: title,
|
||||
placement: 'bottomRight',
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export { initComponentAdapter };
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
## 基础用法
|
||||
|
||||
::: tip README
|
||||
|
||||
下方示例代码中的,存在一些国际化、主题色未适配问题,这些问题只在文档内会出现,实际使用并不会有这些问题,可忽略,不必纠结。
|
||||
|
||||
:::
|
||||
|
||||
使用 `useVbenForm` 创建最基础的表单。
|
||||
|
||||
<DemoPreview dir="demos/vben-form/basic" />
|
||||
|
||||
## 查询表单
|
||||
|
||||
查询表单是一种特殊的表单,用于查询数据。查询表单不会触发表单验证,只会触发查询事件。
|
||||
|
||||
<DemoPreview dir="demos/vben-form/query" />
|
||||
|
||||
## 表单校验
|
||||
|
||||
表单校验是一个非常重要的功能,可以通过 `rules` 属性进行校验。
|
||||
|
||||
<DemoPreview dir="demos/vben-form/rules" />
|
||||
|
||||
## 表单联动
|
||||
|
||||
表单联动是一个非常常见的功能,可以通过 `dependencies` 属性进行联动。
|
||||
|
||||
_注意_ 需要指定 `dependencies` 的 `triggerFields` 属性,设置由谁的改动来触发,以便表单组件能够正确的联动。
|
||||
|
||||
<DemoPreview dir="demos/vben-form/dynamic" />
|
||||
|
||||
## 自定义组件
|
||||
|
||||
如果你的业务组件库没有提供某个组件,你可以自行封装一个组件,然后加到表单内部。
|
||||
|
||||
<DemoPreview dir="demos/vben-form/custom" />
|
||||
|
||||
## 操作
|
||||
|
||||
一些常见的表单操作。
|
||||
|
||||
<DemoPreview dir="demos/vben-form/api" />
|
||||
|
||||
## API
|
||||
|
||||
`useVbenForm` 返回一个数组,第一个元素是表单组件,第二个元素是表单的方法。
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
|
||||
// Form 为弹窗组件
|
||||
// formApi 为弹窗的方法
|
||||
const [Form, formApi] = useVbenForm({
|
||||
// 属性
|
||||
// 事件
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Form />
|
||||
</template>
|
||||
```
|
||||
|
||||
### FormApi
|
||||
|
||||
useVbenForm 返回的第二个参数,是一个对象,包含了一些表单的方法。
|
||||
|
||||
| 方法名 | 描述 | 类型 | 版本号 |
|
||||
| --- | --- | --- | --- |
|
||||
| submitForm | 提交表单 | `(e:Event)=>Promise<Record<string,any>>` | - |
|
||||
| validateAndSubmitForm | 提交并校验表单 | `(e:Event)=>Promise<Record<string,any>>` | - |
|
||||
| resetForm | 重置表单 | `()=>Promise<void>` | - |
|
||||
| setValues | 设置表单值, 默认会过滤不在schema中定义的field, 可通过filterFields形参关闭过滤 | `(fields: Record<string, any>, filterFields?: boolean, shouldValidate?: boolean) => Promise<void>` | - |
|
||||
| getValues | 获取表单值 | `(fields:Record<string, any>,shouldValidate: boolean = false)=>Promise<void>` | - |
|
||||
| validate | 表单校验 | `()=>Promise<void>` | - |
|
||||
| validateField | 校验指定字段 | `(fieldName: string)=>Promise<ValidationResult<unknown>>` | - |
|
||||
| isFieldValid | 检查某个字段是否已通过校验 | `(fieldName: string)=>Promise<boolean>` | - |
|
||||
| resetValidate | 重置表单校验 | `()=>Promise<void>` | - |
|
||||
| updateSchema | 更新formSchema | `(schema:FormSchema[])=>void` | - |
|
||||
| setFieldValue | 设置字段值 | `(field: string, value: any, shouldValidate?: boolean)=>Promise<void>` | - |
|
||||
| setState | 设置组件状态(props) | `(stateOrFn:\| ((prev: VbenFormProps) => Partial<VbenFormProps>)\| Partial<VbenFormProps>)=>Promise<void>` | - |
|
||||
| getState | 获取组件状态(props) | `()=>Promise<VbenFormProps>` | - |
|
||||
| form | 表单对象实例,可以操作表单,见 [useForm](https://vee-validate.logaretm.com/v4/api/use-form/) | - | - |
|
||||
| getFieldComponentRef | 获取指定字段的组件实例 | `<T=unknown>(fieldName: string)=>T` | >5.5.3 |
|
||||
| getFocusedField | 获取当前已获得焦点的字段 | `()=>string\|undefined` | >5.5.3 |
|
||||
|
||||
## Props
|
||||
|
||||
所有属性都可以传入 `useVbenForm` 的第一个参数中。
|
||||
|
||||
| 属性名 | 描述 | 类型 | 默认值 |
|
||||
| --- | --- | --- | --- |
|
||||
| layout | 表单项布局 | `'horizontal' \| 'vertical'\| 'inline'` | `horizontal` |
|
||||
| showCollapseButton | 是否显示折叠按钮 | `boolean` | `false` |
|
||||
| wrapperClass | 表单的布局,基于tailwindcss | `any` | - |
|
||||
| actionWrapperClass | 表单操作区域class | `any` | - |
|
||||
| actionLayout | 表单操作按钮位置 | `'newLine' \| 'rowEnd' \| 'inline'` | `rowEnd` |
|
||||
| actionPosition | 表单操作按钮对齐方式 | `'left' \| 'center' \| 'right'` | `right` |
|
||||
| handleReset | 表单重置回调 | `(values: Record<string, any>,) => Promise<void> \| void` | - |
|
||||
| handleSubmit | 表单提交回调 | `(values: Record<string, any>,) => Promise<void> \| void` | - |
|
||||
| handleValuesChange | 表单值变化回调 | `(values: Record<string, any>, fieldsChanged: string[]) => void` | - |
|
||||
| handleCollapsedChange | 表单收起展开状态变化回调 | `(collapsed: boolean) => void` | - |
|
||||
| actionButtonsReverse | 调换操作按钮位置 | `boolean` | `false` |
|
||||
| resetButtonOptions | 重置按钮组件参数 | `ActionButtonOptions` | - |
|
||||
| submitButtonOptions | 提交按钮组件参数 | `ActionButtonOptions` | - |
|
||||
| showDefaultActions | 是否显示默认操作按钮 | `boolean` | `true` |
|
||||
| collapsed | 是否折叠,在`showCollapseButton`为`true`时生效 | `boolean` | `false` |
|
||||
| collapseTriggerResize | 折叠时,触发`resize`事件 | `boolean` | `false` |
|
||||
| collapsedRows | 折叠时保持的行数 | `number` | `1` |
|
||||
| fieldMappingTime | 用于将表单内的数组值映射成 2 个字段 | `[string, [string, string],Nullable<string>\|[string,string]\|((any,string)=>any)?][]` | - |
|
||||
| commonConfig | 表单项的通用配置,每个配置都会传递到每个表单项,表单项可覆盖 | `FormCommonConfig` | - |
|
||||
| schema | 表单项的每一项配置 | `FormSchema[]` | - |
|
||||
| submitOnEnter | 按下回车健时提交表单 | `boolean` | false |
|
||||
| submitOnChange | 字段值改变时提交表单(内部防抖,这个属性一般用于表格的搜索表单) | `boolean` | false |
|
||||
| compact | 是否紧凑模式(忽略为校验信息所预留的空间) | `boolean` | false |
|
||||
| scrollToFirstError | 表单验证失败时是否自动滚动到第一个错误字段 | `boolean` | false |
|
||||
|
||||
::: tip handleValuesChange
|
||||
|
||||
`handleValuesChange` 回调函数的第一个参数`values`装载了表单改变后的当前值对象,第二个参数`fieldsChanged`是一个数组,包含了所有被改变的字段名。注意:第二个参数仅在v5.5.4(不含)以上版本可用,并且传递的是已在schema中定义的字段名。如果你使用了字段映射并且需要检查是哪些字段发生了变化的话,请注意该参数并不会包含映射后的字段名。
|
||||
|
||||
:::
|
||||
|
||||
::: tip fieldMappingTime
|
||||
|
||||
此属性用于将表单内的数组值映射成 2 个字段,它应当传入一个数组,数组的每一项是一个映射规则,规则的第一个成员是一个字符串,表示需要映射的字段名,第二个成员是一个数组,表示映射后的字段名,第三个成员是一个可选的格式掩码,用于格式化日期时间字段;也可以提供一个格式化函数(参数分别为当前值和当前字段名,返回格式化后的值)。如果明确地将格式掩码设为null,则原值映射而不进行格式化(适用于非日期时间字段)。例如:`[['timeRange', ['startTime', 'endTime'], 'YYYY-MM-DD']]`,`timeRange`应当是一个至少具有2个成员的数组类型的值。Form会将`timeRange`的值前两个值分别按照格式掩码`YYYY-MM-DD`格式化后映射到`startTime`和`endTime`字段上。每一项的第三个参数是一个可选的格式掩码,
|
||||
|
||||
:::
|
||||
|
||||
### TS 类型说明
|
||||
|
||||
::: details ActionButtonOptions
|
||||
|
||||
```ts
|
||||
export interface ActionButtonOptions {
|
||||
/** 样式 */
|
||||
class?: ClassType;
|
||||
/** 是否禁用 */
|
||||
disabled?: boolean;
|
||||
/** 是否加载中 */
|
||||
loading?: boolean;
|
||||
/** 按钮大小 */
|
||||
size?: ButtonVariantSize;
|
||||
/** 按钮类型 */
|
||||
variant?: ButtonVariants;
|
||||
/** 是否显示 */
|
||||
show?: boolean;
|
||||
/** 按钮文本 */
|
||||
content?: string;
|
||||
/** 任意属性 */
|
||||
[key: string]: any;
|
||||
}
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
::: details FormCommonConfig
|
||||
|
||||
```ts
|
||||
export interface FormCommonConfig {
|
||||
/**
|
||||
* 所有表单项的props
|
||||
*/
|
||||
componentProps?: ComponentProps;
|
||||
/**
|
||||
* 所有表单项的控件样式
|
||||
*/
|
||||
controlClass?: string;
|
||||
/**
|
||||
* 在表单项的Label后显示一个冒号
|
||||
*/
|
||||
colon?: boolean;
|
||||
/**
|
||||
* 所有表单项的禁用状态
|
||||
* @default false
|
||||
*/
|
||||
disabled?: boolean;
|
||||
/**
|
||||
* 所有表单项的控件样式
|
||||
* @default {}
|
||||
*/
|
||||
formFieldProps?: Partial<typeof Field>;
|
||||
/**
|
||||
* 所有表单项的栅格布局
|
||||
* @default ""
|
||||
*/
|
||||
formItemClass?: (() => string) | string;
|
||||
/**
|
||||
* 隐藏所有表单项label
|
||||
* @default false
|
||||
*/
|
||||
hideLabel?: boolean;
|
||||
/**
|
||||
* 是否隐藏必填标记
|
||||
* @default false
|
||||
*/
|
||||
hideRequiredMark?: boolean;
|
||||
/**
|
||||
* 所有表单项的label样式
|
||||
* @default ""
|
||||
*/
|
||||
labelClass?: string;
|
||||
/**
|
||||
* 所有表单项的label宽度
|
||||
*/
|
||||
labelWidth?: number;
|
||||
/**
|
||||
* 所有表单项的model属性名。使用自定义组件时可通过此配置指定组件的model属性名。已经在modelPropNameMap中注册的组件不受此配置影响
|
||||
* @default "modelValue"
|
||||
*/
|
||||
modelPropName?: string;
|
||||
/**
|
||||
* 所有表单项的wrapper样式
|
||||
*/
|
||||
wrapperClass?: string;
|
||||
}
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
::: details FormSchema
|
||||
|
||||
```ts
|
||||
export interface FormSchema<
|
||||
T extends BaseFormComponentType = BaseFormComponentType,
|
||||
> extends FormCommonConfig {
|
||||
/** 组件 */
|
||||
component: Component | T;
|
||||
/** 组件参数 */
|
||||
componentProps?: ComponentProps;
|
||||
/** 默认值 */
|
||||
defaultValue?: any;
|
||||
/** 依赖 */
|
||||
dependencies?: FormItemDependencies;
|
||||
/** 描述 */
|
||||
description?: string;
|
||||
/** 字段名,也作为自定义插槽的名称 */
|
||||
fieldName: string;
|
||||
/** 帮助信息 */
|
||||
help?: CustomRenderType;
|
||||
/** 是否隐藏表单项 */
|
||||
hide?: boolean;
|
||||
/** 表单的标签(如果是一个string,会用于默认必选规则的消息提示) */
|
||||
label?: CustomRenderType;
|
||||
/** 自定义组件内部渲染 */
|
||||
renderComponentContent?: RenderComponentContentType;
|
||||
/** 字段规则 */
|
||||
rules?: FormSchemaRuleType;
|
||||
/** 后缀 */
|
||||
suffix?: CustomRenderType;
|
||||
}
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
### 表单联动
|
||||
|
||||
表单联动需要通过 schema 内的 `dependencies` 属性进行联动,允许您添加字段之间的依赖项,以根据其他字段的值控制字段。
|
||||
|
||||
```ts
|
||||
dependencies: {
|
||||
// 触发字段。只有这些字段值变动时,联动才会触发
|
||||
triggerFields: ['name'],
|
||||
// 动态判断当前字段是否需要显示,不显示则直接销毁
|
||||
if(values,formApi){},
|
||||
// 动态判断当前字段是否需要显示,不显示用css隐藏
|
||||
show(values,formApi){},
|
||||
// 动态判断当前字段是否需要禁用
|
||||
disabled(values,formApi){},
|
||||
// 字段变更时,都会触发该函数
|
||||
trigger(values,formApi){},
|
||||
// 动态rules
|
||||
rules(values,formApi){},
|
||||
// 动态必填
|
||||
required(values,formApi){},
|
||||
// 动态组件参数
|
||||
componentProps(values,formApi){},
|
||||
}
|
||||
```
|
||||
|
||||
### 表单校验
|
||||
|
||||
表单校验需要通过 schema 内的 `rules` 属性进行配置。
|
||||
|
||||
rules的值可以是字符串(预定义的校验规则名称),也可以是一个zod的schema。
|
||||
|
||||
#### 预定义的校验规则
|
||||
|
||||
```ts
|
||||
// 表示字段必填,默认会根据适配器的required进行国际化
|
||||
{
|
||||
rules: 'required';
|
||||
}
|
||||
|
||||
// 表示字段必填,默认会根据适配器的required进行国际化,用于下拉选择之类
|
||||
{
|
||||
rules: 'selectRequired';
|
||||
}
|
||||
```
|
||||
|
||||
#### zod
|
||||
|
||||
rules也支持 zod 的 schema,可以进行更复杂的校验,zod 的使用请查看 [zod文档](https://zod.dev/)。
|
||||
|
||||
```ts
|
||||
import { z } from '#/adapter/form';
|
||||
|
||||
// 基础类型
|
||||
{
|
||||
rules: z.string().min(1, { message: '请输入字符串' });
|
||||
}
|
||||
|
||||
// 可选(可以是undefined),并且携带默认值。注意zod的optional不包括空字符串''
|
||||
{
|
||||
rules: z.string().default('默认值').optional();
|
||||
}
|
||||
|
||||
// 可以是空字符串、undefined或者一个邮箱地址(两种不同的用法)
|
||||
{
|
||||
rules: z.union([z.string().email().optional(), z.literal('')]);
|
||||
}
|
||||
|
||||
{
|
||||
rules: z.string().email().or(z.literal('')).optional();
|
||||
}
|
||||
|
||||
// 复杂校验
|
||||
{
|
||||
z.string()
|
||||
.min(1, { message: '请输入' })
|
||||
.refine((value) => value === '123', {
|
||||
message: '值必须为123',
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Slots
|
||||
|
||||
可以使用以下插槽在表单中插入自定义的内容
|
||||
|
||||
| 插槽名 | 描述 |
|
||||
| ------------- | ------------------ |
|
||||
| reset-before | 重置按钮之前的位置 |
|
||||
| submit-before | 提交按钮之前的位置 |
|
||||
| expand-before | 展开按钮之前的位置 |
|
||||
| expand-after | 展开按钮之后的位置 |
|
||||
|
||||
::: tip 字段插槽
|
||||
|
||||
除了以上内置插槽之外,`schema`属性中每个字段的`fieldName`都可以作为插槽名称,这些字段插槽的优先级高于`component`定义的组件。也就是说,当提供了与`fieldName`同名的插槽时,这些插槽的内容将会作为这些字段的组件,此时`component`的值将会被忽略。
|
||||
|
||||
:::
|
||||
@ -1,174 +0,0 @@
|
||||
---
|
||||
outline: deep
|
||||
---
|
||||
|
||||
# Vben Modal 模态框
|
||||
|
||||
框架提供的模态框组件,支持`拖拽`、`全屏`、`自动高度`、`loading`等功能。
|
||||
|
||||
> 如果文档内没有参数说明,可以尝试在在线示例内寻找
|
||||
|
||||
::: info 写在前面
|
||||
|
||||
如果你觉得现有组件的封装不够理想,或者不完全符合你的需求,大可以直接使用原生组件,亦或亲手封装一个适合的组件。框架提供的组件并非束缚,使用与否,完全取决于你的需求与自由。
|
||||
|
||||
:::
|
||||
|
||||
::: tip README
|
||||
|
||||
下方示例代码中的,存在一些国际化、主题色未适配问题,这些问题只在文档内会出现,实际使用并不会有这些问题,可忽略,不必纠结。
|
||||
|
||||
:::
|
||||
|
||||
## 基础用法
|
||||
|
||||
使用 `useVbenModal` 创建最基础的模态框。
|
||||
|
||||
<DemoPreview dir="demos/vben-modal/basic" />
|
||||
|
||||
## 组件抽离
|
||||
|
||||
Modal 内的内容一般业务中,会比较复杂,所以我们可以将 modal 内的内容抽离出来,也方便复用。通过 `connectedComponent` 参数,可以将内外组件进行连接,而不用其他任何操作。
|
||||
|
||||
<DemoPreview dir="demos/vben-modal/extra" />
|
||||
|
||||
## 开启拖拽
|
||||
|
||||
通过 `draggable` 参数,可开启拖拽功能。
|
||||
|
||||
<DemoPreview dir="demos/vben-modal/draggable" />
|
||||
|
||||
## 自动计算高度
|
||||
|
||||
弹窗会自动计算内容高度,超过一定高度会出现滚动条,同时结合 `loading` 效果以及使用 `prepend-footer` 插槽。
|
||||
|
||||
<DemoPreview dir="demos/vben-modal/auto-height" />
|
||||
|
||||
## 使用 Api
|
||||
|
||||
通过 `modalApi` 可以调用 modal 的方法以及使用 `setState` 更新 modal 的状态。
|
||||
|
||||
<DemoPreview dir="demos/vben-modal/dynamic" />
|
||||
|
||||
## 数据共享
|
||||
|
||||
如果你使用了 `connectedComponent` 参数,那么内外组件会共享数据,比如一些表单回填等操作。可以用 `modalApi` 来获取数据和设置数据,配合 `onOpenChange`,可以满足大部分的需求。
|
||||
|
||||
<DemoPreview dir="demos/vben-modal/shared-data" />
|
||||
|
||||
## 动画类型
|
||||
|
||||
通过 `animationType` 属性可以控制弹窗的动画效果:
|
||||
|
||||
- `slide`(默认):从顶部向下滑动进入/退出
|
||||
- `scale`:缩放淡入/淡出效果
|
||||
|
||||
<DemoPreview dir="demos/vben-modal/animation-type" />
|
||||
|
||||
::: info 注意
|
||||
|
||||
- `VbenModal` 组件对与参数的处理优先级是 `slot` > `props` > `state`(通过api更新的状态以及useVbenModal参数)。如果你已经传入了 `slot` 或者 `props`,那么 `setState` 将不会生效,这种情况下你可以通过 `slot` 或者 `props` 来更新状态。
|
||||
- 如果你使用到了 `connectedComponent` 参数,那么会存在 2 个`useVbenModal`, 此时,如果同时设置了相同的参数,那么以内部为准(也就是没有设置 connectedComponent 的代码)。比如 同时设置了 `onConfirm`,那么以内部的 `onConfirm` 为准。`onOpenChange`事件除外,内外都会触发。另外,如果设置了`destroyOnClose`,内部Modal及其子组件会在被关闭后<b>完全销毁</b>。
|
||||
- 如果弹窗的默认行为不符合你的预期,可以在对应应用的 `apps/<app>/src/bootstrap.ts` 中修改 `setDefaultModalProps` 的参数来设置默认属性,例如默认隐藏全屏按钮、修改默认 `zIndex` 等。
|
||||
|
||||
:::
|
||||
|
||||
## API
|
||||
|
||||
```ts
|
||||
// Modal 为弹窗组件
|
||||
// modalApi 为弹窗的方法
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
// 属性
|
||||
// 事件
|
||||
});
|
||||
```
|
||||
|
||||
### Props
|
||||
|
||||
所有属性都可以传入 `useVbenModal` 的第一个参数中。
|
||||
|
||||
| 属性名 | 描述 | 类型 | 默认值 |
|
||||
| --- | --- | --- | --- |
|
||||
| appendToMain | 是否挂载到内容区域(默认挂载到body) | `boolean` | `false` |
|
||||
| connectedComponent | 连接另一个Modal组件 | `Component` | - |
|
||||
| destroyOnClose | 关闭时销毁 | `boolean` | `false` |
|
||||
| title | 标题 | `string\|slot` | - |
|
||||
| titleTooltip | 标题提示信息 | `string\|slot` | - |
|
||||
| description | 描述信息 | `string\|slot` | - |
|
||||
| isOpen | 弹窗打开状态 | `boolean` | `false` |
|
||||
| loading | 弹窗加载状态 | `boolean` | `false` |
|
||||
| fullscreen | 全屏显示 | `boolean` | `false` |
|
||||
| fullscreenButton | 显示全屏按钮 | `boolean` | `true` |
|
||||
| draggable | 可拖拽 | `boolean` | `false` |
|
||||
| closable | 显示关闭按钮 | `boolean` | `true` |
|
||||
| centered | 居中显示 | `boolean` | `false` |
|
||||
| modal | 显示遮罩 | `boolean` | `true` |
|
||||
| header | 显示header | `boolean` | `true` |
|
||||
| footer | 显示footer | `boolean\|slot` | `true` |
|
||||
| confirmDisabled | 禁用确认按钮 | `boolean` | `false` |
|
||||
| confirmLoading | 确认按钮loading状态 | `boolean` | `false` |
|
||||
| closeOnClickModal | 点击遮罩关闭弹窗 | `boolean` | `true` |
|
||||
| closeOnPressEscape | esc 关闭弹窗 | `boolean` | `true` |
|
||||
| confirmText | 确认按钮文本 | `string\|slot` | `确认` |
|
||||
| cancelText | 取消按钮文本 | `string\|slot` | `取消` |
|
||||
| showCancelButton | 显示取消按钮 | `boolean` | `true` |
|
||||
| showConfirmButton | 显示确认按钮 | `boolean` | `true` |
|
||||
| class | modal的class,宽度通过这个配置 | `string` | - |
|
||||
| contentClass | modal内容区域的class | `string` | - |
|
||||
| footerClass | modal底部区域的class | `string` | - |
|
||||
| headerClass | modal顶部区域的class | `string` | - |
|
||||
| bordered | 是否显示border | `boolean` | `false` |
|
||||
| zIndex | 弹窗的ZIndex层级 | `number` | `1000` |
|
||||
| overlayBlur | 遮罩模糊度 | `number` | - |
|
||||
| animationType | 动画类型 | `'slide' \| 'scale'` | `'slide'` |
|
||||
| submitting | 标记为提交中,锁定弹窗当前状态 | `boolean` | `false` |
|
||||
|
||||
::: info appendToMain
|
||||
|
||||
`appendToMain`可以指定将弹窗挂载到内容区域,打开这种弹窗时,内容区域以外的部分(标签栏、导航菜单等等)不会被遮挡。默认情况下,弹窗会挂载到body上。但是:挂载到内容区域时,作为页面根容器的`Page`组件,需要设置`auto-content-height`属性,以便弹窗能够正确计算高度。
|
||||
|
||||
:::
|
||||
|
||||
### Event
|
||||
|
||||
以下事件,只有在 `useVbenModal({onCancel:()=>{}})` 中传入才会生效。
|
||||
|
||||
| 事件名 | 描述 | 类型 | 版本号 |
|
||||
| --- | --- | --- | --- |
|
||||
| onBeforeClose | 关闭前触发,返回 `false`或者被`reject`则禁止关闭 | `()=>Promise<boolean>\|boolean` | >5.5.2支持Promise |
|
||||
| onCancel | 点击取消按钮触发 | `()=>void` | |
|
||||
| onClosed | 关闭动画播放完毕时触发 | `()=>void` | >5.4.3 |
|
||||
| onConfirm | 点击确认按钮触发 | `()=>void` | |
|
||||
| onOpenChange | 关闭或者打开弹窗时触发 | `(isOpen:boolean)=>void` | |
|
||||
| onOpened | 打开动画播放完毕时触发 | `()=>void` | >5.4.3 |
|
||||
|
||||
### Slots
|
||||
|
||||
除了上面的属性类型包含`slot`,还可以通过插槽来自定义弹窗的内容。
|
||||
|
||||
| 插槽名 | 描述 |
|
||||
| -------------- | -------------------------------------------------- |
|
||||
| default | 默认插槽 - 弹窗内容 |
|
||||
| prepend-footer | 取消按钮左侧 |
|
||||
| center-footer | 取消按钮和确认按钮中间(不使用 footer 插槽时有效) |
|
||||
| append-footer | 确认按钮右侧 |
|
||||
|
||||
### modalApi
|
||||
|
||||
| 方法 | 描述 | 类型 | 版本 |
|
||||
| --- | --- | --- | --- |
|
||||
| setState | 动态设置弹窗状态属性 | `(((prev: ModalState) => Partial<ModalState>)\| Partial<ModalState>)=>modalApi` | - |
|
||||
| open | 打开弹窗 | `()=>void` | - |
|
||||
| close | 关闭弹窗 | `()=>void` | - |
|
||||
| setData | 设置共享数据 | `<T>(data:T)=>modalApi` | - |
|
||||
| getData | 获取共享数据 | `<T>()=>T` | - |
|
||||
| useStore | 获取可响应式状态 | - | - |
|
||||
| lock | 将弹窗标记为提交中,锁定当前状态 | `(isLock:boolean)=>modalApi` | >5.5.2 |
|
||||
| unlock | lock方法的反操作,解除弹窗的锁定状态,也是lock(false)的别名 | `()=>modalApi` | >5.5.3 |
|
||||
|
||||
::: info lock
|
||||
|
||||
`lock`方法用于锁定当前弹窗的状态,一般用于提交数据的过程中防止用户重复提交或者弹窗被意外关闭、表单数据被改变等等。当处于锁定状态时,弹窗的确认按钮会变为loading状态,同时禁用取消按钮和关闭按钮、禁止ESC或者点击遮罩等方式关闭弹窗、开启弹窗的spinner动画以遮挡弹窗内容。调用`close`方法关闭处于锁定状态的弹窗时,会自动解锁。要主动解除这种状态,可以调用`unlock`方法或者再次调用lock方法并传入false参数。
|
||||
|
||||
:::
|
||||
@ -1,249 +0,0 @@
|
||||
---
|
||||
outline: deep
|
||||
---
|
||||
|
||||
# Vben Vxe Table 表格
|
||||
|
||||
`Vben Vxe Table` 基于 [vxe-table](https://vxetable.cn/v4/#/grid/api?apiKey=grid) 和 `Vben Form` 做了二次封装,用于构建带搜索表单的列表页面。
|
||||
|
||||
> 如果文档内没有覆盖到你需要的细节,可以结合在线示例和 [vxe-grid 官方 API](https://vxetable.cn/v4/#/grid/api?apiKey=grid) 一起查看。
|
||||
|
||||
::: info 写在前面
|
||||
|
||||
如果现有封装不满足你的场景,可以直接使用原生 `vxe-table` 能力,或者在适配层中继续扩展。:::
|
||||
|
||||
## 适配器
|
||||
|
||||
底层表格基于 `vxe-table`,每个应用都可以在自己的适配层中配置默认行为、自定义渲染器以及与 UI 组件库的集成。
|
||||
|
||||
### 适配器示例
|
||||
|
||||
::: details vxe-table 表格适配器
|
||||
|
||||
```ts
|
||||
import { h } from 'vue';
|
||||
|
||||
import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table';
|
||||
|
||||
import { Button, Image } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm } from './form';
|
||||
|
||||
setupVbenVxeTable({
|
||||
configVxeTable: (vxeUI) => {
|
||||
vxeUI.setConfig({
|
||||
grid: {
|
||||
align: 'center',
|
||||
border: false,
|
||||
columnConfig: {
|
||||
resizable: true,
|
||||
},
|
||||
minHeight: 180,
|
||||
formConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
proxyConfig: {
|
||||
autoLoad: true,
|
||||
response: {
|
||||
result: 'items',
|
||||
total: 'total',
|
||||
list: 'items',
|
||||
},
|
||||
showActiveMsg: true,
|
||||
showResponseMsg: false,
|
||||
},
|
||||
round: true,
|
||||
showOverflow: true,
|
||||
size: 'small',
|
||||
},
|
||||
});
|
||||
|
||||
vxeUI.renderer.add('CellImage', {
|
||||
renderTableDefault(_renderOpts, params) {
|
||||
const { column, row } = params;
|
||||
return h(Image, { src: row[column.field] });
|
||||
},
|
||||
});
|
||||
|
||||
vxeUI.renderer.add('CellLink', {
|
||||
renderTableDefault(renderOpts) {
|
||||
const { props } = renderOpts;
|
||||
return h(
|
||||
Button,
|
||||
{ size: 'small', type: 'link' },
|
||||
{ default: () => props?.text },
|
||||
);
|
||||
},
|
||||
});
|
||||
},
|
||||
useVbenForm,
|
||||
});
|
||||
|
||||
export { useVbenVxeGrid };
|
||||
|
||||
export type * from '@vben/plugins/vxe-table';
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
## 基础表格
|
||||
|
||||
通过 `useVbenVxeGrid` 创建一个基础表格。
|
||||
|
||||
<DemoPreview dir="demos/vben-vxe-table/basic" />
|
||||
|
||||
## 远程加载
|
||||
|
||||
通过配置 `proxyConfig.ajax.query` 实现远程数据加载。
|
||||
|
||||
<DemoPreview dir="demos/vben-vxe-table/remote" />
|
||||
|
||||
## 树形表格
|
||||
|
||||
树形表格的数据源通常是扁平结构,可以通过 `treeConfig` 转换为树形展示。
|
||||
|
||||
```ts
|
||||
treeConfig: {
|
||||
transform: true,
|
||||
parentField: 'parentId',
|
||||
rowField: 'id',
|
||||
},
|
||||
```
|
||||
|
||||
<DemoPreview dir="demos/vben-vxe-table/tree" />
|
||||
|
||||
## 固定列
|
||||
|
||||
固定列可选值为 `'left' | 'right' | '' | null`。
|
||||
|
||||
<DemoPreview dir="demos/vben-vxe-table/fixed" />
|
||||
|
||||
## 自定义单元格
|
||||
|
||||
可以通过插槽或自定义渲染器实现单元格定制。
|
||||
|
||||
```ts
|
||||
vxeUI.renderer.add('CellImage', {
|
||||
renderTableDefault(_renderOpts, params) {
|
||||
const { column, row } = params;
|
||||
return h(Image, { src: row[column.field] } as any);
|
||||
},
|
||||
});
|
||||
|
||||
vxeUI.renderer.add('CellLink', {
|
||||
renderTableDefault(renderOpts) {
|
||||
const { props } = renderOpts;
|
||||
return h(
|
||||
Button,
|
||||
{ size: 'small', type: 'link' },
|
||||
{ default: () => props?.text },
|
||||
);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
<DemoPreview dir="demos/vben-vxe-table/custom-cell" />
|
||||
|
||||
## 搜索表单
|
||||
|
||||
搜索区域底层使用的是 `Vben Form`。启用搜索表单后,可以通过 `gridOptions.toolbarConfig.search = true` 在工具栏中显示搜索面板开关按钮。
|
||||
|
||||
所有以 `form-` 开头的具名插槽都会自动转发到搜索表单。
|
||||
|
||||
### 自定义分隔条
|
||||
|
||||
启用搜索表单时,表单和表格主体之间默认会显示一个分隔条。可以通过 `separator` 调整或关闭它。
|
||||
|
||||
```ts
|
||||
const [Grid] = useVbenVxeGrid({
|
||||
formOptions: {},
|
||||
gridOptions: {},
|
||||
separator: false,
|
||||
// separator: { show: false },
|
||||
// separator: { backgroundColor: 'rgba(100,100,0,0.5)' },
|
||||
});
|
||||
```
|
||||
|
||||
<DemoPreview dir="demos/vben-vxe-table/form" />
|
||||
|
||||
## 单元格编辑
|
||||
|
||||
通过设置 `editConfig.mode = 'cell'` 开启单元格编辑。
|
||||
|
||||
<DemoPreview dir="demos/vben-vxe-table/edit-cell" />
|
||||
|
||||
## 行编辑
|
||||
|
||||
通过设置 `editConfig.mode = 'row'` 开启整行编辑。
|
||||
|
||||
<DemoPreview dir="demos/vben-vxe-table/edit-row" />
|
||||
|
||||
## 虚拟滚动
|
||||
|
||||
通过 `scroll-y.enabled` 和 `scroll-y.gt` 组合开启纵向虚拟滚动。
|
||||
|
||||
> 参考 [vxe-table 官方文档 - 虚拟滚动](https://vxetable.cn/v4/#/component/grid/scroll/vertical)
|
||||
|
||||
<DemoPreview dir="demos/vben-vxe-table/virtual" />
|
||||
|
||||
## API
|
||||
|
||||
`useVbenVxeGrid` 返回一个数组,第一个元素是表格组件,第二个元素是表格 API。
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
gridOptions: {},
|
||||
formOptions: {},
|
||||
gridEvents: {},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Grid />
|
||||
</template>
|
||||
```
|
||||
|
||||
### GridApi
|
||||
|
||||
| 方法名 | 描述 | 类型 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| setLoading | 设置 loading 状态 | `(loading: boolean) => void` | - |
|
||||
| setGridOptions | 更新 `gridOptions` | `(options: Partial<VxeGridProps['gridOptions']>) => void` | - |
|
||||
| reload | 重新加载表格,并重置到初始分页 | `(params?: Record<string, any>) => void` | - |
|
||||
| query | 重新查询表格,保留当前分页 | `(params?: Record<string, any>) => void` | - |
|
||||
| grid | `vxe-grid` 实例 | `VxeGridInstance` | - |
|
||||
| formApi | 搜索表单 API 实例 | `FormApi` | - |
|
||||
| toggleSearchForm | 切换或指定搜索表单显示状态 | `(show?: boolean) => boolean` | 传入参数时强制设置;不传参数时在显示和隐藏之间切换,并返回当前状态 |
|
||||
|
||||
## Props
|
||||
|
||||
所有属性都通过 `useVbenVxeGrid` 的第一个参数传入。
|
||||
|
||||
| 属性名 | 描述 | 类型 | 版本要求 |
|
||||
| --- | --- | --- | --- |
|
||||
| tableTitle | 表格标题 | `string` | - |
|
||||
| tableTitleHelp | 表格标题帮助信息 | `string` | - |
|
||||
| class | 外层容器的 class | `string` | - |
|
||||
| gridClass | `vxe-grid` 的 class | `string` | - |
|
||||
| gridOptions | `vxe-grid` 配置 | `DeepPartial<VxeTableGridOptions>` | - |
|
||||
| gridEvents | `vxe-grid` 事件 | `DeepPartial<VxeGridListeners>` | - |
|
||||
| formOptions | 搜索表单配置 | `VbenFormProps` | - |
|
||||
| showSearchForm | 是否显示搜索表单 | `boolean` | - |
|
||||
| separator | 搜索表单与表格主体之间的分隔条 | `boolean \| SeparatorOptions` | `>5.5.4` |
|
||||
|
||||
## Slots
|
||||
|
||||
大部分插槽说明可参考 [vxe-table 官方文档](https://vxetable.cn/v4/#/grid/api),这里列出封装层新增或约定的部分。
|
||||
|
||||
| 插槽名 | 描述 |
|
||||
| --------------- | ------------------------------------ |
|
||||
| toolbar-actions | 工具栏左侧区域,位于标题附近 |
|
||||
| toolbar-tools | 工具栏右侧区域,位于内置工具按钮左侧 |
|
||||
| table-title | 自定义表格标题 |
|
||||
|
||||
::: info 搜索表单插槽
|
||||
|
||||
当启用了搜索表单时,所有以 `form-` 开头的具名插槽都会被转发给表单。:::
|
||||
@ -1,15 +0,0 @@
|
||||
# 介绍
|
||||
|
||||
::: info README
|
||||
|
||||
该文档介绍的是框架组件的使用方法、属性、事件等。如果你觉得现有组件的封装不够理想,或者不完全符合你的需求,大可以直接使用原生组件,亦或亲手封装一个适合的组件。框架提供的组件并非束缚,使用与否,完全取决于你的需求与自由。
|
||||
|
||||
:::
|
||||
|
||||
## 布局组件
|
||||
|
||||
布局组件一般在页面内容区域用作顶层容器组件,提供一些统一的布局样式和基本功能。
|
||||
|
||||
## 通用组件
|
||||
|
||||
通用组件是一些常用的组件,比如弹窗、抽屉、表单等。大部分基于 `Tailwind CSS` 实现,可适用于不同 UI 组件库的应用。
|
||||
@ -1,41 +0,0 @@
|
||||
---
|
||||
outline: deep
|
||||
---
|
||||
|
||||
# Page 常规页面组件
|
||||
|
||||
`Page` 是页面内容区最常用的顶层布局容器,内置了标题区、内容区和底部区三部分结构。
|
||||
|
||||
::: info 写在前面
|
||||
|
||||
这是一个基础页面容器。如果你的业务页面需要更复杂的布局,例如双列、侧边操作区或自定义滚动区域,建议在 `Page` 之上继续封装。:::
|
||||
|
||||
## 基础用法
|
||||
|
||||
直接将 `Page` 作为业务页面的根组件使用即可。
|
||||
|
||||
### Props
|
||||
|
||||
| 属性名 | 描述 | 类型 | 默认值 | 说明 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| title | 页面标题 | `string \| slot` | - | - |
|
||||
| description | 页面描述 | `string \| slot` | - | - |
|
||||
| contentClass | 内容区域的 class | `string` | - | - |
|
||||
| headerClass | 头部区域的 class | `string` | - | - |
|
||||
| footerClass | 底部区域的 class | `string` | - | - |
|
||||
| autoContentHeight | 根据可视内容高度自动计算内容区高度 | `boolean` | `false` | 开启后内容区会根据布局可视高度自动扣减头部和底部高度 |
|
||||
| heightOffset | 额外扣减的内容区高度偏移量 | `number` | `0` | 仅在 `autoContentHeight` 开启时生效,单位为 `px` |
|
||||
|
||||
::: tip 注意
|
||||
|
||||
如果 `title`、`description`、`extra` 三者都没有提供有效内容,无论是通过 `props` 还是 `slots`,头部区域都不会渲染。:::
|
||||
|
||||
### Slots
|
||||
|
||||
| 插槽名称 | 描述 |
|
||||
| ----------- | ---------------- |
|
||||
| default | 页面内容 |
|
||||
| title | 页面标题 |
|
||||
| description | 页面描述 |
|
||||
| extra | 页面头部右侧内容 |
|
||||
| footer | 页面底部内容 |
|
||||
@ -1,36 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { h } from 'vue';
|
||||
|
||||
import { alert, VbenButton } from '@vben/common-ui';
|
||||
|
||||
import { Result } from 'ant-design-vue';
|
||||
|
||||
function showAlert() {
|
||||
alert('This is an alert message');
|
||||
}
|
||||
|
||||
function showIconAlert() {
|
||||
alert({
|
||||
content: 'This is an alert message with icon',
|
||||
icon: 'success',
|
||||
});
|
||||
}
|
||||
|
||||
function showCustomAlert() {
|
||||
alert({
|
||||
buttonAlign: 'center',
|
||||
content: h(Result, {
|
||||
status: 'success',
|
||||
subTitle: '已成功创建订单。订单ID:2017182818828182881',
|
||||
title: '操作成功',
|
||||
}),
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div class="flex gap-4">
|
||||
<VbenButton @click="showAlert">Alert</VbenButton>
|
||||
<VbenButton @click="showIconAlert">Alert With Icon</VbenButton>
|
||||
<VbenButton @click="showCustomAlert">Alert With Custom Content</VbenButton>
|
||||
</div>
|
||||
</template>
|
||||
@ -1,75 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { h, ref } from 'vue';
|
||||
|
||||
import { alert, confirm, VbenButton } from '@vben/common-ui';
|
||||
|
||||
import { Checkbox, message } from 'ant-design-vue';
|
||||
|
||||
function showConfirm() {
|
||||
confirm('This is an alert message')
|
||||
.then(() => {
|
||||
alert('Confirmed');
|
||||
})
|
||||
.catch(() => {
|
||||
alert('Canceled');
|
||||
});
|
||||
}
|
||||
|
||||
function showIconConfirm() {
|
||||
confirm({
|
||||
content: 'This is an alert message with icon',
|
||||
icon: 'success',
|
||||
});
|
||||
}
|
||||
|
||||
function showfooterConfirm() {
|
||||
const checked = ref(false);
|
||||
confirm({
|
||||
cancelText: '不要虾扯蛋',
|
||||
confirmText: '是的,我们都是NPC',
|
||||
content:
|
||||
'刚才发生的事情,为什么我似乎早就经历过一般?\n我甚至能在事情发生过程中潜意识里预知到接下来会发生什么。\n\n听起来挺玄乎的,你有过这种感觉吗?',
|
||||
footer: () =>
|
||||
h(
|
||||
Checkbox,
|
||||
{
|
||||
checked: checked.value,
|
||||
class: 'flex-1',
|
||||
'onUpdate:checked': (v) => (checked.value = v),
|
||||
},
|
||||
'不再提示',
|
||||
),
|
||||
icon: 'question',
|
||||
title: '未解之谜',
|
||||
}).then(() => {
|
||||
if (checked.value) {
|
||||
message.success('我不会再拿这个问题烦你了');
|
||||
} else {
|
||||
message.info('下次还要继续问你哟');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function showAsyncConfirm() {
|
||||
confirm({
|
||||
beforeClose({ isConfirm }) {
|
||||
if (isConfirm) {
|
||||
// 这里可以执行一些异步操作。如果最终返回了false,将阻止关闭弹窗
|
||||
return new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
}
|
||||
},
|
||||
content: 'This is an alert message with async confirm',
|
||||
icon: 'success',
|
||||
}).then(() => {
|
||||
alert('Confirmed');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div class="flex gap-4">
|
||||
<VbenButton @click="showConfirm">Confirm</VbenButton>
|
||||
<VbenButton @click="showIconConfirm">Confirm With Icon</VbenButton>
|
||||
<VbenButton @click="showfooterConfirm">Confirm With Footer</VbenButton>
|
||||
<VbenButton @click="showAsyncConfirm">Async Confirm</VbenButton>
|
||||
</div>
|
||||
</template>
|
||||
@ -1,118 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { h } from 'vue';
|
||||
|
||||
import { alert, prompt, useAlertContext, VbenButton } from '@vben/common-ui';
|
||||
|
||||
import { Input, RadioGroup, Select } from 'ant-design-vue';
|
||||
import { BadgeJapaneseYen } from 'lucide-vue-next';
|
||||
|
||||
function showPrompt() {
|
||||
prompt({
|
||||
content: '请输入一些东西',
|
||||
})
|
||||
.then((val) => {
|
||||
alert(`已收到你的输入:${val}`);
|
||||
})
|
||||
.catch(() => {
|
||||
alert('Canceled');
|
||||
});
|
||||
}
|
||||
|
||||
function showSlotsPrompt() {
|
||||
prompt({
|
||||
component: () => {
|
||||
// 获取弹窗上下文。注意:只能在setup或者函数式组件中调用
|
||||
const { doConfirm } = useAlertContext();
|
||||
return h(
|
||||
Input,
|
||||
{
|
||||
onKeydown(e: KeyboardEvent) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
// 调用弹窗提供的确认方法
|
||||
doConfirm();
|
||||
}
|
||||
},
|
||||
placeholder: '请输入',
|
||||
prefix: '充值金额:',
|
||||
type: 'number',
|
||||
},
|
||||
{
|
||||
addonAfter: () => h(BadgeJapaneseYen),
|
||||
},
|
||||
);
|
||||
},
|
||||
content:
|
||||
'此弹窗演示了如何使用自定义插槽,并且可以使用useAlertContext获取到弹窗的上下文。\n在输入框中按下回车键会触发确认操作。',
|
||||
icon: 'question',
|
||||
modelPropName: 'value',
|
||||
}).then((val) => {
|
||||
if (val) alert(`你输入的是${val}`);
|
||||
});
|
||||
}
|
||||
|
||||
function showSelectPrompt() {
|
||||
prompt({
|
||||
component: Select,
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: 'Option A', value: 'Option A' },
|
||||
{ label: 'Option B', value: 'Option B' },
|
||||
{ label: 'Option C', value: 'Option C' },
|
||||
],
|
||||
placeholder: '请选择',
|
||||
// 弹窗会设置body的pointer-events为none,这回影响下拉框的点击事件
|
||||
popupClassName: 'pointer-events-auto',
|
||||
},
|
||||
content: '此弹窗演示了如何使用component传递自定义组件',
|
||||
icon: 'question',
|
||||
modelPropName: 'value',
|
||||
}).then((val) => {
|
||||
if (val) {
|
||||
alert(`你选择了${val}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function sleep(ms: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
function showAsyncPrompt() {
|
||||
prompt({
|
||||
async beforeClose(scope) {
|
||||
if (scope.isConfirm) {
|
||||
if (scope.value) {
|
||||
// 模拟异步操作,如果不成功,可以返回false
|
||||
await sleep(2000);
|
||||
} else {
|
||||
alert('请选择一个选项');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
},
|
||||
component: RadioGroup,
|
||||
componentProps: {
|
||||
class: 'flex flex-col',
|
||||
options: [
|
||||
{ label: 'Option 1', value: 'option1' },
|
||||
{ label: 'Option 2', value: 'option2' },
|
||||
{ label: 'Option 3', value: 'option3' },
|
||||
],
|
||||
},
|
||||
content: '选择一个选项后再点击[确认]',
|
||||
icon: 'question',
|
||||
modelPropName: 'value',
|
||||
}).then((val) => {
|
||||
alert(`${val} 已设置。`);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div class="flex gap-4">
|
||||
<VbenButton @click="showPrompt">Prompt</VbenButton>
|
||||
<VbenButton @click="showSlotsPrompt"> Prompt With slots </VbenButton>
|
||||
<VbenButton @click="showSelectPrompt">Prompt With Select</VbenButton>
|
||||
<VbenButton @click="showAsyncPrompt">Prompt With Async</VbenButton>
|
||||
</div>
|
||||
</template>
|
||||
@ -1,100 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { ApiComponent } from '@vben/common-ui';
|
||||
|
||||
import { Cascader } from 'ant-design-vue';
|
||||
|
||||
const treeData: Record<string, any> = [
|
||||
{
|
||||
label: '浙江',
|
||||
value: 'zhejiang',
|
||||
children: [
|
||||
{
|
||||
value: 'hangzhou',
|
||||
label: '杭州',
|
||||
children: [
|
||||
{
|
||||
value: 'xihu',
|
||||
label: '西湖',
|
||||
},
|
||||
{
|
||||
value: 'sudi',
|
||||
label: '苏堤',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'jiaxing',
|
||||
label: '嘉兴',
|
||||
children: [
|
||||
{
|
||||
value: 'wuzhen',
|
||||
label: '乌镇',
|
||||
},
|
||||
{
|
||||
value: 'meihuazhou',
|
||||
label: '梅花洲',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'zhoushan',
|
||||
label: '舟山',
|
||||
children: [
|
||||
{
|
||||
value: 'putuoshan',
|
||||
label: '普陀山',
|
||||
},
|
||||
{
|
||||
value: 'taohuadao',
|
||||
label: '桃花岛',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '江苏',
|
||||
value: 'jiangsu',
|
||||
children: [
|
||||
{
|
||||
value: 'nanjing',
|
||||
label: '南京',
|
||||
children: [
|
||||
{
|
||||
value: 'zhonghuamen',
|
||||
label: '中华门',
|
||||
},
|
||||
{
|
||||
value: 'zijinshan',
|
||||
label: '紫金山',
|
||||
},
|
||||
{
|
||||
value: 'yuhuatai',
|
||||
label: '雨花台',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
/**
|
||||
* 模拟请求接口
|
||||
*/
|
||||
function fetchApi(): Promise<Record<string, any>> {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve(treeData);
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<ApiComponent
|
||||
:api="fetchApi"
|
||||
:component="Cascader"
|
||||
:immediate="false"
|
||||
children-field="children"
|
||||
loading-slot="suffixIcon"
|
||||
visible-event="onDropdownVisibleChange"
|
||||
/>
|
||||
</template>
|
||||
@ -1,6 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { VbenCountToAnimator } from '@vben/common-ui';
|
||||
</script>
|
||||
<template>
|
||||
<VbenCountToAnimator :duration="3000" :end-val="30000" :start-val="1" />
|
||||
</template>
|
||||
@ -1,12 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { VbenCountToAnimator } from '@vben/common-ui';
|
||||
</script>
|
||||
<template>
|
||||
<VbenCountToAnimator
|
||||
:duration="3000"
|
||||
:end-val="2000000"
|
||||
:start-val="1"
|
||||
prefix="$"
|
||||
separator="/"
|
||||
/>
|
||||
</template>
|
||||
@ -1,45 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenDrawer, VbenButton } from '@vben/common-ui';
|
||||
|
||||
const list = ref<number[]>([]);
|
||||
|
||||
const [Drawer, drawerApi] = useVbenDrawer({
|
||||
onCancel() {
|
||||
drawerApi.close();
|
||||
},
|
||||
onConfirm() {
|
||||
console.log('onConfirm');
|
||||
},
|
||||
onOpenChange(isOpen) {
|
||||
if (isOpen) {
|
||||
handleUpdate(10);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
function handleUpdate(len: number) {
|
||||
drawerApi.setState({ loading: true });
|
||||
setTimeout(() => {
|
||||
list.value = Array.from({ length: len }, (_v, k) => k + 1);
|
||||
drawerApi.setState({ loading: false });
|
||||
}, 2000);
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<Drawer title="自动计算高度">
|
||||
<div
|
||||
v-for="item in list"
|
||||
:key="item"
|
||||
class="flex-center h-55 w-full bg-muted even:bg-heavy"
|
||||
>
|
||||
{{ item }}
|
||||
</div>
|
||||
<template #prepend-footer>
|
||||
<VbenButton type="link" @click="handleUpdate(6)">
|
||||
点击更新数据
|
||||
</VbenButton>
|
||||
</template>
|
||||
</Drawer>
|
||||
</template>
|
||||
@ -1,21 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { useVbenDrawer, VbenButton } from '@vben/common-ui';
|
||||
|
||||
import ExtraDrawer from './drawer.vue';
|
||||
|
||||
const [Drawer, drawerApi] = useVbenDrawer({
|
||||
// 连接抽离的组件
|
||||
connectedComponent: ExtraDrawer,
|
||||
});
|
||||
|
||||
function open() {
|
||||
drawerApi.open();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<Drawer />
|
||||
<VbenButton @click="open">Open</VbenButton>
|
||||
</div>
|
||||
</template>
|
||||
@ -1,11 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { useVbenDrawer, VbenButton } from '@vben/common-ui';
|
||||
|
||||
const [Drawer, drawerApi] = useVbenDrawer();
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<VbenButton @click="() => drawerApi.open()">Open</VbenButton>
|
||||
<Drawer class="w-150" title="基础示例"> drawer content </Drawer>
|
||||
</div>
|
||||
</template>
|
||||
@ -1,26 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { useVbenDrawer, VbenButton } from '@vben/common-ui';
|
||||
|
||||
const [Drawer, drawerApi] = useVbenDrawer({
|
||||
onCancel() {
|
||||
drawerApi.close();
|
||||
},
|
||||
onConfirm() {
|
||||
console.info('onConfirm');
|
||||
},
|
||||
title: '动态修改配置示例',
|
||||
});
|
||||
|
||||
function handleUpdateTitle() {
|
||||
drawerApi.setState({ title: '内部动态标题' });
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<Drawer>
|
||||
<div class="flex-col-center">
|
||||
<VbenButton class="mb-3" type="primary" @click="handleUpdateTitle()">
|
||||
内部动态修改标题
|
||||
</VbenButton>
|
||||
</div>
|
||||
</Drawer>
|
||||
</template>
|
||||
@ -1,29 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { useVbenDrawer, VbenButton } from '@vben/common-ui';
|
||||
|
||||
import ExtraDrawer from './drawer.vue';
|
||||
|
||||
const [Drawer, drawerApi] = useVbenDrawer({
|
||||
// 连接抽离的组件
|
||||
connectedComponent: ExtraDrawer,
|
||||
});
|
||||
|
||||
function open() {
|
||||
drawerApi.open();
|
||||
}
|
||||
|
||||
function handleUpdateTitle() {
|
||||
drawerApi.setState({ title: '外部动态标题' }).open();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<Drawer />
|
||||
|
||||
<VbenButton @click="open">Open</VbenButton>
|
||||
<VbenButton class="ml-2" type="primary" @click="handleUpdateTitle">
|
||||
从外部修改标题并打开
|
||||
</VbenButton>
|
||||
</div>
|
||||
</template>
|
||||
@ -1,8 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { useVbenDrawer } from '@vben/common-ui';
|
||||
|
||||
const [Drawer] = useVbenDrawer();
|
||||
</script>
|
||||
<template>
|
||||
<Drawer title="组件抽离示例"> extra drawer content </Drawer>
|
||||
</template>
|
||||
@ -1,21 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { useVbenDrawer, VbenButton } from '@vben/common-ui';
|
||||
|
||||
import ExtraDrawer from './drawer.vue';
|
||||
|
||||
const [Drawer, drawerApi] = useVbenDrawer({
|
||||
// 连接抽离的组件
|
||||
connectedComponent: ExtraDrawer,
|
||||
});
|
||||
|
||||
function open() {
|
||||
drawerApi.open();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<Drawer />
|
||||
<VbenButton @click="open">Open</VbenButton>
|
||||
</div>
|
||||
</template>
|
||||
@ -1,26 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenDrawer } from '@vben/common-ui';
|
||||
|
||||
const data = ref();
|
||||
|
||||
const [Drawer, drawerApi] = useVbenDrawer({
|
||||
onCancel() {
|
||||
drawerApi.close();
|
||||
},
|
||||
onConfirm() {
|
||||
console.info('onConfirm');
|
||||
},
|
||||
onOpenChange(isOpen: boolean) {
|
||||
if (isOpen) {
|
||||
data.value = drawerApi.getData<Record<string, any>>();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<Drawer title="数据共享示例">
|
||||
<div class="flex-col-center">外部传递数据: {{ data }}</div>
|
||||
</Drawer>
|
||||
</template>
|
||||
@ -1,27 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { useVbenDrawer, VbenButton } from '@vben/common-ui';
|
||||
|
||||
import ExtraDrawer from './drawer.vue';
|
||||
|
||||
const [Drawer, drawerApi] = useVbenDrawer({
|
||||
// 连接抽离的组件
|
||||
connectedComponent: ExtraDrawer,
|
||||
});
|
||||
|
||||
function open() {
|
||||
drawerApi
|
||||
.setData({
|
||||
content: '外部传递的数据 content',
|
||||
payload: '外部传递的数据 payload',
|
||||
})
|
||||
.open();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<Drawer />
|
||||
|
||||
<VbenButton @click="open">Open</VbenButton>
|
||||
</div>
|
||||
</template>
|
||||
@ -1,16 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { EllipsisText } from '@vben/common-ui';
|
||||
|
||||
const text = `
|
||||
Vben Admin 是一个基于 Vue3.0、Vite、 TypeScript 的后台解决方案,目标是为开发中大型项目提供开箱即用的解决方案。
|
||||
`;
|
||||
</script>
|
||||
<template>
|
||||
<EllipsisText :line="2" :tooltip-when-ellipsis="true">
|
||||
{{ text }}
|
||||
</EllipsisText>
|
||||
|
||||
<EllipsisText :line="3" :tooltip-when-ellipsis="true">
|
||||
{{ text }}
|
||||
</EllipsisText>
|
||||
</template>
|
||||
@ -1,10 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { EllipsisText } from '@vben/common-ui';
|
||||
|
||||
const text = `
|
||||
Vben Admin 是一个基于 Vue3.0、Vite、 TypeScript 的后台解决方案,目标是为开发中大型项目提供开箱即用的解决方案。包括二次封装组件、utils、hooks、动态菜单、权限校验、多主题配置、按钮级别权限控制等功能。项目会使用前端较新的技术栈,可以作为项目的启动模版,以帮助你快速搭建企业级中后台产品原型。也可以作为一个示例,用于学习 vue3、vite、ts 等主流技术。该项目会持续跟进最新技术,并将其应用在项目中。Vben Admin 是一个基于 Vue3.0、Vite、 TypeScript 的后台解决方案,目标是为开发中大型项目提供开箱即用的解决方案。包括二次封装组件、utils、hooks、动态菜单、权限校验、多主题配置、按钮级别权限控制等功能。项目会使用前端较新的技术栈,可以作为项目的启动模版,以帮助你快速搭建企业级中后台产品原型。也可以作为一个示例,用于学习 vue3、vite、ts 等主流技术。该项目会持续跟进最新技术,并将其应用在项目中。Vben Admin 是一个基于 Vue3.0、Vite、 TypeScript 的后台解决方案,目标是为开发中大型项目提供开箱即用的解决方案。包括二次封装组件、utils、hooks、动态菜单、权限校验、多主题配置、按钮级别权限控制等功能。项目会使用前端较新的技术栈,可以作为项目的启动模版,以帮助你快速搭建企业级中后台产品原型。也可以作为一个示例,用于学习 vue3、vite、ts 等主流技术。该项目会持续跟进最新技术,并将其应用在项目中。Vben Admin 是一个基于 Vue3.0、Vite、 TypeScript 的后台解决方案,目标是为开发中大型项目提供开箱即用的解决方案。包括二次封装组件、utils、hooks、动态菜单、权限校验、多主题配置、按钮级别权限控制等功能。项目会使用前端较新的技术栈,可以作为项目的启动模版,以帮助你快速搭建企业级中后台产品原型。也可以作为一个示例,用于学习 vue3、vite、ts 等主流技术。该项目会持续跟进最新技术,并将其应用在项目中。
|
||||
`;
|
||||
</script>
|
||||
<template>
|
||||
<EllipsisText :line="3" expand>{{ text }}</EllipsisText>
|
||||
</template>
|
||||
@ -1,10 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { EllipsisText } from '@vben/common-ui';
|
||||
|
||||
const text = `
|
||||
Vben Admin 是一个基于 Vue3.0、Vite、 TypeScript 的后台解决方案,目标是为开发中大型项目提供开箱即用的解决方案。包括二次封装组件、utils、hooks、动态菜单、权限校验、多主题配置、按钮级别权限控制等功能。项目会使用前端较新的技术栈,可以作为项目的启动模版,以帮助你快速搭建企业级中后台产品原型。也可以作为一个示例,用于学习 vue3、vite、ts 等主流技术。该项目会持续跟进最新技术,并将其应用在项目中。Vben Admin 是一个基于 Vue3.0、Vite、 TypeScript 的后台解决方案,目标是为开发中大型项目提供开箱即用的解决方案。包括二次封装组件、utils、hooks、动态菜单、权限校验、多主题配置、按钮级别权限控制等功能。项目会使用前端较新的技术栈,可以作为项目的启动模版,以帮助你快速搭建企业级中后台产品原型。也可以作为一个示例,用于学习 vue3、vite、ts 等主流技术。该项目会持续跟进最新技术,并将其应用在项目中。Vben Admin 是一个基于 Vue3.0、Vite、 TypeScript 的后台解决方案,目标是为开发中大型项目提供开箱即用的解决方案。包括二次封装组件、utils、hooks、动态菜单、权限校验、多主题配置、按钮级别权限控制等功能。项目会使用前端较新的技术栈,可以作为项目的启动模版,以帮助你快速搭建企业级中后台产品原型。也可以作为一个示例,用于学习 vue3、vite、ts 等主流技术。该项目会持续跟进最新技术,并将其应用在项目中。Vben Admin 是一个基于 Vue3.0、Vite、 TypeScript 的后台解决方案,目标是为开发中大型项目提供开箱即用的解决方案。包括二次封装组件、utils、hooks、动态菜单、权限校验、多主题配置、按钮级别权限控制等功能。项目会使用前端较新的技术栈,可以作为项目的启动模版,以帮助你快速搭建企业级中后台产品原型。也可以作为一个示例,用于学习 vue3、vite、ts 等主流技术。该项目会持续跟进最新技术,并将其应用在项目中。
|
||||
`;
|
||||
</script>
|
||||
<template>
|
||||
<EllipsisText :max-width="500">{{ text }}</EllipsisText>
|
||||
</template>
|
||||
@ -1,14 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { EllipsisText } from '@vben/common-ui';
|
||||
</script>
|
||||
<template>
|
||||
<EllipsisText :max-width="240">
|
||||
住在我心里孤独的 孤独的海怪 痛苦之王 开始厌倦 深海的光 停滞的海浪
|
||||
<template #tooltip>
|
||||
<div style="text-align: center">
|
||||
《秦皇岛》<br />住在我心里孤独的<br />孤独的海怪 痛苦之王<br />开始厌倦
|
||||
深海的光 停滞的海浪
|
||||
</div>
|
||||
</template>
|
||||
</EllipsisText>
|
||||
</template>
|
||||
@ -1,236 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { Button, message, Space } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
|
||||
const [BaseForm, formApi] = useVbenForm({
|
||||
// 所有表单项共用,可单独在表单内覆盖
|
||||
commonConfig: {
|
||||
// 所有表单项
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
},
|
||||
// 使用 tailwindcss grid布局
|
||||
// 提交函数
|
||||
handleSubmit: onSubmit,
|
||||
// 垂直布局,label和input在不同行,值为vertical
|
||||
layout: 'horizontal',
|
||||
// 水平布局,label和input在同一行
|
||||
schema: [
|
||||
{
|
||||
// 组件需要在 #/adapter.ts内注册,并加上类型
|
||||
component: 'Input',
|
||||
// 对应组件的参数
|
||||
componentProps: {
|
||||
placeholder: '请输入用户名',
|
||||
},
|
||||
// 字段名
|
||||
fieldName: 'field1',
|
||||
// 界面显示的label
|
||||
label: 'field1',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
filterOption: true,
|
||||
options: [
|
||||
{
|
||||
label: '选项1',
|
||||
value: '1',
|
||||
},
|
||||
{
|
||||
label: '选项2',
|
||||
value: '2',
|
||||
},
|
||||
],
|
||||
placeholder: '请选择',
|
||||
showSearch: true,
|
||||
},
|
||||
fieldName: 'fieldOptions',
|
||||
label: '下拉选',
|
||||
},
|
||||
],
|
||||
wrapperClass: 'grid-cols-1 md:grid-cols-2',
|
||||
});
|
||||
|
||||
function onSubmit(values: Record<string, any>) {
|
||||
message.success({
|
||||
content: `form values: ${JSON.stringify(values)}`,
|
||||
});
|
||||
}
|
||||
|
||||
function handleClick(
|
||||
action:
|
||||
| 'batchAddSchema'
|
||||
| 'batchDeleteSchema'
|
||||
| 'disabled'
|
||||
| 'hiddenAction'
|
||||
| 'hiddenResetButton'
|
||||
| 'hiddenSubmitButton'
|
||||
| 'labelWidth'
|
||||
| 'resetDisabled'
|
||||
| 'resetLabelWidth'
|
||||
| 'showAction'
|
||||
| 'showResetButton'
|
||||
| 'showSubmitButton'
|
||||
| 'updateActionAlign'
|
||||
| 'updateResetButton'
|
||||
| 'updateSchema'
|
||||
| 'updateSubmitButton',
|
||||
) {
|
||||
switch (action) {
|
||||
case 'batchAddSchema': {
|
||||
formApi.setState((prev) => {
|
||||
const currentSchema = prev?.schema ?? [];
|
||||
const newSchema = [];
|
||||
for (let i = 0; i < 2; i++) {
|
||||
newSchema.push({
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入',
|
||||
},
|
||||
fieldName: `field${i}${Date.now()}`,
|
||||
label: `field+`,
|
||||
});
|
||||
}
|
||||
return {
|
||||
schema: [...currentSchema, ...newSchema],
|
||||
};
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'batchDeleteSchema': {
|
||||
formApi.setState((prev) => {
|
||||
const currentSchema = prev?.schema ?? [];
|
||||
return {
|
||||
schema: currentSchema.slice(0, -2),
|
||||
};
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'disabled': {
|
||||
formApi.setState({ commonConfig: { disabled: true } });
|
||||
break;
|
||||
}
|
||||
case 'hiddenAction': {
|
||||
formApi.setState({ showDefaultActions: false });
|
||||
break;
|
||||
}
|
||||
case 'hiddenResetButton': {
|
||||
formApi.setState({ resetButtonOptions: { show: false } });
|
||||
break;
|
||||
}
|
||||
case 'hiddenSubmitButton': {
|
||||
formApi.setState({ submitButtonOptions: { show: false } });
|
||||
break;
|
||||
}
|
||||
case 'labelWidth': {
|
||||
formApi.setState({
|
||||
commonConfig: {
|
||||
labelWidth: 150,
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'resetDisabled': {
|
||||
formApi.setState({ commonConfig: { disabled: false } });
|
||||
break;
|
||||
}
|
||||
case 'resetLabelWidth': {
|
||||
formApi.setState({
|
||||
commonConfig: {
|
||||
labelWidth: 100,
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'showAction': {
|
||||
formApi.setState({ showDefaultActions: true });
|
||||
break;
|
||||
}
|
||||
case 'showResetButton': {
|
||||
formApi.setState({ resetButtonOptions: { show: true } });
|
||||
break;
|
||||
}
|
||||
case 'showSubmitButton': {
|
||||
formApi.setState({ submitButtonOptions: { show: true } });
|
||||
break;
|
||||
}
|
||||
case 'updateActionAlign': {
|
||||
formApi.setState({
|
||||
// 可以自行调整class
|
||||
actionWrapperClass: 'text-center',
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'updateResetButton': {
|
||||
formApi.setState({
|
||||
resetButtonOptions: { disabled: true },
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'updateSchema': {
|
||||
formApi.updateSchema([
|
||||
{
|
||||
componentProps: {
|
||||
options: [
|
||||
{
|
||||
label: '选项1',
|
||||
value: '1',
|
||||
},
|
||||
{
|
||||
label: '选项2',
|
||||
value: '2',
|
||||
},
|
||||
{
|
||||
label: '选项3',
|
||||
value: '3',
|
||||
},
|
||||
],
|
||||
},
|
||||
fieldName: 'fieldOptions',
|
||||
},
|
||||
]);
|
||||
message.success('字段 `fieldOptions` 下拉选项更新成功。');
|
||||
break;
|
||||
}
|
||||
case 'updateSubmitButton': {
|
||||
formApi.setState({
|
||||
submitButtonOptions: { loading: true },
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<Space class="mb-5 flex-wrap">
|
||||
<Button @click="handleClick('updateSchema')">updateSchema</Button>
|
||||
<Button @click="handleClick('labelWidth')">更改labelWidth</Button>
|
||||
<Button @click="handleClick('resetLabelWidth')">还原labelWidth</Button>
|
||||
<Button @click="handleClick('disabled')">禁用表单</Button>
|
||||
<Button @click="handleClick('resetDisabled')">解除禁用</Button>
|
||||
<Button @click="handleClick('hiddenAction')">隐藏操作按钮</Button>
|
||||
<Button @click="handleClick('showAction')">显示操作按钮</Button>
|
||||
<Button @click="handleClick('hiddenResetButton')">隐藏重置按钮</Button>
|
||||
<Button @click="handleClick('showResetButton')">显示重置按钮</Button>
|
||||
<Button @click="handleClick('hiddenSubmitButton')">隐藏提交按钮</Button>
|
||||
<Button @click="handleClick('showSubmitButton')">显示提交按钮</Button>
|
||||
<Button @click="handleClick('updateResetButton')">修改重置按钮</Button>
|
||||
<Button @click="handleClick('updateSubmitButton')">修改提交按钮</Button>
|
||||
<Button @click="handleClick('updateActionAlign')">
|
||||
调整操作按钮位置
|
||||
</Button>
|
||||
<Button @click="handleClick('batchAddSchema')"> 批量添加表单项 </Button>
|
||||
<Button @click="handleClick('batchDeleteSchema')">
|
||||
批量删除表单项
|
||||
</Button>
|
||||
</Space>
|
||||
<BaseForm />
|
||||
</div>
|
||||
</template>
|
||||
@ -1,231 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
|
||||
const [BaseForm] = useVbenForm({
|
||||
// 所有表单项共用,可单独在表单内覆盖
|
||||
commonConfig: {
|
||||
// 所有表单项
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
},
|
||||
// 提交函数
|
||||
handleSubmit: onSubmit,
|
||||
// 垂直布局,label和input在不同行,值为vertical
|
||||
// 水平布局,label和input在同一行
|
||||
layout: 'horizontal',
|
||||
schema: [
|
||||
{
|
||||
// 组件需要在 #/adapter.ts内注册,并加上类型
|
||||
component: 'Input',
|
||||
// 对应组件的参数
|
||||
componentProps: {
|
||||
placeholder: '请输入用户名',
|
||||
},
|
||||
// 字段名
|
||||
fieldName: 'username',
|
||||
// 界面显示的label
|
||||
label: '字符串',
|
||||
},
|
||||
{
|
||||
component: 'InputPassword',
|
||||
componentProps: {
|
||||
placeholder: '请输入密码',
|
||||
},
|
||||
fieldName: 'password',
|
||||
label: '密码',
|
||||
},
|
||||
{
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
placeholder: '请输入',
|
||||
},
|
||||
fieldName: 'number',
|
||||
label: '数字(带后缀)',
|
||||
suffix: () => '¥',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
filterOption: true,
|
||||
options: [
|
||||
{
|
||||
label: '选项1',
|
||||
value: '1',
|
||||
},
|
||||
{
|
||||
label: '选项2',
|
||||
value: '2',
|
||||
},
|
||||
],
|
||||
placeholder: '请选择',
|
||||
showSearch: true,
|
||||
},
|
||||
fieldName: 'options',
|
||||
label: '下拉选',
|
||||
},
|
||||
{
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: [
|
||||
{
|
||||
label: '选项1',
|
||||
value: '1',
|
||||
},
|
||||
{
|
||||
label: '选项2',
|
||||
value: '2',
|
||||
},
|
||||
],
|
||||
},
|
||||
fieldName: 'radioGroup',
|
||||
label: '单选组',
|
||||
},
|
||||
{
|
||||
component: 'Radio',
|
||||
fieldName: 'radio',
|
||||
label: '',
|
||||
renderComponentContent: () => {
|
||||
return {
|
||||
default: () => ['Radio'],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'CheckboxGroup',
|
||||
componentProps: {
|
||||
name: 'cname',
|
||||
options: [
|
||||
{
|
||||
label: '选项1',
|
||||
value: '1',
|
||||
},
|
||||
{
|
||||
label: '选项2',
|
||||
value: '2',
|
||||
},
|
||||
],
|
||||
},
|
||||
fieldName: 'checkboxGroup',
|
||||
label: '多选组',
|
||||
},
|
||||
{
|
||||
component: 'Checkbox',
|
||||
fieldName: 'checkbox',
|
||||
label: '',
|
||||
renderComponentContent: () => {
|
||||
return {
|
||||
default: () => ['我已阅读并同意'],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Mentions',
|
||||
componentProps: {
|
||||
options: [
|
||||
{
|
||||
label: 'afc163',
|
||||
value: 'afc163',
|
||||
},
|
||||
{
|
||||
label: 'zombieJ',
|
||||
value: 'zombieJ',
|
||||
},
|
||||
],
|
||||
placeholder: '请输入',
|
||||
},
|
||||
fieldName: 'mentions',
|
||||
label: '提及',
|
||||
},
|
||||
{
|
||||
component: 'Rate',
|
||||
fieldName: 'rate',
|
||||
label: '评分',
|
||||
},
|
||||
{
|
||||
component: 'Switch',
|
||||
componentProps: {
|
||||
class: 'w-auto',
|
||||
},
|
||||
fieldName: 'switch',
|
||||
label: '开关',
|
||||
},
|
||||
{
|
||||
component: 'DatePicker',
|
||||
fieldName: 'datePicker',
|
||||
label: '日期选择框',
|
||||
},
|
||||
{
|
||||
component: 'RangePicker',
|
||||
fieldName: 'rangePicker',
|
||||
label: '范围选择器',
|
||||
},
|
||||
{
|
||||
component: 'TimePicker',
|
||||
fieldName: 'timePicker',
|
||||
label: '时间选择框',
|
||||
},
|
||||
{
|
||||
component: 'TreeSelect',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
placeholder: '请选择',
|
||||
showSearch: true,
|
||||
treeData: [
|
||||
{
|
||||
label: 'root 1',
|
||||
value: 'root 1',
|
||||
children: [
|
||||
{
|
||||
label: 'parent 1',
|
||||
value: 'parent 1',
|
||||
children: [
|
||||
{
|
||||
label: 'parent 1-0',
|
||||
value: 'parent 1-0',
|
||||
children: [
|
||||
{
|
||||
label: 'my leaf',
|
||||
value: 'leaf1',
|
||||
},
|
||||
{
|
||||
label: 'your leaf',
|
||||
value: 'leaf2',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'parent 1-1',
|
||||
value: 'parent 1-1',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'parent 2',
|
||||
value: 'parent 2',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
treeNodeFilterProp: 'label',
|
||||
},
|
||||
fieldName: 'treeSelect',
|
||||
label: '树选择',
|
||||
},
|
||||
],
|
||||
wrapperClass: 'grid-cols-1',
|
||||
});
|
||||
|
||||
function onSubmit(values: Record<string, any>) {
|
||||
message.success({
|
||||
content: `form values: ${JSON.stringify(values)}`,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseForm />
|
||||
</template>
|
||||
@ -1,68 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { h } from 'vue';
|
||||
|
||||
import { Input, message } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
|
||||
const [Form] = useVbenForm({
|
||||
// 所有表单项共用,可单独在表单内覆盖
|
||||
commonConfig: {
|
||||
// 所有表单项
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
labelClass: 'w-2/6',
|
||||
},
|
||||
// 提交函数
|
||||
handleSubmit: onSubmit,
|
||||
// 垂直布局,label和input在不同行,值为vertical
|
||||
// 水平布局,label和input在同一行
|
||||
layout: 'horizontal',
|
||||
schema: [
|
||||
{
|
||||
// 组件需要在 #/adapter.ts内注册,并加上类型
|
||||
component: 'Input',
|
||||
fieldName: 'field',
|
||||
label: '自定义后缀',
|
||||
suffix: () => h('span', { class: 'text-red-600' }, '元'),
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'field1',
|
||||
label: '自定义组件slot',
|
||||
renderComponentContent: () => ({
|
||||
prefix: () => 'prefix',
|
||||
suffix: () => 'suffix',
|
||||
}),
|
||||
},
|
||||
{
|
||||
component: h(Input, { placeholder: '请输入' }),
|
||||
fieldName: 'field2',
|
||||
label: '自定义组件',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'field3',
|
||||
label: '自定义组件(slot)',
|
||||
rules: 'required',
|
||||
},
|
||||
],
|
||||
wrapperClass: 'grid-cols-1',
|
||||
});
|
||||
|
||||
function onSubmit(values: Record<string, any>) {
|
||||
message.success({
|
||||
content: `form values: ${JSON.stringify(values)}`,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Form>
|
||||
<template #field3="slotProps">
|
||||
<Input placeholder="请输入" v-bind="slotProps" />
|
||||
</template>
|
||||
</Form>
|
||||
</template>
|
||||
@ -1,168 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
|
||||
const [Form] = useVbenForm({
|
||||
// 提交函数
|
||||
handleSubmit: onSubmit,
|
||||
schema: [
|
||||
{
|
||||
component: 'Input',
|
||||
defaultValue: 'hidden value',
|
||||
dependencies: {
|
||||
show: false,
|
||||
// 随意一个字段改变时,都会触发
|
||||
triggerFields: ['field1Switch'],
|
||||
},
|
||||
fieldName: 'hiddenField',
|
||||
label: '隐藏字段',
|
||||
},
|
||||
{
|
||||
component: 'Switch',
|
||||
defaultValue: true,
|
||||
fieldName: 'field1Switch',
|
||||
help: '通过Dom控制销毁',
|
||||
label: '显示字段1',
|
||||
},
|
||||
{
|
||||
component: 'Switch',
|
||||
defaultValue: true,
|
||||
fieldName: 'field2Switch',
|
||||
help: '通过css控制隐藏',
|
||||
label: '显示字段2',
|
||||
},
|
||||
{
|
||||
component: 'Switch',
|
||||
fieldName: 'field3Switch',
|
||||
label: '禁用字段3',
|
||||
},
|
||||
{
|
||||
component: 'Switch',
|
||||
fieldName: 'field4Switch',
|
||||
label: '字段4必填',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
if(values) {
|
||||
return !!values.field1Switch;
|
||||
},
|
||||
// 只有指定的字段改变时,才会触发
|
||||
triggerFields: ['field1Switch'],
|
||||
},
|
||||
// 字段名
|
||||
fieldName: 'field1',
|
||||
// 界面显示的label
|
||||
label: '字段1',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
show(values) {
|
||||
return !!values.field2Switch;
|
||||
},
|
||||
triggerFields: ['field2Switch'],
|
||||
},
|
||||
fieldName: 'field2',
|
||||
label: '字段2',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
disabled(values) {
|
||||
return !!values.field3Switch;
|
||||
},
|
||||
triggerFields: ['field3Switch'],
|
||||
},
|
||||
fieldName: 'field3',
|
||||
label: '字段3',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
required(values) {
|
||||
return !!values.field4Switch;
|
||||
},
|
||||
triggerFields: ['field4Switch'],
|
||||
},
|
||||
fieldName: 'field4',
|
||||
label: '字段4',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
rules(values) {
|
||||
if (values.field1 === '123') {
|
||||
return 'required';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
triggerFields: ['field1'],
|
||||
},
|
||||
fieldName: 'field5',
|
||||
help: '当字段1的值为`123`时,必填',
|
||||
label: '动态rules',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
class: 'w-full',
|
||||
filterOption: true,
|
||||
options: [
|
||||
{
|
||||
label: '选项1',
|
||||
value: '1',
|
||||
},
|
||||
{
|
||||
label: '选项2',
|
||||
value: '2',
|
||||
},
|
||||
],
|
||||
placeholder: '请选择',
|
||||
showSearch: true,
|
||||
},
|
||||
dependencies: {
|
||||
componentProps(values) {
|
||||
if (values.field2 === '123') {
|
||||
return {
|
||||
options: [
|
||||
{
|
||||
label: '选项1',
|
||||
value: '1',
|
||||
},
|
||||
{
|
||||
label: '选项2',
|
||||
value: '2',
|
||||
},
|
||||
{
|
||||
label: '选项3',
|
||||
value: '3',
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
return {};
|
||||
},
|
||||
triggerFields: ['field2'],
|
||||
},
|
||||
fieldName: 'field6',
|
||||
help: '当字段2的值为`123`时,更改下拉选项',
|
||||
label: '动态配置',
|
||||
},
|
||||
],
|
||||
// 大屏一行显示3个,中屏一行显示2个,小屏一行显示1个
|
||||
wrapperClass: 'grid-cols-1 md:grid-cols-2',
|
||||
});
|
||||
|
||||
function onSubmit(values: Record<string, any>) {
|
||||
message.success({
|
||||
content: `form values: ${JSON.stringify(values)}`,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Form />
|
||||
</template>
|
||||
@ -1,94 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
|
||||
const [QueryForm] = useVbenForm({
|
||||
// 默认展开
|
||||
collapsed: false,
|
||||
// 所有表单项共用,可单独在表单内覆盖
|
||||
commonConfig: {
|
||||
// 所有表单项
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
},
|
||||
// 提交函数
|
||||
handleSubmit: onSubmit,
|
||||
// 垂直布局,label和input在不同行,值为vertical
|
||||
// 水平布局,label和input在同一行
|
||||
layout: 'horizontal',
|
||||
schema: [
|
||||
{
|
||||
// 组件需要在 #/adapter.ts内注册,并加上类型
|
||||
component: 'Input',
|
||||
// 对应组件的参数
|
||||
componentProps: {
|
||||
placeholder: '请输入用户名',
|
||||
},
|
||||
// 字段名
|
||||
fieldName: 'username',
|
||||
// 界面显示的label
|
||||
label: '字符串',
|
||||
},
|
||||
{
|
||||
component: 'InputPassword',
|
||||
componentProps: {
|
||||
placeholder: '请输入密码',
|
||||
},
|
||||
fieldName: 'password',
|
||||
label: '密码',
|
||||
},
|
||||
{
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
placeholder: '请输入',
|
||||
},
|
||||
fieldName: 'number',
|
||||
label: '数字(带后缀)',
|
||||
suffix: () => '¥',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
filterOption: true,
|
||||
options: [
|
||||
{
|
||||
label: '选项1',
|
||||
value: '1',
|
||||
},
|
||||
{
|
||||
label: '选项2',
|
||||
value: '2',
|
||||
},
|
||||
],
|
||||
placeholder: '请选择',
|
||||
showSearch: true,
|
||||
},
|
||||
fieldName: 'options',
|
||||
label: '下拉选',
|
||||
},
|
||||
{
|
||||
component: 'DatePicker',
|
||||
fieldName: 'datePicker',
|
||||
label: '日期选择框',
|
||||
},
|
||||
],
|
||||
// 是否可展开
|
||||
showCollapseButton: true,
|
||||
submitButtonOptions: {
|
||||
content: '查询',
|
||||
},
|
||||
wrapperClass: 'grid-cols-1 md:grid-cols-2',
|
||||
});
|
||||
function onSubmit(values: Record<string, any>) {
|
||||
message.success({
|
||||
content: `form values: ${JSON.stringify(values)}`,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<QueryForm />
|
||||
</template>
|
||||
@ -1,190 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm, z } from '#/adapter/form';
|
||||
|
||||
const [Form] = useVbenForm({
|
||||
// 所有表单项共用,可单独在表单内覆盖
|
||||
commonConfig: {
|
||||
// 所有表单项
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
},
|
||||
// 提交函数
|
||||
handleSubmit: onSubmit,
|
||||
// 垂直布局,label和input在不同行,值为vertical
|
||||
// 水平布局,label和input在同一行
|
||||
scrollToFirstError: true,
|
||||
layout: 'horizontal',
|
||||
schema: [
|
||||
{
|
||||
// 组件需要在 #/adapter.ts内注册,并加上类型
|
||||
component: 'Input',
|
||||
// 对应组件的参数
|
||||
componentProps: {
|
||||
placeholder: '请输入',
|
||||
},
|
||||
// 字段名
|
||||
fieldName: 'field1',
|
||||
// 界面显示的label
|
||||
label: '字段1',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入',
|
||||
},
|
||||
defaultValue: '默认值',
|
||||
fieldName: 'field2',
|
||||
label: '默认值(必填)',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入',
|
||||
},
|
||||
fieldName: 'field3',
|
||||
label: '默认值(非必填)',
|
||||
rules: z.string().default('默认值').optional(),
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: '请输入',
|
||||
},
|
||||
fieldName: 'field31',
|
||||
label: '自定义信息',
|
||||
rules: z.string().min(1, { message: '最少输入1个字符' }),
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
// 对应组件的参数
|
||||
componentProps: {
|
||||
placeholder: '请输入',
|
||||
},
|
||||
// 字段名
|
||||
fieldName: 'field4',
|
||||
// 界面显示的label
|
||||
label: '邮箱',
|
||||
rules: z.string().email('请输入正确的邮箱'),
|
||||
},
|
||||
{
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
placeholder: '请输入',
|
||||
},
|
||||
fieldName: 'number',
|
||||
label: '数字',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
filterOption: true,
|
||||
options: [
|
||||
{
|
||||
label: '选项1',
|
||||
value: '1',
|
||||
},
|
||||
{
|
||||
label: '选项2',
|
||||
value: '2',
|
||||
},
|
||||
],
|
||||
placeholder: '请选择',
|
||||
showSearch: true,
|
||||
},
|
||||
defaultValue: undefined,
|
||||
fieldName: 'options',
|
||||
label: '下拉选',
|
||||
rules: 'selectRequired',
|
||||
},
|
||||
{
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: [
|
||||
{
|
||||
label: '选项1',
|
||||
value: '1',
|
||||
},
|
||||
{
|
||||
label: '选项2',
|
||||
value: '2',
|
||||
},
|
||||
],
|
||||
},
|
||||
fieldName: 'radioGroup',
|
||||
label: '单选组',
|
||||
rules: 'selectRequired',
|
||||
},
|
||||
{
|
||||
component: 'CheckboxGroup',
|
||||
componentProps: {
|
||||
name: 'cname',
|
||||
options: [
|
||||
{
|
||||
label: '选项1',
|
||||
value: '1',
|
||||
},
|
||||
{
|
||||
label: '选项2',
|
||||
value: '2',
|
||||
},
|
||||
],
|
||||
},
|
||||
fieldName: 'checkboxGroup',
|
||||
label: '多选组',
|
||||
rules: 'selectRequired',
|
||||
},
|
||||
{
|
||||
component: 'Checkbox',
|
||||
fieldName: 'checkbox',
|
||||
label: '',
|
||||
renderComponentContent: () => {
|
||||
return {
|
||||
default: () => ['我已阅读并同意'],
|
||||
};
|
||||
},
|
||||
rules: 'selectRequired',
|
||||
},
|
||||
{
|
||||
component: 'DatePicker',
|
||||
defaultValue: undefined,
|
||||
fieldName: 'datePicker',
|
||||
label: '日期选择框',
|
||||
rules: 'selectRequired',
|
||||
},
|
||||
{
|
||||
component: 'RangePicker',
|
||||
defaultValue: undefined,
|
||||
fieldName: 'rangePicker',
|
||||
label: '区间选择框',
|
||||
rules: 'selectRequired',
|
||||
},
|
||||
{
|
||||
component: 'InputPassword',
|
||||
componentProps: {
|
||||
placeholder: '请输入',
|
||||
},
|
||||
fieldName: 'password',
|
||||
label: '密码',
|
||||
rules: 'required',
|
||||
},
|
||||
],
|
||||
wrapperClass: 'grid-cols-1',
|
||||
});
|
||||
|
||||
function onSubmit(values: Record<string, any>) {
|
||||
message.success({
|
||||
content: `form values: ${JSON.stringify(values)}`,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Form />
|
||||
</template>
|
||||
@ -1,36 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { useVbenModal, VbenButton } from '@vben/common-ui';
|
||||
|
||||
const [SlideModal, slideModalApi] = useVbenModal({
|
||||
animationType: 'slide',
|
||||
});
|
||||
|
||||
const [ScaleModal, scaleModalApi] = useVbenModal({
|
||||
animationType: 'scale',
|
||||
});
|
||||
|
||||
function openSlideModal() {
|
||||
slideModalApi.open();
|
||||
}
|
||||
|
||||
function openScaleModal() {
|
||||
scaleModalApi.open();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<div class="flex gap-4">
|
||||
<VbenButton @click="openSlideModal">滑动动画</VbenButton>
|
||||
<VbenButton @click="openScaleModal">缩放动画</VbenButton>
|
||||
</div>
|
||||
|
||||
<SlideModal title="滑动动画示例" class="w-125">
|
||||
<p>这是使用滑动动画的弹窗,从顶部向下滑动进入。</p>
|
||||
</SlideModal>
|
||||
|
||||
<ScaleModal title="缩放动画示例" class="w-125">
|
||||
<p>这是使用缩放动画的弹窗,以缩放淡入淡出的方式显示。</p>
|
||||
</ScaleModal>
|
||||
</div>
|
||||
</template>
|
||||
@ -1,21 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { useVbenModal, VbenButton } from '@vben/common-ui';
|
||||
|
||||
import ExtraModal from './modal.vue';
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
// 连接抽离的组件
|
||||
connectedComponent: ExtraModal,
|
||||
});
|
||||
|
||||
function openModal() {
|
||||
modalApi.open();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<Modal />
|
||||
<VbenButton @click="openModal">Open</VbenButton>
|
||||
</div>
|
||||
</template>
|
||||
@ -1,45 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal, VbenButton } from '@vben/common-ui';
|
||||
|
||||
const list = ref<number[]>([]);
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
onCancel() {
|
||||
modalApi.close();
|
||||
},
|
||||
onConfirm() {
|
||||
console.log('onConfirm');
|
||||
},
|
||||
onOpenChange(isOpen) {
|
||||
if (isOpen) {
|
||||
handleUpdate(10);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
function handleUpdate(len: number) {
|
||||
modalApi.setState({ loading: true });
|
||||
setTimeout(() => {
|
||||
list.value = Array.from({ length: len }, (_v, k) => k + 1);
|
||||
modalApi.setState({ loading: false });
|
||||
}, 2000);
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<Modal title="自动计算高度">
|
||||
<div
|
||||
v-for="item in list"
|
||||
:key="item"
|
||||
class="flex-center h-55 w-full bg-muted even:bg-heavy"
|
||||
>
|
||||
{{ item }}
|
||||
</div>
|
||||
<template #prepend-footer>
|
||||
<VbenButton type="link" @click="handleUpdate(6)">
|
||||
点击更新数据
|
||||
</VbenButton>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
@ -1,11 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { useVbenModal, VbenButton } from '@vben/common-ui';
|
||||
|
||||
const [Modal, modalApi] = useVbenModal();
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<VbenButton @click="() => modalApi.open()">Open</VbenButton>
|
||||
<Modal class="w-150" title="基础示例"> modal content </Modal>
|
||||
</div>
|
||||
</template>
|
||||
@ -1,21 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { useVbenModal, VbenButton } from '@vben/common-ui';
|
||||
|
||||
import ExtraModal from './modal.vue';
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
// 连接抽离的组件
|
||||
connectedComponent: ExtraModal,
|
||||
});
|
||||
|
||||
function openModal() {
|
||||
modalApi.open();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<Modal />
|
||||
<VbenButton @click="openModal">Open</VbenButton>
|
||||
</div>
|
||||
</template>
|
||||
@ -1,10 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
const [Modal] = useVbenModal({
|
||||
draggable: true,
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<Modal title="拖拽示例"> modal content </Modal>
|
||||
</template>
|
||||
@ -1,29 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { useVbenModal, VbenButton } from '@vben/common-ui';
|
||||
|
||||
import ExtraModal from './modal.vue';
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
// 连接抽离的组件
|
||||
connectedComponent: ExtraModal,
|
||||
});
|
||||
|
||||
function openModal() {
|
||||
modalApi.open();
|
||||
}
|
||||
|
||||
function handleUpdateTitle() {
|
||||
modalApi.setState({ title: '外部动态标题' }).open();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<Modal />
|
||||
|
||||
<VbenButton @click="openModal">Open</VbenButton>
|
||||
<VbenButton class="ml-2" type="primary" @click="handleUpdateTitle">
|
||||
从外部修改标题并打开
|
||||
</VbenButton>
|
||||
</div>
|
||||
</template>
|
||||
@ -1,38 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { useVbenModal, VbenButton } from '@vben/common-ui';
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
draggable: true,
|
||||
onCancel() {
|
||||
modalApi.close();
|
||||
},
|
||||
onConfirm() {
|
||||
console.info('onConfirm');
|
||||
},
|
||||
title: '动态修改配置示例',
|
||||
});
|
||||
|
||||
const state = modalApi.useStore();
|
||||
|
||||
function handleUpdateTitle() {
|
||||
modalApi.setState({ title: '内部动态标题' });
|
||||
}
|
||||
|
||||
function handleToggleFullscreen() {
|
||||
modalApi.setState((prev) => {
|
||||
return { ...prev, fullscreen: !prev.fullscreen };
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<Modal>
|
||||
<div class="flex-col-center">
|
||||
<VbenButton class="mb-3" type="primary" @click="handleUpdateTitle()">
|
||||
内部动态修改标题
|
||||
</VbenButton>
|
||||
<VbenButton class="mb-3" @click="handleToggleFullscreen()">
|
||||
{{ state.fullscreen ? '退出全屏' : '打开全屏' }}
|
||||
</VbenButton>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
@ -1,21 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { useVbenModal, VbenButton } from '@vben/common-ui';
|
||||
|
||||
import ExtraModal from './modal.vue';
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
// 连接抽离的组件
|
||||
connectedComponent: ExtraModal,
|
||||
});
|
||||
|
||||
function openModal() {
|
||||
modalApi.open();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<Modal />
|
||||
<VbenButton @click="openModal">Open</VbenButton>
|
||||
</div>
|
||||
</template>
|
||||
@ -1,8 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
const [Modal] = useVbenModal();
|
||||
</script>
|
||||
<template>
|
||||
<Modal title="组件抽离示例"> extra modal content </Modal>
|
||||
</template>
|
||||
@ -1,27 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { useVbenModal, VbenButton } from '@vben/common-ui';
|
||||
|
||||
import ExtraModal from './modal.vue';
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
// 连接抽离的组件
|
||||
connectedComponent: ExtraModal,
|
||||
});
|
||||
|
||||
function openModal() {
|
||||
modalApi
|
||||
.setData({
|
||||
content: '外部传递的数据 content',
|
||||
payload: '外部传递的数据 payload',
|
||||
})
|
||||
.open();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<Modal />
|
||||
|
||||
<VbenButton @click="openModal">Open</VbenButton>
|
||||
</div>
|
||||
</template>
|
||||
@ -1,26 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
const data = ref();
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
onCancel() {
|
||||
modalApi.close();
|
||||
},
|
||||
onConfirm() {
|
||||
console.info('onConfirm');
|
||||
},
|
||||
onOpenChange(isOpen: boolean) {
|
||||
if (isOpen) {
|
||||
data.value = modalApi.getData<Record<string, any>>();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<Modal title="数据共享示例">
|
||||
<div class="flex-col-center">外部传递数据: {{ data }}</div>
|
||||
</Modal>
|
||||
</template>
|
||||
@ -1,85 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeGridListeners, VxeGridProps } from '#/adapter/vxe-table';
|
||||
|
||||
import { Button, message } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
|
||||
import { MOCK_TABLE_DATA } from '../table-data';
|
||||
|
||||
interface RowType {
|
||||
address: string;
|
||||
age: number;
|
||||
id: number;
|
||||
name: string;
|
||||
nickname: string;
|
||||
role: string;
|
||||
}
|
||||
|
||||
const gridOptions: VxeGridProps<RowType> = {
|
||||
columns: [
|
||||
{ title: '序号', type: 'seq', width: 50 },
|
||||
{ field: 'name', title: 'Name' },
|
||||
{ field: 'age', sortable: true, title: 'Age' },
|
||||
{ field: 'nickname', title: 'Nickname' },
|
||||
{ field: 'role', title: 'Role' },
|
||||
{ field: 'address', showOverflow: true, title: 'Address' },
|
||||
],
|
||||
data: MOCK_TABLE_DATA,
|
||||
pagerConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
sortConfig: {
|
||||
multiple: true,
|
||||
},
|
||||
};
|
||||
|
||||
const gridEvents: VxeGridListeners<RowType> = {
|
||||
cellClick: ({ row }) => {
|
||||
message.info(`cell-click: ${row.name}`);
|
||||
},
|
||||
};
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({ gridEvents, gridOptions });
|
||||
|
||||
const showBorder = gridApi.useStore((state) => state.gridOptions?.border);
|
||||
const showStripe = gridApi.useStore((state) => state.gridOptions?.stripe);
|
||||
|
||||
function changeBorder() {
|
||||
gridApi.setGridOptions({
|
||||
border: !showBorder.value,
|
||||
});
|
||||
}
|
||||
|
||||
function changeStripe() {
|
||||
gridApi.setGridOptions({
|
||||
stripe: !showStripe.value,
|
||||
});
|
||||
}
|
||||
|
||||
function changeLoading() {
|
||||
gridApi.setLoading(true);
|
||||
setTimeout(() => {
|
||||
gridApi.setLoading(false);
|
||||
}, 2000);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 此处的`vp-raw` 是为了适配文档的展示效果,实际使用时不需要 -->
|
||||
<div class="vp-raw w-full">
|
||||
<Grid>
|
||||
<template #toolbar-tools>
|
||||
<Button class="mr-2" type="primary" @click="changeBorder">
|
||||
{{ showBorder ? '隐藏' : '显示' }}边框
|
||||
</Button>
|
||||
<Button class="mr-2" type="primary" @click="changeLoading">
|
||||
显示loading
|
||||
</Button>
|
||||
<Button class="mr-2" type="primary" @click="changeStripe">
|
||||
{{ showStripe ? '隐藏' : '显示' }}斑马纹
|
||||
</Button>
|
||||
</template>
|
||||
</Grid>
|
||||
</div>
|
||||
</template>
|
||||
@ -1,105 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
|
||||
import { Button, Image, Switch, Tag } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
|
||||
import { getExampleTableApi } from '../mock-api';
|
||||
|
||||
interface RowType {
|
||||
category: string;
|
||||
color: string;
|
||||
id: string;
|
||||
imageUrl: string;
|
||||
open: boolean;
|
||||
price: string;
|
||||
productName: string;
|
||||
releaseDate: string;
|
||||
status: 'error' | 'success' | 'warning';
|
||||
}
|
||||
|
||||
const gridOptions: VxeGridProps<RowType> = {
|
||||
checkboxConfig: {
|
||||
highlight: true,
|
||||
labelField: 'name',
|
||||
},
|
||||
columns: [
|
||||
{ title: '序号', type: 'seq', width: 50 },
|
||||
{ field: 'category', title: 'Category', width: 100 },
|
||||
{
|
||||
field: 'imageUrl',
|
||||
slots: { default: 'image-url' },
|
||||
title: 'Image',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
cellRender: { name: 'CellImage' },
|
||||
field: 'imageUrl2',
|
||||
title: 'Render Image',
|
||||
width: 130,
|
||||
},
|
||||
{
|
||||
field: 'open',
|
||||
slots: { default: 'open' },
|
||||
title: 'Open',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
slots: { default: 'status' },
|
||||
title: 'Status',
|
||||
width: 100,
|
||||
},
|
||||
{ field: 'color', title: 'Color', width: 100 },
|
||||
{ field: 'productName', title: 'Product Name', width: 200 },
|
||||
{ field: 'price', title: 'Price', width: 100 },
|
||||
{
|
||||
field: 'releaseDate',
|
||||
formatter: 'formatDateTime',
|
||||
title: 'Date',
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
cellRender: { name: 'CellLink', props: { text: '编辑' } },
|
||||
field: 'action',
|
||||
fixed: 'right',
|
||||
title: '操作',
|
||||
width: 120,
|
||||
},
|
||||
],
|
||||
keepSource: true,
|
||||
pagerConfig: {},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }) => {
|
||||
return await getExampleTableApi({
|
||||
page: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const [Grid] = useVbenVxeGrid({ gridOptions });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="vp-raw w-full">
|
||||
<Grid>
|
||||
<template #image-url="{ row }">
|
||||
<Image :src="row.imageUrl" height="30" width="30" />
|
||||
</template>
|
||||
<template #open="{ row }">
|
||||
<Switch v-model="row.open" />
|
||||
</template>
|
||||
<template #status="{ row }">
|
||||
<Tag :color="row.color">{{ row.status }}</Tag>
|
||||
</template>
|
||||
<template #action>
|
||||
<Button type="link">编辑</Button>
|
||||
</template>
|
||||
</Grid>
|
||||
</div>
|
||||
</template>
|
||||
@ -1,55 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
|
||||
import { getExampleTableApi } from '../mock-api';
|
||||
|
||||
interface RowType {
|
||||
category: string;
|
||||
color: string;
|
||||
id: string;
|
||||
price: string;
|
||||
productName: string;
|
||||
releaseDate: string;
|
||||
}
|
||||
|
||||
const gridOptions: VxeGridProps<RowType> = {
|
||||
columns: [
|
||||
{ title: '序号', type: 'seq', width: 50 },
|
||||
{ editRender: { name: 'input' }, field: 'category', title: 'Category' },
|
||||
{ editRender: { name: 'input' }, field: 'color', title: 'Color' },
|
||||
{
|
||||
editRender: { name: 'input' },
|
||||
field: 'productName',
|
||||
title: 'Product Name',
|
||||
},
|
||||
{ field: 'price', title: 'Price' },
|
||||
{ field: 'releaseDate', formatter: 'formatDateTime', title: 'Date' },
|
||||
],
|
||||
editConfig: {
|
||||
mode: 'cell',
|
||||
trigger: 'click',
|
||||
},
|
||||
pagerConfig: {},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }) => {
|
||||
return await getExampleTableApi({
|
||||
page: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
showOverflow: true,
|
||||
};
|
||||
|
||||
const [Grid] = useVbenVxeGrid({ gridOptions });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="vp-raw w-full">
|
||||
<Grid />
|
||||
</div>
|
||||
</template>
|
||||
@ -1,92 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
|
||||
import { Button, message } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
|
||||
import { getExampleTableApi } from '../mock-api';
|
||||
|
||||
interface RowType {
|
||||
category: string;
|
||||
color: string;
|
||||
id: string;
|
||||
price: string;
|
||||
productName: string;
|
||||
releaseDate: string;
|
||||
}
|
||||
|
||||
const gridOptions: VxeGridProps<RowType> = {
|
||||
columns: [
|
||||
{ title: '序号', type: 'seq', width: 50 },
|
||||
{ editRender: { name: 'input' }, field: 'category', title: 'Category' },
|
||||
{ editRender: { name: 'input' }, field: 'color', title: 'Color' },
|
||||
{
|
||||
editRender: { name: 'input' },
|
||||
field: 'productName',
|
||||
title: 'Product Name',
|
||||
},
|
||||
{ field: 'price', title: 'Price' },
|
||||
{ field: 'releaseDate', formatter: 'formatDateTime', title: 'Date' },
|
||||
{ slots: { default: 'action' }, title: '操作' },
|
||||
],
|
||||
editConfig: {
|
||||
mode: 'row',
|
||||
trigger: 'click',
|
||||
},
|
||||
pagerConfig: {},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }) => {
|
||||
return await getExampleTableApi({
|
||||
page: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
showOverflow: true,
|
||||
};
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({ gridOptions });
|
||||
|
||||
function hasEditStatus(row: RowType) {
|
||||
return gridApi.grid?.isEditByRow(row);
|
||||
}
|
||||
|
||||
function editRowEvent(row: RowType) {
|
||||
gridApi.grid?.setEditRow(row);
|
||||
}
|
||||
|
||||
async function saveRowEvent(row: RowType) {
|
||||
await gridApi.grid?.clearEdit();
|
||||
|
||||
gridApi.setLoading(true);
|
||||
setTimeout(() => {
|
||||
gridApi.setLoading(false);
|
||||
message.success({
|
||||
content: `保存成功!category=${row.category}`,
|
||||
});
|
||||
}, 600);
|
||||
}
|
||||
|
||||
const cancelRowEvent = (_row: RowType) => {
|
||||
gridApi.grid?.clearEdit();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="vp-raw w-full">
|
||||
<Grid>
|
||||
<template #action="{ row }">
|
||||
<template v-if="hasEditStatus(row)">
|
||||
<Button type="link" @click="saveRowEvent(row)">保存</Button>
|
||||
<Button type="link" @click="cancelRowEvent(row)">取消</Button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<Button type="link" @click="editRowEvent(row)">编辑</Button>
|
||||
</template>
|
||||
</template>
|
||||
</Grid>
|
||||
</div>
|
||||
</template>
|
||||
@ -1,67 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
|
||||
import { Button } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
|
||||
import { getExampleTableApi } from '../mock-api';
|
||||
|
||||
interface RowType {
|
||||
category: string;
|
||||
color: string;
|
||||
id: string;
|
||||
price: string;
|
||||
productName: string;
|
||||
releaseDate: string;
|
||||
}
|
||||
|
||||
const gridOptions: VxeGridProps<RowType> = {
|
||||
columns: [
|
||||
{ fixed: 'left', title: '序号', type: 'seq', width: 50 },
|
||||
{ field: 'category', title: 'Category', width: 300 },
|
||||
{ field: 'color', title: 'Color', width: 300 },
|
||||
{ field: 'productName', title: 'Product Name', width: 300 },
|
||||
{ field: 'price', title: 'Price', width: 300 },
|
||||
{
|
||||
field: 'releaseDate',
|
||||
formatter: 'formatDateTime',
|
||||
title: 'DateTime',
|
||||
width: 500,
|
||||
},
|
||||
{
|
||||
field: 'action',
|
||||
fixed: 'right',
|
||||
slots: { default: 'action' },
|
||||
title: '操作',
|
||||
width: 120,
|
||||
},
|
||||
],
|
||||
pagerConfig: {},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }) => {
|
||||
return await getExampleTableApi({
|
||||
page: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
isHover: true,
|
||||
},
|
||||
};
|
||||
|
||||
const [Grid] = useVbenVxeGrid({ gridOptions });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="vp-raw w-full">
|
||||
<Grid>
|
||||
<template #action>
|
||||
<Button type="link">编辑</Button>
|
||||
</template>
|
||||
</Grid>
|
||||
</div>
|
||||
</template>
|
||||
@ -1,127 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VbenFormProps } from '#/adapter/form';
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
|
||||
import { getExampleTableApi } from '../mock-api';
|
||||
|
||||
interface RowType {
|
||||
category: string;
|
||||
color: string;
|
||||
id: string;
|
||||
price: string;
|
||||
productName: string;
|
||||
releaseDate: string;
|
||||
}
|
||||
|
||||
const formOptions: VbenFormProps = {
|
||||
// 默认展开
|
||||
collapsed: false,
|
||||
schema: [
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: 'Please enter category',
|
||||
},
|
||||
defaultValue: '1',
|
||||
fieldName: 'category',
|
||||
label: 'Category',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: 'Please enter productName',
|
||||
},
|
||||
fieldName: 'productName',
|
||||
label: 'ProductName',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
placeholder: 'Please enter price',
|
||||
},
|
||||
fieldName: 'price',
|
||||
label: 'Price',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: [
|
||||
{
|
||||
label: 'Color1',
|
||||
value: '1',
|
||||
},
|
||||
{
|
||||
label: 'Color2',
|
||||
value: '2',
|
||||
},
|
||||
],
|
||||
placeholder: '请选择',
|
||||
},
|
||||
fieldName: 'color',
|
||||
label: 'Color',
|
||||
},
|
||||
{
|
||||
component: 'DatePicker',
|
||||
fieldName: 'datePicker',
|
||||
label: 'Date',
|
||||
},
|
||||
],
|
||||
// 控制表单是否显示折叠按钮
|
||||
showCollapseButton: true,
|
||||
submitButtonOptions: {
|
||||
content: '查询',
|
||||
},
|
||||
// 是否在字段值改变时提交表单
|
||||
submitOnChange: false,
|
||||
// 按下回车时是否提交表单
|
||||
submitOnEnter: false,
|
||||
};
|
||||
|
||||
const gridOptions: VxeGridProps<RowType> = {
|
||||
checkboxConfig: {
|
||||
highlight: true,
|
||||
labelField: 'name',
|
||||
},
|
||||
columns: [
|
||||
{ title: '序号', type: 'seq', width: 50 },
|
||||
{ align: 'left', title: 'Name', type: 'checkbox', width: 100 },
|
||||
{ field: 'category', title: 'Category' },
|
||||
{ field: 'color', title: 'Color' },
|
||||
{ field: 'productName', title: 'Product Name' },
|
||||
{ field: 'price', title: 'Price' },
|
||||
{ field: 'releaseDate', formatter: 'formatDateTime', title: 'Date' },
|
||||
],
|
||||
keepSource: true,
|
||||
pagerConfig: {},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
message.success(`Query params: ${JSON.stringify(formValues)}`);
|
||||
return await getExampleTableApi({
|
||||
page: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
toolbarConfig: {
|
||||
// 是否显示搜索表单控制按钮
|
||||
// @ts-ignore - 正式环境时有完整的类型声明
|
||||
search: true,
|
||||
},
|
||||
};
|
||||
|
||||
const [Grid] = useVbenVxeGrid({ formOptions, gridOptions });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="vp-raw w-full">
|
||||
<Grid />
|
||||
</div>
|
||||
</template>
|
||||
@ -1,36 +0,0 @@
|
||||
import { MOCK_API_DATA } from './table-data';
|
||||
|
||||
export namespace DemoTableApi {
|
||||
export interface PageFetchParams {
|
||||
[key: string]: any;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}
|
||||
}
|
||||
|
||||
export function sleep(time = 1000) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve(true);
|
||||
}, time);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取示例表格数据
|
||||
*/
|
||||
async function getExampleTableApi(params: DemoTableApi.PageFetchParams) {
|
||||
return new Promise<{ items: any; total: number }>((resolve) => {
|
||||
const { page, pageSize } = params;
|
||||
const items = MOCK_API_DATA.slice((page - 1) * pageSize, page * pageSize);
|
||||
|
||||
sleep(1000).then(() => {
|
||||
resolve({
|
||||
total: items.length,
|
||||
items,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export { getExampleTableApi };
|
||||
@ -1,112 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import type { DemoTableApi } from '../mock-api';
|
||||
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
|
||||
import { Button } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
|
||||
import { MOCK_API_DATA } from '../table-data';
|
||||
|
||||
interface RowType {
|
||||
category: string;
|
||||
color: string;
|
||||
id: string;
|
||||
price: string;
|
||||
productName: string;
|
||||
releaseDate: string;
|
||||
}
|
||||
|
||||
// 数据实例
|
||||
// const MOCK_TREE_TABLE_DATA = [
|
||||
// {
|
||||
// date: '2020-08-01',
|
||||
// id: 10_000,
|
||||
// name: 'Test1',
|
||||
// parentId: null,
|
||||
// size: 1024,
|
||||
// type: 'mp3',
|
||||
// },
|
||||
// ]
|
||||
|
||||
const sleep = (time = 1000) => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve(true);
|
||||
}, time);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取示例表格数据
|
||||
*/
|
||||
async function getExampleTableApi(params: DemoTableApi.PageFetchParams) {
|
||||
return new Promise<{ items: any; total: number }>((resolve) => {
|
||||
const { page, pageSize } = params;
|
||||
const items = MOCK_API_DATA.slice((page - 1) * pageSize, page * pageSize);
|
||||
|
||||
sleep(1000).then(() => {
|
||||
resolve({
|
||||
total: items.length,
|
||||
items,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const gridOptions: VxeGridProps<RowType> = {
|
||||
checkboxConfig: {
|
||||
highlight: true,
|
||||
labelField: 'name',
|
||||
},
|
||||
columns: [
|
||||
{ title: '序号', type: 'seq', width: 50 },
|
||||
{ align: 'left', title: 'Name', type: 'checkbox', width: 100 },
|
||||
{ field: 'category', title: 'Category' },
|
||||
{ field: 'color', title: 'Color' },
|
||||
{ field: 'productName', title: 'Product Name' },
|
||||
{ field: 'price', title: 'Price' },
|
||||
{ field: 'releaseDate', formatter: 'formatDateTime', title: 'DateTime' },
|
||||
],
|
||||
exportConfig: {},
|
||||
// height: 'auto', // 如果设置为 auto,则必须确保存在父节点且不允许存在相邻元素,否则会出现高度闪动问题
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }) => {
|
||||
return await getExampleTableApi({
|
||||
page: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
toolbarConfig: {
|
||||
custom: true,
|
||||
export: true,
|
||||
// import: true,
|
||||
refresh: true,
|
||||
zoom: true,
|
||||
},
|
||||
};
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
gridOptions,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="vp-raw w-full">
|
||||
<Grid>
|
||||
<template #toolbar-tools>
|
||||
<Button class="mr-2" type="primary" @click="() => gridApi.query()">
|
||||
刷新当前页面
|
||||
</Button>
|
||||
<Button type="primary" @click="() => gridApi.reload()">
|
||||
刷新并返回第一页
|
||||
</Button>
|
||||
</template>
|
||||
</Grid>
|
||||
</div>
|
||||
</template>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user