diff --git a/apps/web-antd/src/views/notice/compoents/detail.vue b/apps/web-antd/src/views/notice/compoents/detail.vue
index b718f1e8..15b5495a 100644
--- a/apps/web-antd/src/views/notice/compoents/detail.vue
+++ b/apps/web-antd/src/views/notice/compoents/detail.vue
@@ -1,5 +1,5 @@
+
+
+
+
+
diff --git a/apps/web-antd/src/views/system/vip/duration/config/form.ts b/apps/web-antd/src/views/system/vip/duration/config/form.ts
new file mode 100644
index 00000000..db8502e2
--- /dev/null
+++ b/apps/web-antd/src/views/system/vip/duration/config/form.ts
@@ -0,0 +1,57 @@
+import type { VbenFormProps } from '#/adapter/form';
+
+export const modalFormProps: VbenFormProps = {
+ wrapperClass: 'grid-cols-12',
+ commonConfig: {
+ formItemClass: 'col-span-12',
+ componentProps: { class: 'w-full' },
+ },
+ layout: 'horizontal',
+ schema: [
+ {
+ component: 'VbenInput',
+ fieldName: 'id',
+ label: 'ID',
+ dependencies: { show: false, triggerFields: ['id'] },
+ },
+ {
+ component: 'VbenInput',
+ fieldName: 'code',
+ label: '编码',
+ componentProps: { disabled: true },
+ },
+ {
+ component: 'VbenInput',
+ fieldName: 'name',
+ label: '名称',
+ rules: 'required',
+ },
+ {
+ component: 'Avatar',
+ fieldName: 'badge_url',
+ label: '类型徽标',
+ defaultValue: '',
+ },
+ {
+ component: 'InputNumber',
+ fieldName: 'sort',
+ label: '排序',
+ formItemClass: 'col-span-6',
+ defaultValue: 0,
+ },
+ {
+ component: 'VbenSelect',
+ fieldName: 'status',
+ label: '状态',
+ formItemClass: 'col-span-6',
+ componentProps: {
+ options: [
+ { label: '启用', value: 1 },
+ { label: '禁用', value: 0 },
+ ],
+ },
+ defaultValue: 1,
+ },
+ ],
+ showDefaultActions: false,
+};
diff --git a/apps/web-antd/src/views/system/vip/duration/config/search.ts b/apps/web-antd/src/views/system/vip/duration/config/search.ts
new file mode 100644
index 00000000..c163369f
--- /dev/null
+++ b/apps/web-antd/src/views/system/vip/duration/config/search.ts
@@ -0,0 +1,27 @@
+import type { VbenFormProps } from '@vben/common-ui';
+
+export const formOptions: VbenFormProps = {
+ layout: 'inline',
+ showResetButton: true,
+ showSubmitButton: true,
+ schemas: [
+ {
+ fieldName: 'name',
+ component: 'Input',
+ label: '名称',
+ componentProps: { placeholder: '时效名称', allowClear: true },
+ },
+ {
+ fieldName: 'status',
+ component: 'Select',
+ label: '状态',
+ componentProps: {
+ allowClear: true,
+ options: [
+ { label: '启用', value: 1 },
+ { label: '禁用', value: 0 },
+ ],
+ },
+ },
+ ],
+};
diff --git a/apps/web-antd/src/views/system/vip/duration/config/table.ts b/apps/web-antd/src/views/system/vip/duration/config/table.ts
new file mode 100644
index 00000000..8c38d46f
--- /dev/null
+++ b/apps/web-antd/src/views/system/vip/duration/config/table.ts
@@ -0,0 +1,33 @@
+import type { VxeGridProps } from '#/adapter/vxe-table';
+
+import { getVipDurationList } from '../api';
+
+export const gridOptions: VxeGridProps
= {
+ columnConfig: { useKey: true },
+ rowConfig: { useKey: true, isHover: true },
+ columns: [
+ { field: 'id', title: 'ID', width: 70 },
+ { field: 'code', title: '编码', width: 120 },
+ { field: 'name', title: '名称', minWidth: 140 },
+ { field: 'badge_url', title: '丝带徽标', width: 160, slots: { default: 'badge' } },
+ { field: 'sort', title: '排序', width: 80 },
+ { field: 'status_txt', title: '状态', width: 80 },
+ { field: 'updated_at', title: '更新时间', width: 170 },
+ { title: '操作', slots: { default: 'action' }, width: 100, fixed: 'right' },
+ ],
+ keepSource: true,
+ pagerConfig: {},
+ proxyConfig: {
+ ajax: {
+ query: async ({ page }, formValues) => {
+ return await getVipDurationList({
+ page: page.currentPage,
+ pageSize: page.pageSize,
+ ...formValues,
+ });
+ },
+ },
+ },
+ border: false,
+ toolbarConfig: { search: true, refresh: true, zoom: true },
+};
diff --git a/apps/web-antd/src/views/system/vip/duration/index.vue b/apps/web-antd/src/views/system/vip/duration/index.vue
new file mode 100644
index 00000000..f7bb012d
--- /dev/null
+++ b/apps/web-antd/src/views/system/vip/duration/index.vue
@@ -0,0 +1,197 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
![]()
+
+ 未上传丝带
+
+
+
{{ item.name }}
+
{{ item.code }}
+
+ {{ item.status === 1 ? '启用' : '禁用' }}
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antd/src/views/system/vip/level/api/index.ts b/apps/web-antd/src/views/system/vip/level/api/index.ts
new file mode 100644
index 00000000..51e4c1a4
--- /dev/null
+++ b/apps/web-antd/src/views/system/vip/level/api/index.ts
@@ -0,0 +1,28 @@
+import { requestClient } from '#/api/request';
+
+/** VIP 等级配置 API,路由前缀 vip-level/ */
+const prefix = 'vip-level/';
+
+export async function getVipLevelList(data: any) {
+ return requestClient.get(`${prefix}list`, { params: data });
+}
+
+export async function getVipLevelOption(data?: any) {
+ return requestClient.get(`${prefix}option`, { params: data });
+}
+
+export async function getVipLevelInfo(id: number) {
+ return requestClient.get(`${prefix}detail`, { params: { id } });
+}
+
+export async function createVipLevel(data: Record) {
+ return requestClient.post(`${prefix}create`, data);
+}
+
+export async function updateVipLevel(data: Record) {
+ return requestClient.post(`${prefix}update`, data);
+}
+
+export async function deleteVipLevel(data: Record) {
+ return requestClient.post(`${prefix}delete`, data);
+}
diff --git a/apps/web-antd/src/views/system/vip/level/components/modal.vue b/apps/web-antd/src/views/system/vip/level/components/modal.vue
new file mode 100644
index 00000000..ad295826
--- /dev/null
+++ b/apps/web-antd/src/views/system/vip/level/components/modal.vue
@@ -0,0 +1,72 @@
+
+
+
+
+
+
diff --git a/apps/web-antd/src/views/system/vip/level/config/constants.ts b/apps/web-antd/src/views/system/vip/level/config/constants.ts
new file mode 100644
index 00000000..f1214db8
--- /dev/null
+++ b/apps/web-antd/src/views/system/vip/level/config/constants.ts
@@ -0,0 +1,15 @@
+/** VIP 等级组选项 */
+export const VIP_TIER_GROUP_OPTIONS = [
+ { label: '普通', value: 0 },
+ { label: '黄金', value: 1 },
+ { label: '铂金', value: 2 },
+ { label: '钻石', value: 3 },
+ { label: '黑金', value: 4 },
+ { label: '定制', value: 5 },
+];
+
+/** 状态选项 */
+export const STATUS_OPTIONS = [
+ { label: '启用', value: 1 },
+ { label: '禁用', value: 0 },
+];
diff --git a/apps/web-antd/src/views/system/vip/level/config/form.ts b/apps/web-antd/src/views/system/vip/level/config/form.ts
new file mode 100644
index 00000000..0df133b4
--- /dev/null
+++ b/apps/web-antd/src/views/system/vip/level/config/form.ts
@@ -0,0 +1,100 @@
+import type { VbenFormProps } from '#/adapter/form';
+
+import { STATUS_OPTIONS, VIP_TIER_GROUP_OPTIONS } from './constants';
+
+export const modalFormProps: VbenFormProps = {
+ wrapperClass: 'grid-cols-12',
+ commonConfig: {
+ formItemClass: 'col-span-12',
+ componentProps: { class: 'w-full' },
+ },
+ layout: 'horizontal',
+ schema: [
+ {
+ component: 'VbenInput',
+ fieldName: 'id',
+ label: 'ID',
+ dependencies: { show: false, triggerFields: ['id'] },
+ },
+ {
+ component: 'VbenInput',
+ fieldName: 'code',
+ label: '等级编码',
+ rules: 'required',
+ componentProps: { placeholder: '如 V5 / BLACK_GOLD' },
+ },
+ {
+ component: 'VbenInput',
+ fieldName: 'name',
+ label: '等级名称',
+ rules: 'required',
+ componentProps: { placeholder: '如铂金会员' },
+ },
+ {
+ component: 'VbenSelect',
+ fieldName: 'tier_group',
+ label: '等级组',
+ rules: 'selectRequired',
+ formItemClass: 'col-span-6',
+ componentProps: { options: VIP_TIER_GROUP_OPTIONS },
+ defaultValue: 0,
+ },
+ {
+ component: 'InputNumber',
+ fieldName: 'level_weight',
+ label: '权重',
+ formItemClass: 'col-span-6',
+ componentProps: { min: 0, max: 9999 },
+ defaultValue: 0,
+ },
+ {
+ component: 'Avatar',
+ fieldName: 'badge_url',
+ label: '等级徽标',
+ defaultValue: '',
+ },
+ {
+ component: 'InputNumber',
+ fieldName: 'price',
+ label: '标价',
+ formItemClass: 'col-span-6',
+ componentProps: { min: 0, precision: 2, placeholder: '0 表示免费' },
+ defaultValue: 0,
+ },
+ {
+ component: 'InputNumber',
+ fieldName: 'sort',
+ label: '排序',
+ formItemClass: 'col-span-6',
+ componentProps: { min: 0 },
+ defaultValue: 0,
+ },
+ {
+ component: 'Select',
+ fieldName: 'permissions',
+ label: '权限码',
+ componentProps: {
+ mode: 'tags',
+ placeholder: '输入权限码后回车,预留业务权益',
+ tokenSeparators: [','],
+ },
+ defaultValue: [],
+ },
+ {
+ component: 'VbenInput',
+ fieldName: 'description',
+ label: '说明',
+ componentProps: { type: 'textarea', rows: 2 },
+ defaultValue: '',
+ },
+ {
+ component: 'VbenSelect',
+ fieldName: 'status',
+ label: '状态',
+ formItemClass: 'col-span-6',
+ componentProps: { options: STATUS_OPTIONS },
+ defaultValue: 1,
+ },
+ ],
+ showDefaultActions: false,
+};
diff --git a/apps/web-antd/src/views/system/vip/level/config/search.ts b/apps/web-antd/src/views/system/vip/level/config/search.ts
new file mode 100644
index 00000000..d1e22f96
--- /dev/null
+++ b/apps/web-antd/src/views/system/vip/level/config/search.ts
@@ -0,0 +1,43 @@
+import type { VbenFormProps } from '@vben/common-ui';
+
+import { STATUS_OPTIONS, VIP_TIER_GROUP_OPTIONS } from './constants';
+
+export const formOptions: VbenFormProps = {
+ layout: 'inline',
+ showResetButton: true,
+ showSubmitButton: true,
+ schemas: [
+ {
+ fieldName: 'code',
+ component: 'Input',
+ label: '编码',
+ componentProps: { placeholder: '如 V5', allowClear: true },
+ },
+ {
+ fieldName: 'name',
+ component: 'Input',
+ label: '名称',
+ componentProps: { placeholder: '等级名称', allowClear: true },
+ },
+ {
+ fieldName: 'tier_group',
+ component: 'Select',
+ label: '等级组',
+ componentProps: {
+ placeholder: '请选择',
+ allowClear: true,
+ options: VIP_TIER_GROUP_OPTIONS,
+ },
+ },
+ {
+ fieldName: 'status',
+ component: 'Select',
+ label: '状态',
+ componentProps: {
+ placeholder: '请选择',
+ allowClear: true,
+ options: STATUS_OPTIONS,
+ },
+ },
+ ],
+};
diff --git a/apps/web-antd/src/views/system/vip/level/config/table.ts b/apps/web-antd/src/views/system/vip/level/config/table.ts
new file mode 100644
index 00000000..8bad0525
--- /dev/null
+++ b/apps/web-antd/src/views/system/vip/level/config/table.ts
@@ -0,0 +1,50 @@
+import type { VxeGridProps } from '#/adapter/vxe-table';
+
+import { getVipLevelList } from '../api';
+import { VIP_TIER_GROUP_OPTIONS } from './constants';
+
+export const gridOptions: VxeGridProps = {
+ checkboxConfig: { highlight: true, labelField: '' },
+ columnConfig: { useKey: true },
+ rowConfig: { useKey: true, isHover: true },
+ columns: [
+ { type: 'checkbox', width: 60 },
+ { field: 'id', title: 'ID', width: 70 },
+ { field: 'code', title: '编码', width: 120 },
+ { field: 'name', title: '名称', minWidth: 120 },
+ {
+ field: 'tier_group',
+ title: '等级组',
+ width: 90,
+ formatter: ({ cellValue }) =>
+ VIP_TIER_GROUP_OPTIONS.find((o) => o.value === cellValue)?.label || '-',
+ },
+ { field: 'level_weight', title: '权重', width: 80 },
+ { field: 'badge_url', title: '等级徽标', width: 100, slots: { default: 'badge' } },
+ { field: 'price', title: '标价', width: 100 },
+ { field: 'status_txt', title: '状态', width: 80 },
+ { field: 'sort', title: '排序', width: 80 },
+ { field: 'created_at', title: '创建时间', width: 170 },
+ { title: '操作', slots: { default: 'action' }, width: 160, fixed: 'right' },
+ ],
+ keepSource: true,
+ pagerConfig: {},
+ proxyConfig: {
+ ajax: {
+ query: async ({ page }, formValues) => {
+ return await getVipLevelList({
+ page: page.currentPage,
+ pageSize: page.pageSize,
+ ...formValues,
+ });
+ },
+ },
+ },
+ border: false,
+ toolbarConfig: {
+ search: true,
+ refresh: true,
+ zoom: true,
+ slots: { buttons: 'toolbar-buttons' },
+ },
+};
diff --git a/apps/web-antd/src/views/system/vip/level/index.vue b/apps/web-antd/src/views/system/vip/level/index.vue
new file mode 100644
index 00000000..82f453ea
--- /dev/null
+++ b/apps/web-antd/src/views/system/vip/level/index.vue
@@ -0,0 +1,222 @@
+
+
+
+
+
+
+
+
+
+
+
![]()
+
+ {{ item.code }}
+
+
+ {{ item.name }}
+ {{ item.code }}
+
+
+ {{ tierLabel(item.tier_group) }}
+
+ {{ item.status === 1 ? '启用' : '禁用' }}
+
+ ¥{{ item.price }}
+
+
+ {{ item.description || '暂无说明' }}
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antd/src/views/system/vip/record/api/index.ts b/apps/web-antd/src/views/system/vip/record/api/index.ts
new file mode 100644
index 00000000..09b2b828
--- /dev/null
+++ b/apps/web-antd/src/views/system/vip/record/api/index.ts
@@ -0,0 +1,7 @@
+import { requestClient } from '#/api/request';
+
+const prefix = 'vip-record/';
+
+export async function getVipRecordList(data: any) {
+ return requestClient.get(`${prefix}list`, { params: data });
+}
diff --git a/apps/web-antd/src/views/system/vip/record/config/search.ts b/apps/web-antd/src/views/system/vip/record/config/search.ts
new file mode 100644
index 00000000..eccc2578
--- /dev/null
+++ b/apps/web-antd/src/views/system/vip/record/config/search.ts
@@ -0,0 +1,33 @@
+import type { VbenFormProps } from '@vben/common-ui';
+
+export const formOptions: VbenFormProps = {
+ layout: 'inline',
+ showResetButton: true,
+ showSubmitButton: true,
+ schemas: [
+ {
+ fieldName: 'store_id',
+ component: 'Input',
+ label: '门店ID',
+ componentProps: { placeholder: '门店ID', allowClear: true },
+ },
+ {
+ fieldName: 'level_code',
+ component: 'Input',
+ label: '等级编码',
+ componentProps: { placeholder: '如 V5', allowClear: true },
+ },
+ {
+ fieldName: 'open_type',
+ component: 'Select',
+ label: '开通方式',
+ componentProps: {
+ allowClear: true,
+ options: [
+ { label: '管理员开通', value: 1 },
+ { label: '门店自购', value: 2 },
+ ],
+ },
+ },
+ ],
+};
diff --git a/apps/web-antd/src/views/system/vip/record/config/table.ts b/apps/web-antd/src/views/system/vip/record/config/table.ts
new file mode 100644
index 00000000..44515199
--- /dev/null
+++ b/apps/web-antd/src/views/system/vip/record/config/table.ts
@@ -0,0 +1,44 @@
+import type { VxeGridProps } from '#/adapter/vxe-table';
+
+import { getVipRecordList } from '../api';
+
+export const gridOptions: VxeGridProps = {
+ columnConfig: { useKey: true },
+ rowConfig: { useKey: true, isHover: true },
+ columns: [
+ { field: 'id', title: 'ID', width: 70 },
+ { field: 'store_id', title: '门店ID', width: 90 },
+ { field: 'store_name', title: '门店', minWidth: 140 },
+ {
+ field: 'vip_badge',
+ title: 'VIP徽标',
+ width: 100,
+ slots: { default: 'vip_badge' },
+ },
+ { field: 'duration_label', title: '会员类型', width: 110 },
+ { field: 'level_name', title: '等级', width: 110 },
+ { field: 'level_code', title: '编码', width: 100 },
+ { field: 'original_price', title: '原价', width: 90 },
+ { field: 'pay_amount', title: '实付', width: 90 },
+ { field: 'open_type_txt', title: '开通方式', width: 110 },
+ { field: 'operator_name', title: '操作人', width: 110 },
+ { field: 'expire_at', title: '到期时间', width: 170 },
+ { field: 'created_at', title: '开通时间', width: 170 },
+ { field: 'remark', title: '备注', minWidth: 120 },
+ ],
+ keepSource: true,
+ pagerConfig: {},
+ proxyConfig: {
+ ajax: {
+ query: async ({ page }, formValues) => {
+ return await getVipRecordList({
+ page: page.currentPage,
+ pageSize: page.pageSize,
+ ...formValues,
+ });
+ },
+ },
+ },
+ border: false,
+ toolbarConfig: { search: true, refresh: true, zoom: true },
+};
diff --git a/apps/web-antd/src/views/system/vip/record/index.vue b/apps/web-antd/src/views/system/vip/record/index.vue
new file mode 100644
index 00000000..e1bc58a8
--- /dev/null
+++ b/apps/web-antd/src/views/system/vip/record/index.vue
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web-antd/src/views/system/vip/store/api/index.ts b/apps/web-antd/src/views/system/vip/store/api/index.ts
new file mode 100644
index 00000000..4e156482
--- /dev/null
+++ b/apps/web-antd/src/views/system/vip/store/api/index.ts
@@ -0,0 +1,19 @@
+import { requestClient } from '#/api/request';
+
+const prefix = 'vip-store/';
+
+export async function getVipStoreList(data: any) {
+ return requestClient.get(`${prefix}list`, { params: data });
+}
+
+export async function getVipStoreInfo(storeId: number) {
+ return requestClient.get(`${prefix}info`, { params: { store_id: storeId } });
+}
+
+export async function openVipStore(data: Record) {
+ return requestClient.post(`${prefix}open`, data);
+}
+
+export async function getVipDurationPresets() {
+ return requestClient.get(`${prefix}duration-presets`);
+}
diff --git a/apps/web-antd/src/views/system/vip/store/components/open-modal.vue b/apps/web-antd/src/views/system/vip/store/components/open-modal.vue
new file mode 100644
index 00000000..2a8b64e9
--- /dev/null
+++ b/apps/web-antd/src/views/system/vip/store/components/open-modal.vue
@@ -0,0 +1,136 @@
+
+
+
+
+
+
diff --git a/apps/web-antd/src/views/system/vip/store/config/search.ts b/apps/web-antd/src/views/system/vip/store/config/search.ts
new file mode 100644
index 00000000..7c842085
--- /dev/null
+++ b/apps/web-antd/src/views/system/vip/store/config/search.ts
@@ -0,0 +1,21 @@
+import type { VbenFormProps } from '@vben/common-ui';
+
+export const formOptions: VbenFormProps = {
+ layout: 'inline',
+ showResetButton: true,
+ showSubmitButton: true,
+ schemas: [
+ {
+ fieldName: 'name',
+ component: 'Input',
+ label: '门店名称',
+ componentProps: { placeholder: '门店名称', allowClear: true },
+ },
+ {
+ fieldName: 'level_code',
+ component: 'Input',
+ label: '等级编码',
+ componentProps: { placeholder: '如 V5 / V0', allowClear: true },
+ },
+ ],
+};
diff --git a/apps/web-antd/src/views/system/vip/store/config/table.ts b/apps/web-antd/src/views/system/vip/store/config/table.ts
new file mode 100644
index 00000000..002cb39e
--- /dev/null
+++ b/apps/web-antd/src/views/system/vip/store/config/table.ts
@@ -0,0 +1,68 @@
+import type { VxeGridProps } from '#/adapter/vxe-table';
+
+import { getVipStoreList } from '../api';
+
+export const gridOptions: VxeGridProps = {
+ columnConfig: { useKey: true },
+ rowConfig: { useKey: true, isHover: true },
+ columns: [
+ { field: 'store_id', title: '门店ID', width: 90 },
+ { field: 'store_name', title: '门店名称', minWidth: 160 },
+ {
+ field: 'vip_badge',
+ title: 'VIP徽标',
+ width: 100,
+ slots: { default: 'vip_badge' },
+ },
+ {
+ field: 'duration_label',
+ title: '会员类型',
+ width: 110,
+ formatter: ({ row }) => row.vip?.duration_label || '-',
+ },
+ {
+ field: 'level_name',
+ title: '会员等级',
+ minWidth: 140,
+ formatter: ({ row }) =>
+ row.vip
+ ? `${row.vip.level_name || ''}(${row.vip.level_code || ''})`
+ : '-',
+ },
+ {
+ field: 'expire_at',
+ title: '到期时间',
+ width: 170,
+ formatter: ({ row }) => {
+ if (!row.vip) return '-';
+ if (row.vip.is_lifetime) return '终身';
+ const expireAt = Number(row.vip.expire_at || 0);
+ if (!expireAt) {
+ return row.vip.level_code === 'V0' ? '-' : '终身';
+ }
+ if (typeof row.vip.expire_at === 'string' && String(row.vip.expire_at).includes('-')) {
+ return row.vip.expire_at;
+ }
+ const d = new Date(expireAt * 1000);
+ const pad = (n: number) => String(n).padStart(2, '0');
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
+ },
+ },
+ { title: '操作', slots: { default: 'action' }, width: 100, fixed: 'right' },
+ ],
+ keepSource: true,
+ pagerConfig: {},
+ proxyConfig: {
+ ajax: {
+ query: async ({ page }, formValues) => {
+ return await getVipStoreList({
+ page: page.currentPage,
+ pageSize: page.pageSize,
+ ...formValues,
+ });
+ },
+ },
+ },
+ border: false,
+ toolbarConfig: { search: true, refresh: true, zoom: true },
+};
diff --git a/apps/web-antd/src/views/system/vip/store/index.vue b/apps/web-antd/src/views/system/vip/store/index.vue
new file mode 100644
index 00000000..51da8188
--- /dev/null
+++ b/apps/web-antd/src/views/system/vip/store/index.vue
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+