Merge remote-tracking branch 'yudao/dev' into dev

This commit is contained in:
jason
2025-08-25 09:45:03 +08:00
330 changed files with 20106 additions and 3544 deletions

1
.gitignore vendored
View File

@@ -49,3 +49,4 @@ vite.config.ts.*
*.sln
*.sw?
.history
.cursor

View File

@@ -21,7 +21,8 @@
// CSS 变量提示
"vunguyentuan.vscode-css-variables",
// 在 package.json 中显示 PNPM catalog 的版本
"antfu.pnpm-catalog-lens"
"antfu.pnpm-catalog-lens",
"augment.vscode-augment"
],
"unwantedRecommendations": [
// 和 volar 冲突

View File

@@ -41,24 +41,24 @@
| 框架 | 说明 | 版本 |
| --- | --- | --- |
| [Vue](https://staging-cn.vuejs.org/) | vue框架 | 3.5.13 |
| [Vite](https://cn.vitejs.dev//) | 开发与构建工具 | 6.2.5 |
| [Vue](https://staging-cn.vuejs.org/) | vue框架 | 3.5.17 |
| [Vite](https://cn.vitejs.dev//) | 开发与构建工具 | 7.1.2 |
| [Ant Design Vue](https://www.antdv.com/) | Ant Design Vue | 4.2.6 |
| [Element Plus](https://element-plus.org/zh-CN/) | Element Plus | 2.9.7 |
| [Naive UI](https://www.naiveui.com/) | Naive UI | 2.41.0 |
| [Element Plus](https://element-plus.org/zh-CN/) | Element Plus | 2.10.2 |
| [Naive UI](https://www.naiveui.com/) | Naive UI | 2.42.0 |
| [TypeScript](https://www.typescriptlang.org/docs/) | JavaScript 超集 | 5.8.3 |
| [pinia](https://pinia.vuejs.org/) | Vue 存储库替代 vuex5 | 2.3.1 |
| [vueuse](https://vueuse.org/) | 常用工具集 | 12.8.2 |
| [vue-i18n](https://kazupon.github.io/vue-i18n/zh/introduction.html/) | 国际化 | 11.1.3 |
| [vue-router](https://router.vuejs.org/) | Vue 路由 | 4.5.0 |
| [pinia](https://pinia.vuejs.org/) | Vue 存储库替代 vuex5 | 3.0.3 |
| [vueuse](https://vueuse.org/) | 常用工具集 | 13.4.0 |
| [vue-i18n](https://kazupon.github.io/vue-i18n/zh/introduction.html/) | 国际化 | 11.1.7 |
| [vue-router](https://router.vuejs.org/) | Vue 路由 | 4.5.1 |
| [Tailwind CSS](https://tailwindcss.com/) | 原子 CSS | 3.4.17 |
| [Iconify](https://icon-sets.iconify.design/) | 在线图标库 | 2.2.324 |
| [Iconify](https://icon-sets.iconify.design/) | 在线图标库 | 2.2.354 |
| [TinyMCE](https://www.tiny.cloud/) | 富文本编辑器 | 6.1.0 |
| [Echarts](https://echarts.apache.org/) | 图表库 | 5.6.0 |
| [axios](https://axios-http.com/) | http客户端 | 1.8.4 |
| [axios](https://axios-http.com/) | http客户端 | 1.10.0 |
| [dayjs](https://day.js.org/) | 日期处理库 | 1.11.13 |
| [vee-validate](https://vee-validate.logaretm.com/) | 表单验证 | 4.15.0 |
| [zod](https://zod.dev/) | 数据验证 | 3.24.2 |
| [vee-validate](https://vee-validate.logaretm.com/) | 表单验证 | 4.15.1 |
| [zod](https://zod.dev/) | 数据验证 | 3.25.67 |
## 🔥 后端架构

View File

@@ -1,5 +1,7 @@
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse } from '~/utils/response';
import { MOCK_CODES } from '~/utils/mock-data';
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
export default eventHandler((event) => {
const userinfo = verifyAccessToken(event);

View File

@@ -1,9 +1,15 @@
import { defineEventHandler, readBody, setResponseStatus } from 'h3';
import {
clearRefreshTokenCookie,
setRefreshTokenCookie,
} from '~/utils/cookie-utils';
import { generateAccessToken, generateRefreshToken } from '~/utils/jwt-utils';
import { forbiddenResponse } from '~/utils/response';
import { MOCK_USERS } from '~/utils/mock-data';
import {
forbiddenResponse,
useResponseError,
useResponseSuccess,
} from '~/utils/response';
export default defineEventHandler(async (event) => {
const { password, username } = await readBody(event);

View File

@@ -1,7 +1,9 @@
import { defineEventHandler } from 'h3';
import {
clearRefreshTokenCookie,
getRefreshTokenFromCookie,
} from '~/utils/cookie-utils';
import { useResponseSuccess } from '~/utils/response';
export default defineEventHandler(async (event) => {
const refreshToken = getRefreshTokenFromCookie(event);

View File

@@ -1,9 +1,11 @@
import { defineEventHandler } from 'h3';
import {
clearRefreshTokenCookie,
getRefreshTokenFromCookie,
setRefreshTokenCookie,
} from '~/utils/cookie-utils';
import { verifyRefreshToken } from '~/utils/jwt-utils';
import { generateAccessToken, verifyRefreshToken } from '~/utils/jwt-utils';
import { MOCK_USERS } from '~/utils/mock-data';
import { forbiddenResponse } from '~/utils/response';
export default defineEventHandler(async (event) => {

View File

@@ -1,3 +1,7 @@
import { eventHandler, setHeader } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse } from '~/utils/response';
export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {

View File

@@ -1,5 +1,7 @@
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse } from '~/utils/response';
import { MOCK_MENUS } from '~/utils/mock-data';
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(event);

View File

@@ -1,3 +1,6 @@
import { eventHandler, getQuery, setResponseStatus } from 'h3';
import { useResponseError } from '~/utils/response';
export default eventHandler((event) => {
const { status } = getQuery(event);
setResponseStatus(event, Number(status));

View File

@@ -1,3 +1,4 @@
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import {
sleep,

View File

@@ -1,3 +1,4 @@
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import {
sleep,

View File

@@ -1,3 +1,4 @@
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import {
sleep,

View File

@@ -1,4 +1,5 @@
import { faker } from '@faker-js/faker';
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';

View File

@@ -1,3 +1,4 @@
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { MOCK_MENU_LIST } from '~/utils/mock-data';
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';

View File

@@ -1,6 +1,7 @@
import { eventHandler, getQuery } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { MOCK_MENU_LIST } from '~/utils/mock-data';
import { unAuthorizedResponse } from '~/utils/response';
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
const namesMap: Record<string, any> = {};

View File

@@ -1,6 +1,7 @@
import { eventHandler, getQuery } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { MOCK_MENU_LIST } from '~/utils/mock-data';
import { unAuthorizedResponse } from '~/utils/response';
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
const pathMap: Record<string, any> = { '/': 0 };

View File

@@ -1,4 +1,5 @@
import { faker } from '@faker-js/faker';
import { eventHandler, getQuery } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { getMenuIds, MOCK_MENU_LIST } from '~/utils/mock-data';
import { unAuthorizedResponse, usePageResponseSuccess } from '~/utils/response';

View File

@@ -1,6 +1,11 @@
import { faker } from '@faker-js/faker';
import { eventHandler, getQuery } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse, usePageResponseSuccess } from '~/utils/response';
import {
sleep,
unAuthorizedResponse,
usePageResponseSuccess,
} from '~/utils/response';
function generateMockDataList(count: number) {
const dataList = [];
@@ -44,30 +49,69 @@ export default eventHandler(async (event) => {
await sleep(600);
const { page, pageSize, sortBy, sortOrder } = getQuery(event);
// 规范化分页参数,处理 string[]
const pageRaw = Array.isArray(page) ? page[0] : page;
const pageSizeRaw = Array.isArray(pageSize) ? pageSize[0] : pageSize;
const pageNumber = Math.max(
1,
Number.parseInt(String(pageRaw ?? '1'), 10) || 1,
);
const pageSizeNumber = Math.min(
100,
Math.max(1, Number.parseInt(String(pageSizeRaw ?? '10'), 10) || 10),
);
const listData = structuredClone(mockData);
if (sortBy && Reflect.has(listData[0], sortBy as string)) {
// 规范化 query 入参,兼容 string[]
const sortKeyRaw = Array.isArray(sortBy) ? sortBy[0] : sortBy;
const sortOrderRaw = Array.isArray(sortOrder) ? sortOrder[0] : sortOrder;
// 检查 sortBy 是否是 listData 元素的合法属性键
if (
typeof sortKeyRaw === 'string' &&
listData[0] &&
Object.prototype.hasOwnProperty.call(listData[0], sortKeyRaw)
) {
// 定义数组元素的类型
type ItemType = (typeof listData)[0];
const sortKey = sortKeyRaw as keyof ItemType; // 将 sortBy 断言为合法键
const isDesc = sortOrderRaw === 'desc';
listData.sort((a, b) => {
if (sortOrder === 'asc') {
if (sortBy === 'price') {
return (
Number.parseFloat(a[sortBy as string]) -
Number.parseFloat(b[sortBy as string])
);
const aValue = a[sortKey] as unknown;
const bValue = b[sortKey] as unknown;
let result = 0;
if (typeof aValue === 'number' && typeof bValue === 'number') {
result = aValue - bValue;
} else if (aValue instanceof Date && bValue instanceof Date) {
result = aValue.getTime() - bValue.getTime();
} else if (typeof aValue === 'boolean' && typeof bValue === 'boolean') {
if (aValue === bValue) {
result = 0;
} else {
return a[sortBy as string] > b[sortBy as string] ? 1 : -1;
result = aValue ? 1 : -1;
}
} else {
if (sortBy === 'price') {
return (
Number.parseFloat(b[sortBy as string]) -
Number.parseFloat(a[sortBy as string])
);
} else {
return a[sortBy as string] < b[sortBy as string] ? 1 : -1;
}
const aStr = String(aValue);
const bStr = String(bValue);
const aNum = Number(aStr);
const bNum = Number(bStr);
result =
Number.isFinite(aNum) && Number.isFinite(bNum)
? aNum - bNum
: aStr.localeCompare(bStr, undefined, {
numeric: true,
sensitivity: 'base',
});
}
return isDesc ? -result : result;
});
}
return usePageResponseSuccess(page as string, pageSize as string, listData);
return usePageResponseSuccess(
String(pageNumber),
String(pageSizeNumber),
listData,
);
});

View File

@@ -1 +1,3 @@
import { defineEventHandler } from 'h3';
export default defineEventHandler(() => 'Test get handler');

View File

@@ -1 +1,3 @@
import { defineEventHandler } from 'h3';
export default defineEventHandler(() => 'Test post handler');

View File

@@ -1,5 +1,6 @@
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse } from '~/utils/response';
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
export default eventHandler((event) => {
const userinfo = verifyAccessToken(event);

View File

@@ -1,5 +1,6 @@
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse } from '~/utils/response';
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
export default eventHandler((event) => {
const userinfo = verifyAccessToken(event);

View File

@@ -1,3 +1,4 @@
import { defineEventHandler } from 'h3';
import { forbiddenResponse, sleep } from '~/utils/response';
export default defineEventHandler(async (event) => {

View File

@@ -1,3 +1,5 @@
import { defineEventHandler } from 'h3';
export default defineEventHandler(() => {
return `
<h1>Hello Vben Admin</h1>

View File

@@ -1,5 +1,7 @@
import type { EventHandlerRequest, H3Event } from 'h3';
import { deleteCookie, getCookie, setCookie } from 'h3';
export function clearRefreshTokenCookie(event: H3Event<EventHandlerRequest>) {
deleteCookie(event, 'jwt', {
httpOnly: true,

View File

@@ -1,8 +1,11 @@
import type { EventHandlerRequest, H3Event } from 'h3';
import type { UserInfo } from './mock-data';
import { getHeader } from 'h3';
import jwt from 'jsonwebtoken';
import { UserInfo } from './mock-data';
import { MOCK_USERS } from './mock-data';
// TODO: Replace with your own secret key
const ACCESS_TOKEN_SECRET = 'access_token_secret';
@@ -31,12 +34,22 @@ export function verifyAccessToken(
return null;
}
const token = authHeader.split(' ')[1];
const tokenParts = authHeader.split(' ');
if (tokenParts.length !== 2) {
return null;
}
const token = tokenParts[1] as string;
try {
const decoded = jwt.verify(token, ACCESS_TOKEN_SECRET) as UserPayload;
const decoded = jwt.verify(
token,
ACCESS_TOKEN_SECRET,
) as unknown as UserPayload;
const username = decoded.username;
const user = MOCK_USERS.find((item) => item.username === username);
if (!user) {
return null;
}
const { password: _pwd, ...userinfo } = user;
return userinfo;
} catch {
@@ -50,7 +63,12 @@ export function verifyRefreshToken(
try {
const decoded = jwt.verify(token, REFRESH_TOKEN_SECRET) as UserPayload;
const username = decoded.username;
const user = MOCK_USERS.find((item) => item.username === username);
const user = MOCK_USERS.find(
(item) => item.username === username,
) as UserInfo;
if (!user) {
return null;
}
const { password: _pwd, ...userinfo } = user;
return userinfo;
} catch {

View File

@@ -1,5 +1,7 @@
import type { EventHandlerRequest, H3Event } from 'h3';
import { setResponseStatus } from 'h3';
export function useResponseSuccess<T = any>(data: T) {
return {
code: 0,

View File

@@ -24,3 +24,12 @@ VITE_APP_BAIDU_CODE = e98f2eab6ceb8688bc6d8fc5332ff093
# GoView域名
VITE_GOVIEW_URL='http://127.0.0.1:3000'
# API 加解密
VITE_APP_API_ENCRYPT_ENABLE = true
VITE_APP_API_ENCRYPT_HEADER = X-Api-Encrypt
VITE_APP_API_ENCRYPT_ALGORITHM = AES
VITE_APP_API_ENCRYPT_REQUEST_KEY = 52549111389893486934626385991395
VITE_APP_API_ENCRYPT_RESPONSE_KEY = 96103715984234343991809655248883
# VITE_APP_API_ENCRYPT_REQUEST_KEY = MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCls2rIpnGdYnLFgz1XU13GbNQ5DloyPpvW00FPGjqn5Z6JpK+kDtVlnkhwR87iRrE5Vf2WNqRX6vzbLSgveIQY8e8oqGCb829myjf1MuI+ZzN4ghf/7tEYhZJGPI9AbfxFqBUzm+kR3/HByAI22GLT96WM26QiMK8n3tIP/yiLswIDAQAB
# VITE_APP_API_ENCRYPT_RESPONSE_KEY = MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAOH8IfIFxL/MR9XIg1UDv5z1fGXQI93E8wrU4iPFovL/sEt9uSgSkjyidC2O7N+m7EKtoN6b1u7cEwXSkwf3kfK0jdWLSQaNpX5YshqXCBzbDfugDaxuyYrNA4/tIMs7mzZAk0APuRXB35Dmupou7Yw7TFW/7QhQmGfzeEKULQvnAgMBAAECgYAw8LqlQGyQoPv5p3gRxEMOCfgL0JzD3XBJKztiRd35RDh40Nx1ejgjW4dPioFwGiVWd2W8cAGHLzALdcQT2KDJh+T/tsd4SPmI6uSBBK6Ff2DkO+kFFcuYvfclQQKqxma5CaZOSqhgenacmgTMFeg2eKlY3symV6JlFNu/IKU42QJBAOhxAK/Eq3e61aYQV2JSguhMR3b8NXJJRroRs/QHEanksJtl+M+2qhkC9nQVXBmBkndnkU/l2tYcHfSBlAyFySMCQQD445tgm/J2b6qMQmuUGQAYDN8FIkHjeKmha+l/fv0igWm8NDlBAem91lNDIPBUzHL1X1+pcts5bjmq99YdOnhtAkAg2J8dN3B3idpZDiQbC8fd5bGPmdI/pSUudAP27uzLEjr2qrE/QPPGdwm2m7IZFJtK7kK1hKio6u48t/bg0iL7AkEAuUUs94h+v702Fnym+jJ2CHEkXvz2US8UDs52nWrZYiM1o1y4tfSHm8H8bv8JCAa9GHyriEawfBraILOmllFdLQJAQSRZy4wmlaG48MhVXodB85X+VZ9krGXZ2TLhz7kz9iuToy53l9jTkESt6L5BfBDCVdIwcXLYgK+8KFdHN5W7HQ==

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/web-antd",
"version": "5.5.8",
"version": "5.5.9",
"homepage": "https://vben.pro",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {
@@ -47,7 +47,6 @@
"@vueuse/integrations": "catalog:",
"ant-design-vue": "catalog:",
"cropperjs": "catalog:",
"crypto-js": "catalog:",
"dayjs": "catalog:",
"highlight.js": "catalog:",
"pinia": "catalog:",

View File

@@ -64,7 +64,11 @@ export namespace AuthApi {
/** 登录 */
export async function loginApi(data: AuthApi.LoginParams) {
return requestClient.post<AuthApi.LoginResult>('/system/auth/login', data);
return requestClient.post<AuthApi.LoginResult>('/system/auth/login', data, {
headers: {
isEncrypt: false,
},
});
}
/** 刷新 accessToken */

View File

@@ -4,11 +4,12 @@ export namespace ErpProductCategoryApi {
/** ERP 产品分类信息 */
export interface ProductCategory {
id?: number; // 分类编号
parentId: number; // 父分类编号
parentId?: number; // 父分类编号
name: string; // 分类名称
code: string; // 分类编码
sort: number; // 分类排序
status: number; // 开启状态
code?: string; // 分类编码
sort?: number; // 分类排序
status?: number; // 开启状态
children?: ProductCategory[]; // 子分类
}
}

View File

@@ -2,7 +2,7 @@ import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
namespace ErpStockOutApi {
export namespace ErpStockOutApi {
/** 其它出库单信息 */
export interface StockOut {
id?: number; // 出库编号
@@ -13,6 +13,24 @@ namespace ErpStockOutApi {
totalPrice: number; // 合计金额,单位:元
status: number; // 状态
remark: string; // 备注
fileUrl?: string; // 附件
items?: StockOutItem[]; // 出库产品清单
}
/** 其它出库单产品信息 */
export interface StockOutItem {
id?: number; // 编号
warehouseId?: number; // 仓库编号
productId?: number; // 产品编号
productName?: string; // 产品名称
productUnitId?: number; // 产品单位编号
productUnitName?: string; // 产品单位名称
productBarCode?: string; // 产品条码
count: number; // 数量
productPrice: number; // 产品单价
totalPrice: number; // 总价
stockCount?: number; // 库存数量
remark?: string; // 备注
}
/** 其它出库单分页查询参数 */

View File

@@ -16,6 +16,7 @@ export namespace InfraFileConfigApi {
accessKey?: string;
accessSecret?: string;
pathStyle?: boolean;
enablePublicAccess?: boolean;
domain: string;
}

View File

@@ -12,6 +12,7 @@ import {
RequestClient,
} from '@vben/request';
import { useAccessStore } from '@vben/stores';
import { createApiEncrypt } from '@vben/utils';
import { message } from 'ant-design-vue';
@@ -21,6 +22,7 @@ import { refreshTokenApi } from './core';
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
const tenantEnable = isTenantEnable();
const apiEncrypt = createApiEncrypt(import.meta.env);
function createRequestClient(baseURL: string, options?: RequestClientOptions) {
const client = new RequestClient({
@@ -84,10 +86,46 @@ function createRequestClient(baseURL: string, options?: RequestClientOptions) {
config.headers['visit-tenant-id'] = tenantEnable
? accessStore.visitTenantId
: undefined;
// 是否 API 加密
if ((config.headers || {}).isEncrypt) {
try {
// 加密请求数据
if (config.data) {
config.data = apiEncrypt.encryptRequest(config.data);
// 设置加密标识头
config.headers[apiEncrypt.getEncryptHeader()] = 'true';
}
} catch (error) {
console.error('请求数据加密失败:', error);
throw error;
}
}
return config;
},
});
// API 解密响应拦截器
client.addResponseInterceptor({
fulfilled: (response) => {
// 检查是否需要解密响应数据
const encryptHeader = apiEncrypt.getEncryptHeader();
const isEncryptResponse =
response.headers[encryptHeader] === 'true' ||
response.headers[encryptHeader.toLowerCase()] === 'true';
if (isEncryptResponse && typeof response.data === 'string') {
try {
// 解密响应数据
response.data = apiEncrypt.decryptResponse(response.data);
} catch (error) {
console.error('响应数据解密失败:', error);
throw new Error(`响应数据解密失败: ${(error as Error).message}`);
}
}
return response;
},
});
// 处理返回的响应数据格式
client.addResponseInterceptor(
defaultResponseInterceptor({

View File

@@ -12,7 +12,7 @@ export namespace SystemTenantApi {
contactMobile: string;
accountCount: number;
expireTime: Date;
website: string;
websites: string[];
status: number;
}
}

View File

@@ -1,2 +1,2 @@
export { default as Tinyflow } from './tinyflow.vue';
export { default as Tinyflow } from './Tinyflow.vue';
export * from './ui/typing';

View File

@@ -10,7 +10,7 @@ import { isNumber } from '@vben/utils';
import { Button, Input, Select } from 'ant-design-vue';
import { testWorkflow } from '#/api/ai/workflow';
import { Tinyflow } from '#/components/tinyflow';
import { Tinyflow } from '#/components/Tinyflow';
defineProps<{
provider: any;

View File

@@ -51,6 +51,9 @@ const props = defineProps({
const emit = defineEmits(['cancel']);
// 增加表单就绪状态变量 表单就绪后再渲染form-create
const isFormReady = ref(false)
const { closeCurrentTab } = useTabs();
const getTitle = computed(() => {
@@ -137,6 +140,9 @@ async function initProcessInfo(row: any, formVariables?: any) {
}
setConfAndFields2(detailForm, row.formConf, row.formFields, formVariables);
// 设置表单就绪状态
isFormReady.value = true
await nextTick();
fApi.value?.btn.show(false); // 隐藏提交按钮
@@ -293,6 +299,7 @@ defineExpose({ initProcessInfo });
class="flex-1 overflow-auto"
>
<form-create
v-if="isFormReady"
:rule="detailForm.rule"
v-model:api="fApi"
v-model="detailForm.value"

View File

@@ -0,0 +1,125 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { ErpProductCategoryApi } from '#/api/erp/product/category';
import { handleTree } from '@vben/utils';
import { z } from '#/adapter/form';
import { getProductCategoryList } from '#/api/erp/product/category';
import { CommonStatusEnum, DICT_TYPE, getDictOptions } from '#/utils';
/** 新增/修改的表单 */
export function useFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'id',
component: 'Input',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
fieldName: 'parentId',
label: '上级分类',
component: 'ApiTreeSelect',
componentProps: {
allowClear: true,
api: async () => {
const data = await getProductCategoryList();
data.unshift({
id: 0,
name: '顶级分类',
});
return handleTree(data);
},
labelField: 'name',
valueField: 'id',
childrenField: 'children',
placeholder: '请选择上级分类',
treeDefaultExpandAll: true,
},
rules: 'selectRequired',
},
{
fieldName: 'name',
label: '分类名称',
component: 'Input',
componentProps: {
placeholder: '请输入分类名称',
},
rules: 'required',
},
{
fieldName: 'code',
label: '分类编码',
component: 'Input',
componentProps: {
placeholder: '请输入分类编码',
},
rules: z.string().regex(/^[A-Z]+$/, '分类编码必须为大写字母'),
},
{
fieldName: 'sort',
label: '显示顺序',
component: 'InputNumber',
componentProps: {
min: 0,
controlsPosition: 'right',
placeholder: '请输入显示顺序',
},
rules: 'required',
},
{
fieldName: 'status',
label: '状态',
component: 'RadioGroup',
componentProps: {
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
buttonStyle: 'solid',
optionType: 'button',
},
rules: z.number().default(CommonStatusEnum.ENABLE),
},
];
}
/** 列表的字段 */
export function useGridColumns(): VxeTableGridOptions<ErpProductCategoryApi.ProductCategory>['columns'] {
return [
{
field: 'name',
title: '分类名称',
align: 'left',
treeNode: true,
},
{
field: 'code',
title: '分类编码',
},
{
field: 'sort',
title: '显示顺序',
},
{
field: 'status',
title: '分类状态',
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.COMMON_STATUS },
},
},
{
field: 'createTime',
title: '创建时间',
formatter: 'formatDateTime',
},
{
title: '操作',
width: 220,
fixed: 'right',
slots: { default: 'actions' },
},
];
}

View File

@@ -1,34 +1,166 @@
<script lang="ts" setup>
import { DocAlert, Page } from '@vben/common-ui';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { ErpProductCategoryApi } from '#/api/erp/product/category';
import { Button } from 'ant-design-vue';
import { ref } from 'vue';
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
deleteProductCategory,
getProductCategoryList,
} from '#/api/erp/product/category';
import { $t } from '#/locales';
import { useGridColumns } from './data';
import Form from './modules/form.vue';
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
destroyOnClose: true,
});
/** 刷新表格 */
function onRefresh() {
gridApi.query();
}
/** 切换树形展开/收缩状态 */
const isExpanded = ref(true);
function toggleExpand() {
isExpanded.value = !isExpanded.value;
gridApi.grid.setAllTreeExpand(isExpanded.value);
}
/** 创建分类 */
function handleCreate() {
formModalApi.setData(null).open();
}
/** 添加下级分类 */
function handleAppend(row: ErpProductCategoryApi.ProductCategory) {
formModalApi.setData({ parentId: row.id }).open();
}
/** 编辑分类 */
function handleEdit(row: ErpProductCategoryApi.ProductCategory) {
formModalApi.setData(row).open();
}
/** 删除分类 */
async function handleDelete(row: ErpProductCategoryApi.ProductCategory) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]),
key: 'action_key_msg',
});
try {
await deleteProductCategory(row.id as number);
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
key: 'action_key_msg',
});
onRefresh();
} finally {
hideLoading();
}
}
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
columns: useGridColumns(),
height: 'auto',
proxyConfig: {
ajax: {
query: async () => {
return await getProductCategoryList();
},
},
},
pagerConfig: {
enabled: false,
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: true,
search: true,
},
treeConfig: {
transform: true,
rowField: 'id',
parentField: 'parentId',
expandAll: true,
accordion: false,
},
} as VxeTableGridOptions<ErpProductCategoryApi.ProductCategory>,
});
</script>
<template>
<Page>
<Page auto-content-height>
<template #doc>
<DocAlert
title="【产品】产品信息、分类、单位"
url="https://doc.iocoder.cn/erp/product/"
/>
</template>
<Button
danger
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
>
该功能支持 Vue3 + element-plus 版本
</Button>
<br />
<Button
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/erp/product/category/index"
>
可参考
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/erp/product/category/index
代码pull request 贡献给我们
</Button>
<FormModal @success="onRefresh" />
<Grid table-title="产品分类列表">
<template #toolbar-tools>
<TableAction
:actions="[
{
label: $t('ui.actionTitle.create', ['产品分类']),
type: 'primary',
icon: ACTION_ICON.ADD,
auth: ['erp:product-category:create'],
onClick: handleCreate,
},
{
label: isExpanded ? '收缩' : '展开',
type: 'primary',
onClick: toggleExpand,
},
]"
/>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: '新增下级',
type: 'link',
icon: ACTION_ICON.ADD,
auth: ['erp:product-category:create'],
onClick: handleAppend.bind(null, row),
},
{
label: $t('common.edit'),
type: 'link',
icon: ACTION_ICON.EDIT,
auth: ['erp:product-category:update'],
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'link',
danger: true,
icon: ACTION_ICON.DELETE,
auth: ['erp:product-category:delete'],
ifShow: !(row.children && row.children.length > 0),
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template>
</Grid>
</Page>
</template>

View File

@@ -0,0 +1,92 @@
<script lang="ts" setup>
import type { ErpProductCategoryApi } from '#/api/erp/product/category';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
createProductCategory,
getProductCategory,
updateProductCategory,
} from '#/api/erp/product/category';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<ErpProductCategoryApi.ProductCategory>();
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', ['分类'])
: $t('ui.actionTitle.create', ['分类']);
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-2',
labelWidth: 80,
},
layout: 'horizontal',
schema: useFormSchema(),
showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
// 提交表单
const data =
(await formApi.getValues()) as ErpProductCategoryApi.ProductCategory;
try {
await (formData.value?.id
? updateProductCategory(data)
: createProductCategory(data));
// 关闭并提示
await modalApi.close();
emit('success');
message.success($t('ui.actionMessage.operationSuccess'));
} finally {
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
return;
}
// 加载数据
let data = modalApi.getData<ErpProductCategoryApi.ProductCategory>();
if (!data) {
return;
}
if (data.id) {
modalApi.lock();
try {
data = await getProductCategory(data.id);
} finally {
modalApi.unlock();
}
}
// 设置到 values
formData.value = data;
await formApi.setValues(formData.value);
},
});
</script>
<template>
<Modal :title="getTitle">
<Form class="mx-4" />
</Modal>
</template>

View File

@@ -0,0 +1,220 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { handleTree } from '@vben/utils';
import { z } from '#/adapter/form';
import { getProductCategorySimpleList } from '#/api/erp/product/category';
import { getProductUnitSimpleList } from '#/api/erp/product/unit';
import { CommonStatusEnum, DICT_TYPE, getDictOptions } from '#/utils';
/** 新增/修改的表单 */
export function useFormSchema(): VbenFormSchema[] {
return [
{
component: 'Input',
fieldName: 'id',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
component: 'Input',
fieldName: 'name',
label: '名称',
rules: 'required',
componentProps: {
placeholder: '请输入名称',
},
},
{
fieldName: 'barCode',
label: '条码',
component: 'Input',
rules: 'required',
componentProps: {
placeholder: '请输入条码',
},
},
{
fieldName: 'categoryId',
label: '分类',
component: 'ApiTreeSelect',
componentProps: {
api: async () => {
const data = await getProductCategorySimpleList();
return handleTree(data);
},
labelField: 'name',
valueField: 'id',
childrenField: 'children',
placeholder: '请选择分类',
treeDefaultExpandAll: true,
},
rules: 'required',
},
{
fieldName: 'unitId',
label: '单位',
component: 'ApiSelect',
componentProps: {
api: getProductUnitSimpleList,
labelField: 'name',
valueField: 'id',
placeholder: '请选择单位',
},
rules: 'required',
},
{
fieldName: 'status',
label: '状态',
component: 'RadioGroup',
componentProps: {
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
buttonStyle: 'solid',
optionType: 'button',
},
rules: z.number().default(CommonStatusEnum.ENABLE),
},
{
fieldName: 'standard',
label: '规格',
component: 'Input',
componentProps: {
placeholder: '请输入规格',
},
},
{
fieldName: 'expiryDay',
label: '保质期天数',
component: 'InputNumber',
componentProps: {
placeholder: '请输入保质期天数',
},
},
{
fieldName: 'weight',
label: '重量kg',
component: 'InputNumber',
componentProps: {
placeholder: '请输入重量kg',
},
},
{
fieldName: 'purchasePrice',
label: '采购价格',
component: 'InputNumber',
componentProps: {
placeholder: '请输入采购价格,单位:元',
},
},
{
fieldName: 'salePrice',
label: '销售价格',
component: 'InputNumber',
componentProps: {
placeholder: '请输入销售价格,单位:元',
},
},
{
fieldName: 'minPrice',
label: '最低价格',
component: 'InputNumber',
componentProps: {
placeholder: '请输入最低价格,单位:元',
},
},
{
fieldName: 'remark',
label: '备注',
component: 'Textarea',
},
];
}
/** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'name',
label: '名称',
component: 'Input',
},
{
fieldName: 'categoryId',
label: '分类',
component: 'ApiTreeSelect',
componentProps: {
api: async () => {
const data = await getProductCategorySimpleList();
return handleTree(data);
},
labelField: 'name',
valueField: 'id',
childrenField: 'children',
placeholder: '请选择分类',
treeDefaultExpandAll: true,
},
},
];
}
/** 列表的字段 */
export function useGridColumns(): VxeTableGridOptions['columns'] {
return [
{
field: 'barCode',
title: '条码',
},
{
field: 'name',
title: '名称',
},
{
field: 'standard',
title: '规格',
},
{
field: 'categoryName',
title: '分类',
},
{
field: 'unitName',
title: '单位',
},
{
field: 'purchasePrice',
title: '采购价格',
},
{
field: 'salePrice',
title: '销售价格',
},
{
field: 'minPrice',
title: '最低价格',
},
{
field: 'status',
title: '状态',
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.COMMON_STATUS },
},
},
{
field: 'createTime',
title: '创建时间',
formatter: 'formatDateTime',
},
{
title: '操作',
width: 130,
fixed: 'right',
slots: { default: 'actions' },
},
];
}

View File

@@ -1,34 +1,152 @@
<script lang="ts" setup>
import { DocAlert, Page } from '@vben/common-ui';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { ErpProductApi } from '#/api/erp/product/product';
import { Button } from 'ant-design-vue';
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
import { downloadFileFromBlobPart } from '@vben/utils';
import { message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
deleteProduct,
exportProduct,
getProductPage,
} from '#/api/erp/product/product';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
destroyOnClose: true,
});
/** 刷新表格 */
function onRefresh() {
gridApi.query();
}
/** 导出表格 */
async function handleExport() {
const data = await exportProduct(await gridApi.formApi.getValues());
downloadFileFromBlobPart({ fileName: '产品信息.xls', source: data });
}
/** 创建产品 */
function handleCreate() {
formModalApi.setData(null).open();
}
/** 编辑产品 */
function handleEdit(row: ErpProductApi.Product) {
formModalApi.setData(row).open();
}
/** 删除产品 */
async function handleDelete(row: ErpProductApi.Product) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]),
key: 'action_key_msg',
});
try {
await deleteProduct(row.id as number);
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
key: 'action_key_msg',
});
onRefresh();
} finally {
hideLoading();
}
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
},
gridOptions: {
columns: useGridColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getProductPage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: true,
search: true,
},
} as VxeTableGridOptions<ErpProductApi.Product>,
});
</script>
<template>
<Page>
<Page auto-content-height>
<template #doc>
<DocAlert
title="【产品】产品信息、分类、单位"
url="https://doc.iocoder.cn/erp/product/"
/>
</template>
<Button
danger
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
>
该功能支持 Vue3 + element-plus 版本
</Button>
<br />
<Button
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/erp/product/product/index"
>
可参考
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/erp/product/product/index
代码pull request 贡献给我们
</Button>
<FormModal @success="onRefresh" />
<Grid table-title="产品列表">
<template #toolbar-tools>
<TableAction
:actions="[
{
label: $t('ui.actionTitle.create', ['产品']),
type: 'primary',
icon: ACTION_ICON.ADD,
auth: ['erp:product:create'],
onClick: handleCreate,
},
{
label: $t('ui.actionTitle.export'),
type: 'primary',
icon: ACTION_ICON.DOWNLOAD,
auth: ['erp:product:export'],
onClick: handleExport,
},
]"
/>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'link',
icon: ACTION_ICON.EDIT,
auth: ['erp:product:update'],
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'link',
danger: true,
icon: ACTION_ICON.DELETE,
auth: ['erp:product:delete'],
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template>
</Grid>
</Page>
</template>

View File

@@ -0,0 +1,86 @@
<script lang="ts" setup>
import type { ErpProductApi } from '#/api/erp/product/product';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
createProduct,
getProduct,
updateProduct,
} from '#/api/erp/product/product';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<ErpProductApi.Product>();
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', ['产品'])
: $t('ui.actionTitle.create', ['产品']);
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
},
// 一共2列
wrapperClass: 'grid-cols-2',
layout: 'horizontal',
schema: useFormSchema(),
showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
// 提交表单
const data = (await formApi.getValues()) as ErpProductApi.Product;
try {
await (formData.value?.id ? updateProduct(data) : createProduct(data));
// 关闭并提示
await modalApi.close();
emit('success');
message.success($t('ui.actionMessage.operationSuccess'));
} finally {
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
return;
}
// 加载数据
const data = modalApi.getData<ErpProductApi.Product>();
if (!data || !data.id) {
return;
}
modalApi.lock();
try {
formData.value = await getProduct(data.id as number);
// 设置到 values
await formApi.setValues(formData.value);
} finally {
modalApi.unlock();
}
},
});
</script>
<template>
<Modal class="w-2/5" :title="getTitle">
<Form class="mx-4" />
</Modal>
</template>

View File

@@ -0,0 +1,89 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { z } from '#/adapter/form';
import { CommonStatusEnum, DICT_TYPE, getDictOptions } from '#/utils';
/** 新增/修改的表单 */
export function useFormSchema(): VbenFormSchema[] {
return [
{
component: 'Input',
fieldName: 'id',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
component: 'Input',
fieldName: 'name',
label: '单位名称',
rules: 'required',
},
{
fieldName: 'status',
label: '单位状态',
component: 'RadioGroup',
componentProps: {
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
buttonStyle: 'solid',
optionType: 'button',
},
rules: z.number().default(CommonStatusEnum.ENABLE),
},
];
}
/** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'name',
label: '单位名称',
component: 'Input',
},
{
fieldName: 'status',
label: '单位状态',
component: 'Select',
componentProps: {
allowClear: true,
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
},
},
];
}
/** 列表的字段 */
export function useGridColumns(): VxeTableGridOptions['columns'] {
return [
{
field: 'id',
title: '单位编号',
},
{
field: 'name',
title: '单位名称',
},
{
field: 'status',
title: '单位状态',
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.COMMON_STATUS },
},
},
{
field: 'createTime',
title: '创建时间',
formatter: 'formatDateTime',
},
{
title: '操作',
width: 130,
fixed: 'right',
slots: { default: 'actions' },
},
];
}

View File

@@ -1,34 +1,152 @@
<script lang="ts" setup>
import { DocAlert, Page } from '@vben/common-ui';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { ErpProductUnitApi } from '#/api/erp/product/unit';
import { Button } from 'ant-design-vue';
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
import { downloadFileFromBlobPart } from '@vben/utils';
import { message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
deleteProductUnit,
exportProductUnit,
getProductUnitPage,
} from '#/api/erp/product/unit';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
destroyOnClose: true,
});
/** 刷新表格 */
function onRefresh() {
gridApi.query();
}
/** 导出表格 */
async function handleExport() {
const data = await exportProductUnit(await gridApi.formApi.getValues());
downloadFileFromBlobPart({ fileName: '产品单位.xls', source: data });
}
/** 创建产品单位 */
function handleCreate() {
formModalApi.setData(null).open();
}
/** 编辑产品单位 */
function handleEdit(row: ErpProductUnitApi.ProductUnit) {
formModalApi.setData(row).open();
}
/** 删除产品单位 */
async function handleDelete(row: ErpProductUnitApi.ProductUnit) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]),
key: 'action_key_msg',
});
try {
await deleteProductUnit(row.id as number);
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
key: 'action_key_msg',
});
onRefresh();
} finally {
hideLoading();
}
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
},
gridOptions: {
columns: useGridColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getProductUnitPage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: true,
search: true,
},
} as VxeTableGridOptions<ErpProductUnitApi.ProductUnit>,
});
</script>
<template>
<Page>
<Page auto-content-height>
<template #doc>
<DocAlert
title="【产品】产品信息、分类、单位"
url="https://doc.iocoder.cn/erp/product/"
/>
</template>
<Button
danger
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
>
该功能支持 Vue3 + element-plus 版本
</Button>
<br />
<Button
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/erp/product/unit/index"
>
可参考
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/erp/product/unit/index
代码pull request 贡献给我们
</Button>
<FormModal @success="onRefresh" />
<Grid table-title="产品单位列表">
<template #toolbar-tools>
<TableAction
:actions="[
{
label: $t('ui.actionTitle.create', ['产品单位']),
type: 'primary',
icon: ACTION_ICON.ADD,
auth: ['erp:product-unit:create'],
onClick: handleCreate,
},
{
label: $t('ui.actionTitle.export'),
type: 'primary',
icon: ACTION_ICON.DOWNLOAD,
auth: ['erp:product-unit:export'],
onClick: handleExport,
},
]"
/>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'link',
icon: ACTION_ICON.EDIT,
auth: ['erp:product-unit:update'],
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'link',
danger: true,
icon: ACTION_ICON.DELETE,
auth: ['erp:product-unit:delete'],
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template>
</Grid>
</Page>
</template>

View File

@@ -0,0 +1,88 @@
<script lang="ts" setup>
import type { ErpProductUnitApi } from '#/api/erp/product/unit';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
createProductUnit,
getProductUnit,
updateProductUnit,
} from '#/api/erp/product/unit';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<ErpProductUnitApi.ProductUnit>();
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', ['产品单位'])
: $t('ui.actionTitle.create', ['产品单位']);
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-2',
labelWidth: 80,
},
layout: 'horizontal',
schema: useFormSchema(),
showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
// 提交表单
const data = (await formApi.getValues()) as ErpProductUnitApi.ProductUnit;
try {
await (formData.value?.id
? updateProductUnit(data)
: createProductUnit(data));
// 关闭并提示
await modalApi.close();
emit('success');
message.success($t('ui.actionMessage.operationSuccess'));
} finally {
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
return;
}
// 加载数据
const data = modalApi.getData<ErpProductUnitApi.ProductUnit>();
if (!data || !data.id) {
return;
}
modalApi.lock();
try {
formData.value = await getProductUnit(data.id as number);
// 设置到 values
await formApi.setValues(formData.value);
} finally {
modalApi.unlock();
}
},
});
</script>
<template>
<Modal class="w-2/5" :title="getTitle">
<Form class="mx-4" />
</Modal>
</template>

View File

@@ -91,6 +91,7 @@ async function handleBatchDelete() {
});
try {
await deletePurchaseOrderList(checkedIds.value);
checkedIds.value = [];
message.success({
content: $t('ui.actionMessage.deleteSuccess'),
key: 'action_process_msg',

View File

@@ -114,7 +114,11 @@ const [Modal, modalApi] = useVbenModal({
// 提交表单
const data =
(await formApi.getValues()) as ErpPurchaseOrderApi.PurchaseOrder;
data.items = formData.value?.items;
data.items = formData.value?.items?.map((item) => ({
...item,
// 解决新增采购订单报错
id: undefined,
}));
// 将文件数组转换为字符串
if (data.fileUrl && Array.isArray(data.fileUrl)) {
data.fileUrl = data.fileUrl.length > 0 ? data.fileUrl[0] : '';

View File

@@ -88,7 +88,10 @@ const [Modal, modalApi] = useVbenModal({
modalApi.lock();
// 提交表单
const data = (await formApi.getValues()) as ErpStockInApi.StockIn;
data.items = formData.value?.items;
data.items = formData.value?.items?.map((item) => ({
...item,
id: undefined,
}));
// 将文件数组转换为字符串
if (data.fileUrl && Array.isArray(data.fileUrl)) {
data.fileUrl = data.fileUrl.length > 0 ? data.fileUrl[0] : '';

View File

@@ -0,0 +1,329 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { createRequiredValidation } from '#/adapter/vxe-table';
import { getProductSimpleList } from '#/api/erp/product/product';
import { getCustomerSimpleList } from '#/api/erp/sale/customer';
import { getSimpleUserList } from '#/api/system/user';
import { DICT_TYPE, getDictOptions } from '#/utils';
/** 表单的配置项 */
export function useFormSchema(formType: string): VbenFormSchema[] {
return [
{
component: 'Input',
componentProps: {
style: { display: 'none' },
},
fieldName: 'id',
label: 'ID',
hideLabel: true,
formItemClass: 'hidden',
},
{
component: 'Input',
componentProps: {
placeholder: '系统自动生成',
disabled: true,
},
fieldName: 'no',
label: '出库单号',
disabled: formType === 'detail',
},
{
component: 'DatePicker',
componentProps: {
placeholder: '选择出库时间',
showTime: true,
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'x',
style: { width: '100%' },
},
disabled: formType === 'detail',
fieldName: 'outTime',
label: '出库时间',
rules: 'required',
},
{
component: 'ApiSelect',
componentProps: {
placeholder: '请选择客户',
allowClear: true,
showSearch: true,
api: getCustomerSimpleList,
fieldNames: {
label: 'name',
value: 'id',
},
},
disabled: formType === 'detail',
fieldName: 'customerId',
label: '客户',
},
{
component: 'Textarea',
componentProps: {
placeholder: '请输入备注',
autoSize: { minRows: 2, maxRows: 4 },
class: 'w-full',
},
disabled: formType === 'detail',
fieldName: 'remark',
label: '备注',
formItemClass: 'col-span-3',
},
{
component: 'FileUpload',
disabled: formType === 'detail',
componentProps: {
maxNumber: 1,
maxSize: 10,
accept: [
'pdf',
'doc',
'docx',
'xls',
'xlsx',
'txt',
'jpg',
'jpeg',
'png',
],
showDescription: true,
},
fieldName: 'fileUrl',
label: '附件',
formItemClass: 'col-span-3',
},
{
fieldName: 'product',
disabled: formType === 'detail',
label: '产品清单',
component: 'Input',
formItemClass: 'col-span-3',
},
];
}
/** 出库产品清单表格列定义 */
export function useStockInItemTableColumns(
isValidating?: any,
): VxeTableGridOptions['columns'] {
return [
{ type: 'seq', title: '序号', minWidth: 50, fixed: 'left' },
{
field: 'warehouseId',
title: '仓库名称',
minWidth: 150,
slots: { default: 'warehouseId' },
className: createRequiredValidation(isValidating, 'warehouseId'),
},
{
field: 'productId',
title: '产品名称',
minWidth: 200,
slots: { default: 'productId' },
className: createRequiredValidation(isValidating, 'productId'),
},
{
field: 'stockCount',
title: '库存',
minWidth: 100,
},
{
field: 'productBarCode',
title: '条码',
minWidth: 120,
},
{
field: 'productUnitName',
title: '单位',
minWidth: 80,
},
{
field: 'count',
title: '数量',
minWidth: 120,
slots: { default: 'count' },
className: createRequiredValidation(isValidating, 'count'),
},
{
field: 'productPrice',
title: '产品单价',
minWidth: 120,
slots: { default: 'productPrice' },
},
{
field: 'totalPrice',
title: '金额',
minWidth: 120,
formatter: 'formatAmount2',
},
{
field: 'remark',
title: '备注',
minWidth: 150,
slots: { default: 'remark' },
},
{
title: '操作',
width: 50,
fixed: 'right',
slots: { default: 'actions' },
},
];
}
/** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'no',
label: '出库单号',
component: 'Input',
componentProps: {
placeholder: '请输入出库单号',
allowClear: true,
},
},
{
fieldName: 'productId',
label: '产品',
component: 'ApiSelect',
componentProps: {
placeholder: '请选择产品',
allowClear: true,
showSearch: true,
api: getProductSimpleList,
labelField: 'name',
valueField: 'id',
filterOption: false,
},
},
{
fieldName: 'outTime',
label: '出库时间',
component: 'RangePicker',
componentProps: {
placeholder: ['开始日期', '结束日期'],
showTime: true,
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
},
},
{
fieldName: 'customerId',
label: '客户',
component: 'ApiSelect',
componentProps: {
placeholder: '请选择客户',
allowClear: true,
showSearch: true,
api: getCustomerSimpleList,
labelField: 'name',
valueField: 'id',
filterOption: false,
},
},
{
fieldName: 'status',
label: '状态',
component: 'Select',
componentProps: {
placeholder: '请选择状态',
allowClear: true,
options: getDictOptions(DICT_TYPE.ERP_AUDIT_STATUS, 'number'),
},
},
{
fieldName: 'remark',
label: '备注',
component: 'Input',
componentProps: {
placeholder: '请输入备注',
allowClear: true,
},
},
{
fieldName: 'creator',
label: '创建人',
component: 'ApiSelect',
componentProps: {
placeholder: '请选择创建人',
allowClear: true,
showSearch: true,
api: getSimpleUserList,
labelField: 'nickname',
valueField: 'id',
filterOption: false,
},
},
];
}
/** 列表的字段 */
export function useGridColumns(): VxeTableGridOptions['columns'] {
return [
{
type: 'checkbox',
width: 50,
fixed: 'left',
},
{
field: 'no',
title: '出库单号',
minWidth: 180,
},
{
field: 'productNames',
title: '产品信息',
minWidth: 200,
showOverflow: 'tooltip',
},
{
field: 'customerName',
title: '客户',
minWidth: 120,
},
{
field: 'outTime',
title: '出库时间',
minWidth: 180,
cellRender: {
name: 'CellDateTime',
},
},
{
field: 'creatorName',
title: '创建人',
minWidth: 100,
},
{
field: 'totalCount',
title: '数量',
minWidth: 100,
},
{
field: 'totalPrice',
title: '价格',
minWidth: 100,
},
{
field: 'status',
title: '状态',
minWidth: 90,
fixed: 'right',
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.ERP_AUDIT_STATUS },
},
},
{
title: '操作',
width: 300,
fixed: 'right',
slots: { default: 'actions' },
},
];
}

View File

@@ -1,34 +1,219 @@
<script lang="ts" setup>
import { DocAlert, Page } from '@vben/common-ui';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { ErpStockOutApi } from '#/api/erp/stock/out';
import { Button } from 'ant-design-vue';
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
import { downloadFileFromBlobPart } from '@vben/utils';
import { message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
deleteStockOut,
exportStockOut,
getStockOutPage,
updateStockOutStatus,
} from '#/api/erp/stock/out';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import StockInForm from './modules/form.vue';
/** 其它出库单管理 */
defineOptions({ name: 'ErpStockOut' });
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: StockInForm,
destroyOnClose: true,
});
/** 刷新表格 */
function onRefresh() {
gridApi.query();
}
/** 导出出库单 */
async function handleExport() {
const data = await exportStockOut(await gridApi.formApi.getValues());
downloadFileFromBlobPart({ fileName: '其它出库单.xls', source: data });
}
/** 新增/编辑/详情 */
function openForm(type: string, id?: number) {
formModalApi.setData({ type, id }).open();
}
/** 删除 */
async function handleDelete(ids: any[]) {
const hideLoading = message.loading({
content: '删除中...',
duration: 0,
key: 'action_process_msg',
});
try {
await deleteStockOut(ids);
message.success({
content: '删除成功',
key: 'action_process_msg',
});
onRefresh();
} finally {
hideLoading();
}
}
/** 审核/反审核 */
async function handleUpdateStatus(id: any, status: number) {
const statusText = status === 20 ? '审核' : '反审核';
const hideLoading = message.loading({
content: `${statusText}中...`,
duration: 0,
key: 'action_process_msg',
});
try {
await updateStockOutStatus({ id, status });
message.success({
content: `${statusText}成功`,
key: 'action_process_msg',
});
onRefresh();
} finally {
hideLoading();
}
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
},
gridOptions: {
columns: useGridColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getStockOutPage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
},
toolbarConfig: {
refresh: true,
search: true,
},
checkboxConfig: {
reserve: true,
},
} as VxeTableGridOptions<ErpStockOutApi.StockOut>,
});
</script>
<template>
<Page>
<Page auto-content-height>
<template #doc>
<DocAlert
title="【库存】其它入库、其它出库"
url="https://doc.iocoder.cn/erp/stock-in-out/"
/>
</template>
<Button
danger
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
>
该功能支持 Vue3 + element-plus 版本
</Button>
<br />
<Button
type="link"
target="_blank"
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/erp/stock/out/index"
>
可参考
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/erp/stock/out/index
代码pull request 贡献给我们
</Button>
<Grid table-title="其它出库单列表">
<template #toolbar-tools>
<TableAction
:actions="[
{
label: $t('ui.actionTitle.create', ['其它出库']),
type: 'primary',
icon: ACTION_ICON.ADD,
auth: ['erp:stock-out:create'],
onClick: () => openForm('create'),
},
{
label: $t('ui.actionTitle.export'),
type: 'default',
icon: ACTION_ICON.DOWNLOAD,
auth: ['erp:stock-out:export'],
onClick: handleExport,
},
{
label: '批量删除',
type: 'default',
danger: true,
icon: ACTION_ICON.DELETE,
auth: ['erp:stock-out:delete'],
popConfirm: {
title: '是否删除所选中数据?',
confirm: () => {
const checkboxRecords = gridApi.grid.getCheckboxRecords();
if (checkboxRecords.length === 0) {
message.warning('请选择要删除的数据');
return;
}
handleDelete(checkboxRecords.map((item) => item.id));
},
},
},
]"
/>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: '详情',
icon: ACTION_ICON.VIEW,
auth: ['erp:stock-out:query'],
onClick: () => openForm('detail', row.id),
},
{
label: '编辑',
auth: ['erp:stock-out:update'],
icon: ACTION_ICON.EDIT,
disabled: row.status !== 10,
onClick: () => openForm('update', row.id),
},
{
label: '审核',
auth: ['erp:stock-out:update'],
ifShow: row.status === 10,
popConfirm: {
title: '确认要审核该入库单吗?',
confirm: () => handleUpdateStatus(row.id, 20),
},
},
{
label: '反审核',
danger: true,
auth: ['erp:stock-out:update'],
ifShow: row.status === 20,
popConfirm: {
title: '确认要反审核该入库单吗?',
confirm: () => handleUpdateStatus(row.id, 10),
},
},
{
label: '删除',
danger: true,
auth: ['erp:stock-out:delete'],
disabled: row.status !== 10,
popConfirm: {
title: '确认要删除该出库单吗?',
confirm: () => handleDelete([row.id]),
},
},
]"
/>
</template>
</Grid>
<!-- 表单弹窗 -->
<FormModal @success="onRefresh" />
</Page>
</template>

View File

@@ -0,0 +1,196 @@
<script lang="ts" setup>
import type { ErpStockOutApi } from '#/api/erp/stock/out';
import { computed, nextTick, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
createStockOut,
getStockOut,
updateStockOut,
updateStockOutStatus,
} from '#/api/erp/stock/out';
import { useFormSchema } from '../data';
import StockInItemForm from './stock-out-item-form.vue';
const emit = defineEmits(['success']);
const formData = ref<ErpStockOutApi.StockOut>();
const formType = ref('');
const itemFormRef = ref();
const getTitle = computed(() => {
if (formType.value === 'create') return '添加其它出库单';
if (formType.value === 'update') return '编辑其它出库单';
return '其它出库单详情';
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
labelWidth: 120,
},
wrapperClass: 'grid-cols-3',
layout: 'vertical',
schema: useFormSchema(formType.value),
showDefaultActions: false,
});
const handleUpdateItems = (items: ErpStockOutApi.StockOutItem[]) => {
formData.value = modalApi.getData<ErpStockOutApi.StockOut>();
if (formData.value) {
formData.value.items = items;
}
};
/**
* 创建或更新其它出库单
*/
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) {
return;
}
await nextTick();
const itemFormInstance = Array.isArray(itemFormRef.value)
? itemFormRef.value[0]
: itemFormRef.value;
if (itemFormInstance && typeof itemFormInstance.validate === 'function') {
try {
const isValid = await itemFormInstance.validate();
if (!isValid) {
message.error('产品清单验证失败,请检查必填项');
return;
}
} catch (error: any) {
message.error(error.message || '产品清单验证失败');
return;
}
} else {
message.error('产品清单验证方法不存在');
return;
}
// 验证产品清单不能为空
if (!formData.value?.items || formData.value.items.length === 0) {
message.error('产品清单不能为空,请至少添加一个产品');
return;
}
modalApi.lock();
// 提交表单
const data = (await formApi.getValues()) as ErpStockOutApi.StockOut;
data.items = formData.value?.items?.map((item) => ({
...item,
id: undefined,
}));
// 将文件数组转换为字符串
if (data.fileUrl && Array.isArray(data.fileUrl)) {
data.fileUrl = data.fileUrl.length > 0 ? data.fileUrl[0] : '';
}
try {
await (formType.value === 'create'
? createStockOut(data)
: updateStockOut(data));
// 关闭并提示
await modalApi.close();
emit('success');
message.success(formType.value === 'create' ? '新增成功' : '更新成功');
} finally {
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
return;
}
// 加载数据
const data = modalApi.getData<{ id?: number; type: string }>();
if (!data) {
return;
}
formType.value = data.type;
formApi.updateSchema(useFormSchema(formType.value));
if (!data.id) {
// 初始化空的表单数据
formData.value = { items: [] } as unknown as ErpStockOutApi.StockOut;
await nextTick();
const itemFormInstance = Array.isArray(itemFormRef.value)
? itemFormRef.value[0]
: itemFormRef.value;
if (itemFormInstance && typeof itemFormInstance.init === 'function') {
itemFormInstance.init([]);
}
// 如果是新增,自动添加一行
if (formType.value === 'create' && itemFormInstance) {
itemFormInstance.handleAdd();
}
return;
}
modalApi.lock();
try {
formData.value = await getStockOut(data.id);
// 设置到 values
await formApi.setValues(formData.value);
// 初始化子表单
await nextTick();
const itemFormInstance = Array.isArray(itemFormRef.value)
? itemFormRef.value[0]
: itemFormRef.value;
if (itemFormInstance && typeof itemFormInstance.init === 'function') {
itemFormInstance.init(formData.value.items || []);
}
} finally {
modalApi.unlock();
}
},
});
/** 审核/反审核 */
async function handleUpdateStatus(id: number, status: number) {
try {
await updateStockOutStatus({ id, status });
message.success(status === 20 ? '审核成功' : '反审核成功');
emit('success');
await modalApi.close();
} catch (error: any) {
message.error(error.message || '操作失败');
}
}
defineExpose({ modalApi, handleUpdateStatus });
</script>
<template>
<Modal
v-bind="$attrs"
:title="getTitle"
class="w-4/5"
:closable="true"
:mask-closable="true"
:show-confirm-button="formType !== 'detail'"
>
<Form class="mx-3">
<template #product="slotProps">
<StockInItemForm
v-bind="slotProps"
ref="itemFormRef"
class="w-full"
:items="formData?.items ?? []"
:disabled="formType === 'detail'"
@update:items="handleUpdateItems"
/>
</template>
</Form>
</Modal>
</template>

View File

@@ -0,0 +1,367 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { ErpStockOutApi } from '#/api/erp/stock/out';
import { nextTick, onMounted, ref, watch } from 'vue';
import { erpPriceMultiply } from '@vben/utils';
import { Input, InputNumber, Select } from 'ant-design-vue';
import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { getProductSimpleList } from '#/api/erp/product/product';
import { getStockCount } from '#/api/erp/stock/stock';
import { getWarehouseSimpleList } from '#/api/erp/stock/warehouse';
import { useStockInItemTableColumns } from '../data';
const props = withDefaults(defineProps<Props>(), {
items: () => [],
disabled: false,
});
const emit = defineEmits(['update:items']);
interface Props {
items?: ErpStockOutApi.StockOutItem[];
disabled?: boolean;
}
const tableData = ref<ErpStockOutApi.StockOutItem[]>([]);
const productOptions = ref<any[]>([]);
const warehouseOptions = ref<any[]>([]);
const isValidating = ref(false);
/** 表格配置 */
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
editConfig: {
trigger: 'click',
mode: 'cell',
},
columns: useStockInItemTableColumns(isValidating),
data: tableData.value,
border: true,
showOverflow: true,
autoResize: true,
minHeight: 250,
keepSource: true,
rowConfig: {
keyField: 'id',
},
pagerConfig: {
enabled: false,
},
toolbarConfig: {
enabled: false,
},
showFooter: true,
footerCellClassName: 'stock-in-footer-cell',
footerMethod: ({
columns,
}: {
columns: VxeTableGridOptions['columns'];
}) => {
const footers: any[][] = [];
const sums = getSummaries();
const footerData: any[] = [];
columns!.forEach((column, columnIndex: number) => {
if (columnIndex === 0) {
footerData.push('合计');
} else if (column.field === 'count') {
footerData.push(sums.count);
} else if (column.field === 'totalPrice') {
footerData.push(sums.totalPrice);
} else {
footerData.push('');
}
});
footers.push(footerData);
return footers;
},
},
});
/** 监听外部传入的列数据 */
watch(
() => props.items,
async (items) => {
if (!items) {
return;
}
await nextTick();
tableData.value = [...items];
await nextTick();
gridApi.grid.reloadData(tableData.value);
},
{
immediate: true,
},
);
/** 初始化 */
onMounted(async () => {
productOptions.value = await getProductSimpleList();
warehouseOptions.value = await getWarehouseSimpleList();
});
function handleAdd() {
const newRow = {
warehouseId: undefined,
productId: undefined,
productName: '',
productUnitId: undefined,
productUnitName: '',
productBarCode: '',
count: 1,
productPrice: 0,
totalPrice: 0,
stockCount: 0,
remark: '',
};
tableData.value.push(newRow);
gridApi.grid.insertAt(newRow, -1);
emit('update:items', [...tableData.value]);
// 触发表格重新渲染以更新cellClassName
nextTick(() => {
gridApi.grid.refreshColumn();
});
}
function handleDelete(row: ErpStockOutApi.StockOutItem) {
gridApi.grid.remove(row);
const index = tableData.value.findIndex((item) => item.id === row.id);
if (index !== -1) {
tableData.value.splice(index, 1);
}
emit('update:items', [...tableData.value]);
}
async function handleWarehouseChange(warehouseId: any, row: any) {
const warehouse = warehouseOptions.value.find((w) => w.id === warehouseId);
if (!warehouse) {
return;
}
row.warehouseId = warehouseId;
// 如果已选择产品,重新获取库存
if (row.productId) {
const stockCount = await getStockCount(row.productId, warehouseId);
row.stockCount = stockCount || 0;
}
handleUpdateValue(row);
}
async function handleProductChange(productId: any, row: any) {
const product = productOptions.value.find((p) => p.id === productId);
if (!product) {
return;
}
// 获取库存数量
const stockCount = row.warehouseId
? await getStockCount(productId, row.warehouseId)
: await getStockCount(productId);
row.productId = productId;
row.productUnitId = product.unitId;
row.productBarCode = product.barCode;
row.productUnitName = product.unitName;
row.productName = product.name;
row.stockCount = stockCount || 0;
row.productPrice = product.purchasePrice || 0;
row.count = row.count || 1;
handlePriceChange(row);
}
function handlePriceChange(row: any) {
if (row.productPrice && row.count) {
row.totalPrice = erpPriceMultiply(row.productPrice, row.count) ?? 0;
}
handleUpdateValue(row);
}
function handleUpdateValue(row: any) {
const index = tableData.value.findIndex((item) => item.id === row.id);
if (index === -1) {
tableData.value.push(row);
} else {
tableData.value[index] = row;
}
emit('update:items', [...tableData.value]);
// 触发表格重新渲染以更新cellClassName
nextTick(() => {
gridApi.grid.refreshColumn();
});
}
const getSummaries = (): {
count: number;
totalPrice: number;
} => {
return {
count: tableData.value.reduce((sum, item) => sum + (item.count || 0), 0),
totalPrice: tableData.value.reduce(
(sum, item) => sum + (item.totalPrice || 0),
0,
),
};
};
/** 验证表单 */
function validate(): Promise<boolean> {
return new Promise((resolve) => {
isValidating.value = true;
// 触发表格重新渲染以显示验证错误
nextTick(() => {
gridApi.grid.refreshColumn();
});
// 验证是否有产品清单
if (!tableData.value || tableData.value.length === 0) {
resolve(false);
return;
}
// 验证每一行的必填字段
for (const item of tableData.value) {
if (
!item.warehouseId ||
!item.productId ||
!item.count ||
item.count <= 0
) {
resolve(false);
return;
}
}
// 验证通过,清除验证状态
isValidating.value = false;
nextTick(() => {
gridApi.grid.refreshColumn();
});
resolve(true);
});
}
/** 初始化表格数据 */
function init(items: ErpStockOutApi.StockOutItem[]) {
tableData.value = items || [];
gridApi.grid.reloadData(tableData.value);
}
defineExpose({
validate,
init,
handleAdd,
});
</script>
<template>
<div class="w-full">
<div class="mb-4 flex justify-between">
<span class="text-lg font-medium"></span>
</div>
<Grid>
<template #warehouseId="{ row }">
<Select
v-model:value="row.warehouseId"
:options="warehouseOptions"
:field-names="{ label: 'name', value: 'id' }"
placeholder="请选择仓库"
:disabled="disabled"
show-search
@change="(value) => handleWarehouseChange(value, row)"
class="w-full"
/>
</template>
<template #productId="{ row }">
<Select
v-model:value="row.productId"
:options="productOptions"
:field-names="{ label: 'name', value: 'id' }"
placeholder="请选择产品"
:disabled="disabled"
show-search
@change="(value) => handleProductChange(value, row)"
class="w-full"
/>
</template>
<template #count="{ row }">
<InputNumber
v-model:value="row.count"
:disabled="disabled"
:min="0.001"
:precision="3"
@change="() => handlePriceChange(row)"
class="w-full"
/>
</template>
<template #productPrice="{ row }">
<InputNumber
v-model:value="row.productPrice"
:disabled="disabled"
:min="0.01"
:precision="2"
@change="() => handlePriceChange(row)"
class="w-full"
/>
</template>
<template #remark="{ row }">
<Input
v-model:value="row.remark"
:disabled="disabled"
placeholder="请输入备注"
/>
</template>
<template #actions="{ row }">
<TableAction
v-if="!disabled"
:actions="[
{
label: '删除',
type: 'link',
danger: true,
popConfirm: {
title: '确认删除该产品吗?',
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template>
<template #bottom>
<TableAction
v-if="!disabled"
class="mt-4 flex justify-center"
:actions="[
{
label: '添加产品',
type: 'default',
onClick: handleAdd,
},
]"
/>
</template>
</Grid>
</div>
</template>
<style scoped>
:deep(.vxe-table .vxe-footer--column.stock-in-footer-cell .vxe-cell) {
background-color: #f5f5f5 !important;
}
</style>

View File

@@ -192,10 +192,13 @@ export function useDetailSchema(): DescriptionItemSchema[] {
field: 'requestParams',
label: '请求参数',
content: (data) => {
return h(JsonViewer, {
value: data.requestParams,
previewMode: true,
});
if (data.requestParams) {
return h(JsonViewer, {
value: JSON.parse(data.requestParams),
previewMode: true,
});
}
return '';
},
},
{

View File

@@ -7,6 +7,8 @@ import { h } from 'vue';
import { JsonViewer } from '@vben/common-ui';
import { formatDateTime } from '@vben/utils';
import { Textarea } from 'ant-design-vue';
import { DictTag } from '#/components/dict-tag';
import {
DICT_TYPE,
@@ -177,10 +179,13 @@ export function useDetailSchema(): DescriptionItemSchema[] {
field: 'requestParams',
label: '请求参数',
content: (data) => {
return h(JsonViewer, {
value: data.requestParams,
previewMode: true,
});
if (data.requestParams) {
return h(JsonViewer, {
value: JSON.parse(data.requestParams),
previewMode: true,
});
}
return '';
},
},
{
@@ -198,9 +203,9 @@ export function useDetailSchema(): DescriptionItemSchema[] {
field: 'exceptionStackTrace',
label: '异常堆栈',
content: (data) => {
return h(JsonViewer, {
return h(Textarea, {
value: data.exceptionStackTrace,
previewMode: true,
rows: 20,
});
},
},

View File

@@ -102,6 +102,7 @@ async function handleDeleteBatch() {
});
try {
await deleteCodegenTableList(checkedIds.value);
checkedIds.value = [];
message.success($t('ui.actionMessage.deleteSuccess'));
onRefresh();
} finally {

View File

@@ -84,6 +84,7 @@ async function handleDeleteBatch() {
});
try {
await deleteConfigList(checkedIds.value);
checkedIds.value = [];
message.success($t('ui.actionMessage.deleteSuccess'));
onRefresh();
} finally {

View File

@@ -66,6 +66,7 @@ async function handleDeleteBatch() {
});
try {
await deleteDemo01ContactList(checkedIds.value);
checkedIds.value = [];
message.success($t('ui.actionMessage.deleteSuccess'));
onRefresh();
} finally {

View File

@@ -76,6 +76,7 @@ async function onDeleteBatch() {
});
try {
await deleteDemo03StudentList(checkedIds.value);
checkedIds.value = [];
message.success($t('ui.actionMessage.deleteSuccess'));
onRefresh();
} finally {

View File

@@ -75,6 +75,7 @@ async function onDeleteBatch() {
});
try {
await deleteDemo03CourseList(checkedIds.value);
checkedIds.value = [];
message.success($t('ui.actionMessage.deleteSuccess'));
onRefresh();
} finally {

View File

@@ -75,6 +75,7 @@ async function onDeleteBatch() {
});
try {
await deleteDemo03GradeList(checkedIds.value);
checkedIds.value = [];
message.success($t('ui.actionMessage.deleteSuccess'));
onRefresh();
} finally {

View File

@@ -75,6 +75,7 @@ async function onDeleteBatch() {
});
try {
await deleteDemo03StudentList(checkedIds.value);
checkedIds.value = [];
message.success($t('ui.actionMessage.deleteSuccess'));
onRefresh();
} finally {

View File

@@ -78,6 +78,7 @@ async function onDeleteBatch() {
});
try {
await deleteDemo03StudentList(checkedIds.value);
checkedIds.value = [];
message.success($t('ui.actionMessage.deleteSuccess'));
onRefresh();
} finally {

View File

@@ -123,6 +123,7 @@ async function handleDeleteBatch() {
});
try {
await deleteDemo01ContactList(checkedIds.value);
checkedIds.value = [];
message.success($t('ui.actionMessage.deleteSuccess'));
await getList();
} finally {

View File

@@ -134,6 +134,7 @@ async function onDeleteBatch() {
});
try {
await deleteDemo03StudentList(checkedIds.value);
checkedIds.value = [];
message.success($t('ui.actionMessage.deleteSuccess'));
await getList();
} finally {

View File

@@ -81,6 +81,7 @@ async function onDeleteBatch() {
});
try {
await deleteDemo03CourseList(checkedIds.value);
checkedIds.value = [];
message.success($t('ui.actionMessage.deleteSuccess'));
await getList();
} finally {

View File

@@ -81,6 +81,7 @@ async function onDeleteBatch() {
});
try {
await deleteDemo03GradeList(checkedIds.value);
checkedIds.value = [];
message.success($t('ui.actionMessage.deleteSuccess'));
await getList();
} finally {

View File

@@ -130,6 +130,7 @@ async function onDeleteBatch() {
});
try {
await deleteDemo03StudentList(checkedIds.value);
checkedIds.value = [];
message.success($t('ui.actionMessage.deleteSuccess'));
await getList();
} finally {

View File

@@ -124,6 +124,7 @@ async function onDeleteBatch() {
});
try {
await deleteDemo03StudentList(checkedIds.value);
checkedIds.value = [];
message.success($t('ui.actionMessage.deleteSuccess'));
await getList();
} finally {

View File

@@ -84,6 +84,7 @@ async function handleDeleteBatch() {
});
try {
await deleteFileList(checkedIds.value);
checkedIds.value = [];
message.success($t('ui.actionMessage.deleteSuccess'));
onRefresh();
} finally {

View File

@@ -200,6 +200,25 @@ export function useFormSchema(): VbenFormSchema[] {
},
defaultValue: false,
},
{
fieldName: 'config.enablePublicAccess',
label: '公开访问',
component: 'RadioGroup',
componentProps: {
options: [
{ label: '公开', value: true },
{ label: '私有', value: false },
],
buttonStyle: 'solid',
optionType: 'button',
},
rules: 'required',
dependencies: {
triggerFields: ['storage'],
show: (formValues) => formValues.storage === 20,
},
defaultValue: false,
},
// 通用
{
fieldName: 'config.domain',

View File

@@ -118,6 +118,7 @@ async function handleDeleteBatch() {
});
try {
await deleteFileConfigList(checkedIds.value);
checkedIds.value = [];
message.success($t('ui.actionMessage.deleteSuccess'));
onRefresh();
} finally {

View File

@@ -130,6 +130,7 @@ async function handleDeleteBatch() {
});
try {
await deleteJobList(checkedIds.value);
checkedIds.value = [];
message.success($t('ui.actionMessage.deleteSuccess'));
onRefresh();
} finally {

View File

@@ -93,6 +93,7 @@ async function handleDeleteBatch() {
});
try {
await deleteDeptList(checkedIds.value);
checkedIds.value = [];
message.success($t('ui.actionMessage.deleteSuccess'));
onRefresh();
} finally {

View File

@@ -90,6 +90,7 @@ async function handleDeleteBatch() {
});
try {
await deleteDictDataList(checkedIds.value);
checkedIds.value = [];
message.success($t('ui.actionMessage.deleteSuccess'));
onRefresh();
} finally {

View File

@@ -88,6 +88,7 @@ async function handleDeleteBatch() {
});
try {
await deleteDictTypeList(checkedIds.value);
checkedIds.value = [];
message.success($t('ui.actionMessage.deleteSuccess'));
onRefresh();
} finally {

View File

@@ -76,6 +76,7 @@ async function handleDeleteBatch() {
});
try {
await deleteMailAccountList(checkedIds.value);
checkedIds.value = [];
message.success($t('ui.actionMessage.deleteSuccess'));
onRefresh();
} finally {

View File

@@ -88,6 +88,7 @@ async function handleDeleteBatch() {
});
try {
await deleteMailTemplateList(checkedIds.value);
checkedIds.value = [];
message.success($t('ui.actionMessage.deleteSuccess'));
onRefresh();
} finally {

View File

@@ -77,6 +77,7 @@ async function handleDeleteBatch() {
});
try {
await deleteNoticeList(checkedIds.value);
checkedIds.value = [];
message.success($t('ui.actionMessage.deleteSuccess'));
onRefresh();
} finally {

View File

@@ -94,6 +94,7 @@ async function handleDeleteBatch() {
});
try {
await deleteNotifyTemplateList(checkedIds.value);
checkedIds.value = [];
message.success($t('ui.actionMessage.deleteSuccess'));
onRefresh();
} finally {

View File

@@ -83,6 +83,7 @@ async function handleDeleteBatch() {
});
try {
await deletePostList(checkedIds.value);
checkedIds.value = [];
message.success($t('ui.actionMessage.deleteSuccess'));
onRefresh();
} finally {

View File

@@ -96,6 +96,7 @@ async function handleDeleteBatch() {
});
try {
await deleteRoleList(checkedIds.value);
checkedIds.value = [];
message.success($t('ui.actionMessage.deleteSuccess'));
onRefresh();
} finally {

View File

@@ -87,6 +87,7 @@ async function handleDeleteBatch() {
});
try {
await deleteSmsChannelList(checkedIds.value);
checkedIds.value = [];
message.success({
content: $t('ui.actionMessage.deleteSuccess', ['短信渠道']),
key: 'action_key_msg',

View File

@@ -94,6 +94,7 @@ async function handleDeleteBatch() {
});
try {
await deleteSmsTemplateList(checkedIds.value);
checkedIds.value = [];
message.success($t('ui.actionMessage.deleteSuccess'));
onRefresh();
} finally {

View File

@@ -90,9 +90,13 @@ export function useFormSchema(): VbenFormSchema[] {
},
{
label: '绑定域名',
fieldName: 'website',
component: 'Input',
rules: 'required',
fieldName: 'websites',
component: 'Textarea',
componentProps: {
placeholder: '请输入绑定域名,多个域名请换行分隔',
rows: 3,
allowClear: true,
},
},
{
fieldName: 'status',
@@ -195,7 +199,7 @@ export function useGridColumns(
formatter: 'formatDateTime',
},
{
field: 'website',
field: 'websites',
title: '绑定域名',
},
{

View File

@@ -95,6 +95,7 @@ async function handleDeleteBatch() {
});
try {
await deleteTenantList(checkedIds.value);
checkedIds.value = [];
message.success($t('ui.actionMessage.deleteSuccess'));
onRefresh();
} finally {

View File

@@ -42,7 +42,16 @@ const [Modal, modalApi] = useVbenModal({
}
modalApi.lock();
// 提交表单
const data = (await formApi.getValues()) as SystemTenantApi.Tenant;
const formValues = (await formApi.getValues()) as SystemTenantApi.Tenant & {
websites: string;
};
// 将换行符分隔的字符串转换为数组
const data: SystemTenantApi.Tenant = {
...formValues,
websites: formValues.websites
? formValues.websites.split('\n').filter((item) => item.trim())
: [],
};
try {
await (formData.value ? updateTenant(data) : createTenant(data));
// 关闭并提示
@@ -66,8 +75,15 @@ const [Modal, modalApi] = useVbenModal({
modalApi.lock();
try {
formData.value = await getTenant(data.id as number);
// 将数组转换为换行符分隔的字符串
const formValues = {
...formData.value,
websites: Array.isArray(formData.value.websites)
? formData.value.websites.join('\n')
: formData.value.websites || '',
};
// 设置到 values
await formApi.setValues(formData.value);
await formApi.setValues(formValues);
} finally {
modalApi.unlock();
}
@@ -75,7 +91,7 @@ const [Modal, modalApi] = useVbenModal({
});
</script>
<template>
<Modal :title="getTitle" class="w-1/3">
<Modal :title="getTitle" class="w-1/2">
<Form class="mx-4" />
</Modal>
</template>

View File

@@ -76,6 +76,7 @@ async function handleDeleteBatch() {
});
try {
await deleteTenantPackageList(checkedIds.value);
checkedIds.value = [];
message.success($t('ui.actionMessage.deleteSuccess'));
onRefresh();
} finally {

View File

@@ -118,6 +118,7 @@ async function handleDeleteBatch() {
});
try {
await deleteUserList(checkedIds.value);
checkedIds.value = [];
message.success($t('ui.actionMessage.deleteSuccess'));
onRefresh();
} finally {

View File

@@ -24,3 +24,12 @@ VITE_APP_BAIDU_CODE = b79d8f49e2d38b26503b92810b740f45
# GoView域名
VITE_GOVIEW_URL='http://127.0.0.1:3000'
# API 加解密
VITE_APP_API_ENCRYPT_ENABLE = true
VITE_APP_API_ENCRYPT_HEADER = X-Api-Encrypt
VITE_APP_API_ENCRYPT_ALGORITHM = AES
VITE_APP_API_ENCRYPT_REQUEST_KEY = 52549111389893486934626385991395
VITE_APP_API_ENCRYPT_RESPONSE_KEY = 96103715984234343991809655248883
# VITE_APP_API_ENCRYPT_REQUEST_KEY = MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCls2rIpnGdYnLFgz1XU13GbNQ5DloyPpvW00FPGjqn5Z6JpK+kDtVlnkhwR87iRrE5Vf2WNqRX6vzbLSgveIQY8e8oqGCb829myjf1MuI+ZzN4ghf/7tEYhZJGPI9AbfxFqBUzm+kR3/HByAI22GLT96WM26QiMK8n3tIP/yiLswIDAQAB
# VITE_APP_API_ENCRYPT_RESPONSE_KEY = MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAOH8IfIFxL/MR9XIg1UDv5z1fGXQI93E8wrU4iPFovL/sEt9uSgSkjyidC2O7N+m7EKtoN6b1u7cEwXSkwf3kfK0jdWLSQaNpX5YshqXCBzbDfugDaxuyYrNA4/tIMs7mzZAk0APuRXB35Dmupou7Yw7TFW/7QhQmGfzeEKULQvnAgMBAAECgYAw8LqlQGyQoPv5p3gRxEMOCfgL0JzD3XBJKztiRd35RDh40Nx1ejgjW4dPioFwGiVWd2W8cAGHLzALdcQT2KDJh+T/tsd4SPmI6uSBBK6Ff2DkO+kFFcuYvfclQQKqxma5CaZOSqhgenacmgTMFeg2eKlY3symV6JlFNu/IKU42QJBAOhxAK/Eq3e61aYQV2JSguhMR3b8NXJJRroRs/QHEanksJtl+M+2qhkC9nQVXBmBkndnkU/l2tYcHfSBlAyFySMCQQD445tgm/J2b6qMQmuUGQAYDN8FIkHjeKmha+l/fv0igWm8NDlBAem91lNDIPBUzHL1X1+pcts5bjmq99YdOnhtAkAg2J8dN3B3idpZDiQbC8fd5bGPmdI/pSUudAP27uzLEjr2qrE/QPPGdwm2m7IZFJtK7kK1hKio6u48t/bg0iL7AkEAuUUs94h+v702Fnym+jJ2CHEkXvz2US8UDs52nWrZYiM1o1y4tfSHm8H8bv8JCAa9GHyriEawfBraILOmllFdLQJAQSRZy4wmlaG48MhVXodB85X+VZ9krGXZ2TLhz7kz9iuToy53l9jTkESt6L5BfBDCVdIwcXLYgK+8KFdHN5W7HQ==

View File

@@ -19,3 +19,5 @@ VITE_INJECT_APP_LOADING=true
VITE_APP_DEFAULT_USERNAME=admin
# 默认登录密码
VITE_APP_DEFAULT_PASSWORD=admin123
VITE_MALL_H5_DOMAIN='http://mall.yudao.iocoder.cn'

View File

@@ -21,3 +21,5 @@ VITE_INJECT_APP_LOADING=true
# 打包后是否生成dist.zip
VITE_ARCHIVER=true
VITE_MALL_H5_DOMAIN='http://mall.yudao.iocoder.cn'

View File

@@ -1,6 +1,6 @@
{
"name": "@vben/web-ele",
"version": "5.5.8",
"version": "5.5.9",
"homepage": "https://vben.pro",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {
@@ -45,13 +45,14 @@
"@vben/utils": "workspace:*",
"@vueuse/core": "catalog:",
"cropperjs": "catalog:",
"crypto-js": "catalog:",
"dayjs": "catalog:",
"element-plus": "catalog:",
"highlight.js": "catalog:",
"pinia": "catalog:",
"vue": "catalog:",
"vue-router": "catalog:"
"vue-dompurify-html": "catalog:",
"vue-router": "catalog:",
"vuedraggable": "catalog:"
},
"devDependencies": {
"unplugin-element-plus": "catalog:"

View File

@@ -64,7 +64,11 @@ export namespace AuthApi {
/** 登录 */
export async function loginApi(data: AuthApi.LoginParams) {
return requestClient.post<AuthApi.LoginResult>('/system/auth/login', data);
return requestClient.post<AuthApi.LoginResult>('/system/auth/login', data, {
headers: {
isEncrypt: false,
},
});
}
/** 刷新 accessToken */

View File

@@ -16,6 +16,7 @@ export namespace InfraFileConfigApi {
accessKey?: string;
accessSecret?: string;
pathStyle?: boolean;
enablePublicAccess?: boolean;
domain: string;
}

View File

@@ -48,6 +48,8 @@ export namespace MallCombinationActivityApi {
combinationPrice?: number;
/** 商品列表 */
products: CombinationProduct[];
/** 图片 */
picUrl?: string;
}
/** 扩展 SKU 配置 */

View File

@@ -52,7 +52,7 @@ export function deleteDiyPage(id: number) {
/** 获得装修页面属性 */
export function getDiyPageProperty(id: number) {
return requestClient.get<string>(`/promotion/diy-page/get-property?id=${id}`);
return requestClient.get(`/promotion/diy-page/get-property?id=${id}`);
}
/** 更新装修页面属性 */

View File

@@ -55,6 +55,8 @@ export namespace MallSeckillActivityApi {
seckillPrice?: number;
/** 秒杀商品列表 */
products?: SeckillProduct[];
/** 图片 */
picUrl?: string;
}
/** 扩展 SKU 配置 */

Some files were not shown because too many files have changed in this diff Show More