feat: 添加产品管理功能,包括产品列表和添加产品表单

This commit is contained in:
2026-01-19 17:25:48 +07:00
parent 724f0a47e9
commit 9b36a114b3
11 changed files with 310 additions and 17 deletions

2
.env
View File

@@ -17,7 +17,7 @@ VITE_ICON_LOCAL_PREFIX=icon-local
VITE_AUTH_ROUTE_MODE=static
# static auth route home
VITE_ROUTE_HOME=/home
VITE_ROUTE_HOME=home
# default menu icon
VITE_MENU_ICON=mdi:menu

View File

@@ -51,7 +51,7 @@
"@better-scroll/core": "2.5.1",
"@elysiajs/eden": "^1.4.5",
"@iconify/vue": "5.0.0",
"@riwa/api-types": "http://192.168.1.7:9528/api/riwa-eden-0.0.145.tgz",
"@riwa/api-types": "http://192.168.1.2:9538/api/capp-eden-0.0.9.tgz",
"@sa/axios": "workspace:*",
"@sa/color": "workspace:*",
"@sa/hooks": "workspace:*",

24
pnpm-lock.yaml generated
View File

@@ -18,8 +18,8 @@ importers:
specifier: 5.0.0
version: 5.0.0(vue@3.5.25(typescript@5.9.3))
'@riwa/api-types':
specifier: http://192.168.1.7:9528/api/riwa-eden-0.0.145.tgz
version: '@riwa/eden@http://192.168.1.7:9528/api/riwa-eden-0.0.145.tgz(@elysiajs/eden@1.4.5(elysia@1.4.19(@sinclair/typebox@0.34.41)(exact-mirror@0.2.5(@sinclair/typebox@0.34.41))(file-type@21.1.1)(openapi-types@12.1.3)(typescript@5.9.3)))'
specifier: http://192.168.1.2:9538/api/capp-eden-0.0.9.tgz
version: '@capp/eden@http://192.168.1.2:9538/api/capp-eden-0.0.9.tgz(@elysiajs/eden@1.4.5(elysia@1.4.19(@sinclair/typebox@0.34.41)(exact-mirror@0.2.5(@sinclair/typebox@0.34.41))(file-type@21.1.1)(openapi-types@12.1.3)(typescript@5.9.3)))'
'@sa/axios':
specifier: workspace:*
version: link:packages/axios
@@ -496,6 +496,12 @@ packages:
'@borewit/text-codec@0.1.1':
resolution: {integrity: sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA==}
'@capp/eden@http://192.168.1.2:9538/api/capp-eden-0.0.9.tgz':
resolution: {tarball: http://192.168.1.2:9538/api/capp-eden-0.0.9.tgz}
version: 0.0.9
peerDependencies:
'@elysiajs/eden': ^1.4.6
'@codemirror/autocomplete@6.20.0':
resolution: {integrity: sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg==}
@@ -1230,12 +1236,6 @@ packages:
'@quansync/fs@0.1.6':
resolution: {integrity: sha512-zoA8SqQO11qH9H8FCBR7NIbowYARIPmBz3nKjgAaOUDi/xPAAu1uAgebtV7KXHTc6CDZJVRZ1u4wIGvY5CWYaw==}
'@riwa/eden@http://192.168.1.7:9528/api/riwa-eden-0.0.145.tgz':
resolution: {tarball: http://192.168.1.7:9528/api/riwa-eden-0.0.145.tgz}
version: 0.0.145
peerDependencies:
'@elysiajs/eden': ^1.4.5
'@rolldown/pluginutils@1.0.0-beta.50':
resolution: {integrity: sha512-5e76wQiQVeL1ICOZVUg4LSOVYg9jyhGCin+icYozhsUzM+fHE7kddi1bdiE0jwVqTfkjba3jUFbEkoC9WkdvyA==}
@@ -4871,6 +4871,10 @@ snapshots:
'@borewit/text-codec@0.1.1': {}
'@capp/eden@http://192.168.1.2:9538/api/capp-eden-0.0.9.tgz(@elysiajs/eden@1.4.5(elysia@1.4.19(@sinclair/typebox@0.34.41)(exact-mirror@0.2.5(@sinclair/typebox@0.34.41))(file-type@21.1.1)(openapi-types@12.1.3)(typescript@5.9.3)))':
dependencies:
'@elysiajs/eden': 1.4.5(elysia@1.4.19(@sinclair/typebox@0.34.41)(exact-mirror@0.2.5(@sinclair/typebox@0.34.41))(file-type@21.1.1)(openapi-types@12.1.3)(typescript@5.9.3))
'@codemirror/autocomplete@6.20.0':
dependencies:
'@codemirror/language': 6.12.1
@@ -5651,10 +5655,6 @@ snapshots:
dependencies:
quansync: 0.3.0
'@riwa/eden@http://192.168.1.7:9528/api/riwa-eden-0.0.145.tgz(@elysiajs/eden@1.4.5(elysia@1.4.19(@sinclair/typebox@0.34.41)(exact-mirror@0.2.5(@sinclair/typebox@0.34.41))(file-type@21.1.1)(openapi-types@12.1.3)(typescript@5.9.3)))':
dependencies:
'@elysiajs/eden': 1.4.5(elysia@1.4.19(@sinclair/typebox@0.34.41)(exact-mirror@0.2.5(@sinclair/typebox@0.34.41))(file-type@21.1.1)(openapi-types@12.1.3)(typescript@5.9.3))
'@rolldown/pluginutils@1.0.0-beta.50': {}
'@rolldown/pluginutils@1.0.0-beta.53': {}

View File

@@ -228,7 +228,8 @@ const local: App.I18n.Schema = {
404: 'Page Not Found',
500: 'Server Error',
'iframe-page': 'Iframe',
home: 'Home'
home: 'Home',
product: 'Product'
},
page: {
login: {

View File

@@ -224,7 +224,8 @@ const local: App.I18n.Schema = {
404: '页面不存在',
500: '服务器错误',
'iframe-page': '外链页面',
home: '首页'
home: '首页',
product: '产品管理'
},
page: {
login: {

View File

@@ -21,4 +21,5 @@ export const views: Record<LastLevelRouteKey, RouteComponent | (() => Promise<Ro
"iframe-page": () => import("@/views/_builtin/iframe-page/[url].vue"),
login: () => import("@/views/_builtin/login/index.vue"),
home: () => import("@/views/home/index.vue"),
product: () => import("@/views/product/index.vue"),
};

View File

@@ -72,5 +72,14 @@ export const generatedRoutes: GeneratedRoute[] = [
constant: true,
hideInMenu: true
}
},
{
name: 'product',
path: '/product',
component: 'layout.base$view.product',
meta: {
title: 'product',
i18nKey: 'route.product'
}
}
];

View File

@@ -168,7 +168,8 @@ const routeMap: RouteMap = {
"500": "/500",
"home": "/home",
"iframe-page": "/iframe-page/:url",
"login": "/login/:module(pwd-login|code-login|register|reset-pwd|bind-wechat)?"
"login": "/login/:module(pwd-login|code-login|register|reset-pwd|bind-wechat)?",
"product": "/product"
};
/**

View File

@@ -23,6 +23,7 @@ declare module "@elegant-router/types" {
"home": "/home";
"iframe-page": "/iframe-page/:url";
"login": "/login/:module(pwd-login|code-login|register|reset-pwd|bind-wechat)?";
"product": "/product";
};
/**
@@ -60,6 +61,7 @@ declare module "@elegant-router/types" {
| "home"
| "iframe-page"
| "login"
| "product"
>;
/**
@@ -82,6 +84,7 @@ declare module "@elegant-router/types" {
| "iframe-page"
| "login"
| "home"
| "product"
>;
/**

View File

@@ -0,0 +1,146 @@
<script lang="ts" setup>
import { ref, useTemplateRef } from 'vue';
import type { FormInst, FormRules } from 'naive-ui';
import { client, safeClient } from '@/service/api';
type Body = CommonType.TreatyBody<typeof client.api.admin.subscription.products.post>;
defineOptions({
name: 'ProductAdd'
});
const emit = defineEmits<{
(e: 'close'): void;
}>();
const formInst = useTemplateRef<FormInst>('formInst');
const form = ref<Body>({
name: '',
price: '',
cycleDays: 1,
maturityYield: '',
subscribeStartAt: new Date(),
subscribeEndAt: new Date(),
description: '',
isActive: true,
reformPioneerAllowance: '',
sortOrder: 0
});
const rules: FormRules = {
name: [{ required: true, message: '请输入产品名称', trigger: ['blur', 'input'] }],
price: [{ required: true, message: '请输入产品价格', trigger: ['blur', 'change'] }],
maturityYield: [{ required: true, message: '请输入到期收益率', trigger: ['blur', 'change'] }],
subscribeStartAt: [{ required: true, message: '请选择订阅时间范围', trigger: ['blur', 'change'] }]
};
function handleSubmit() {
formInst.value?.validate(async errors => {
if (!errors) {
await safeClient(() => client.api.admin.subscription.products.post(form.value));
emit('close');
}
});
}
</script>
<template>
<div class="my-10">
<NForm
ref="formInst"
:model="form"
label-width="140px"
label-placement="left"
:rules="rules"
require-mark-placement="left"
>
<NFormItem path="name" label="产品名称">
<NInput v-model:value="form.name" placeholder="请输入产品名称" />
</NFormItem>
<NGrid :cols="2" :x-gap="12">
<NFormItemGi path="price" label="产品价格">
<NInputNumber
:value="Number(form.price)"
:min="0"
:precision="2"
class="w-full"
@update:value="val => (form.price = String(val))"
>
<template #suffix></template>
</NInputNumber>
</NFormItemGi>
<NFormItemGi path="cycleDays" label="周期天数">
<NInputNumber v-model:value="form.cycleDays" :min="1" :precision="0" class="w-full">
<template #suffix></template>
</NInputNumber>
</NFormItemGi>
</NGrid>
<NGrid :cols="2" :x-gap="12">
<NFormItemGi path="maturityYield" label="到期收益率">
<NInputNumber
:value="Number(form.maturityYield)"
:min="0"
:max="100"
:step="0.01"
class="w-full"
@update:value="val => (form.maturityYield = String(val))"
>
<template #suffix>%</template>
</NInputNumber>
</NFormItemGi>
<NFormItemGi path="reformPioneerAllowance" label="改革先锋津贴">
<NInputNumber
:value="Number(form.reformPioneerAllowance)"
:min="0"
:precision="2"
class="w-full"
@update:value="val => (form.reformPioneerAllowance = String(val))"
>
<template #suffix></template>
</NInputNumber>
</NFormItemGi>
</NGrid>
<NFormItem label="订阅时间范围">
<NDatePicker
:value="[form.subscribeStartAt.valueOf(), form.subscribeEndAt.valueOf()]"
type="datetimerange"
clearable
format="yyyy-MM-dd HH:mm:ss"
class="w-full"
@update:value="
val => {
form.subscribeStartAt = new Date(val[0]);
form.subscribeEndAt = new Date(val[1]);
}
"
/>
</NFormItem>
<NFormItem path="description" label="产品描述">
<NInput v-model:value="form.description" type="textarea" placeholder="请输入产品描述" :rows="3" />
</NFormItem>
<NGrid :cols="2" :x-gap="12">
<NFormItemGi path="isActive" label="是否启用">
<NSwitch v-model:value="form.isActive" />
</NFormItemGi>
<NFormItemGi path="sortOrder" label="排序顺序">
<NInputNumber v-model:value="form.sortOrder" :min="0" :precision="0" class="w-full" />
</NFormItemGi>
</NGrid>
<NSpace justify="end">
<NButton type="primary" ghost @click="$emit('close')"> </NButton>
<NButton type="primary" @click="handleSubmit"> </NButton>
</NSpace>
</NForm>
</div>
</template>
<style lang="css" scoped></style>

131
src/views/product/index.vue Normal file
View File

@@ -0,0 +1,131 @@
<script lang="ts" setup>
import { h, useTemplateRef } from 'vue';
import { useDialog, useMessage } from 'naive-ui';
import dayjs from 'dayjs';
import { client, safeClient } from '@/service/api';
import type { TableBaseColumns, TableFetchData, TableInst } from '@/components/table';
import Add from './components/add.vue';
const message = useMessage();
const dialog = useDialog();
const tableInst = useTemplateRef<TableInst>('tableInst');
const fetchData: TableFetchData = ({ pagination, filter }) => {
return safeClient(() =>
client.api.admin.subscription.products.get({
query: {
...pagination,
...filter
}
})
);
};
const columns: TableBaseColumns = [
{
key: 'id',
title: 'ID',
width: 120
},
{
key: 'name',
title: '名称'
},
{
key: 'price',
title: '价格(元)',
render(row) {
return Number(row.price);
}
},
{
key: 'cycleDays',
title: '周期(天数)'
},
{
key: 'maturityYield',
title: '到期收益率',
render(row) {
return `${Number(row.maturityYield)}%`;
}
},
{
key: 'reformPioneerAllowance',
title: '改革先锋津贴(元)',
render(row) {
return Number(row.reformPioneerAllowance);
}
},
{
key: 'subscribeStartAt',
title: '订阅开始时间',
render(row: any) {
return dayjs(row.subscribeStartAt).format('YYYY-MM-DD HH:mm');
}
},
{
key: 'subscribeEndAt',
title: '订阅结束时间',
render(row: any) {
return dayjs(row.subscribeEndAt).format('YYYY-MM-DD HH:mm');
}
},
{
key: 'description',
title: '描述'
},
{
key: 'isActive',
title: '是否激活',
render(row) {
return row.isActive ? '是' : '否';
}
},
{
key: 'sortOrder',
title: '排序'
},
{
key: 'createdAt',
title: '创建时间',
render(row: any) {
return dayjs(row.createdAt).format('YYYY-MM-DD HH:mm');
}
},
{
key: 'operations',
title: '操作',
width: 120,
fixed: 'right',
operations: row => [
{
contentText: '编辑',
size: 'small',
onClick() {}
}
]
}
];
function handleAdd() {
const d = dialog.create({
title: '添加产品',
showIcon: false,
content: () =>
h(Add, {
onClose: () => {
d.destroy();
tableInst.value?.reload();
}
}),
style: { width: '800px' }
});
}
</script>
<template>
<TableBase ref="tableInst" :fetch-data="fetchData" :columns="columns" @add="handleAdd" />
</template>
<style lang="css" scoped></style>