diff --git a/.gitignore b/.gitignore index c2a8a771f..3399f39c0 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,4 @@ vite.config.ts.* *.sln *.sw? .history +.cursor diff --git a/.vscode/extensions.json b/.vscode/extensions.json index e8dc9ed9b..c67d983e2 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -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 冲突 diff --git a/README.md b/README.md index 0d6c89646..7a71e761f 100644 --- a/README.md +++ b/README.md @@ -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 | ## 🔥 后端架构 diff --git a/apps/backend-mock/api/auth/codes.ts b/apps/backend-mock/api/auth/codes.ts index 7ba012705..e610b3381 100644 --- a/apps/backend-mock/api/auth/codes.ts +++ b/apps/backend-mock/api/auth/codes.ts @@ -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); diff --git a/apps/backend-mock/api/auth/login.post.ts b/apps/backend-mock/api/auth/login.post.ts index df5737a25..e23942c46 100644 --- a/apps/backend-mock/api/auth/login.post.ts +++ b/apps/backend-mock/api/auth/login.post.ts @@ -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); diff --git a/apps/backend-mock/api/auth/logout.post.ts b/apps/backend-mock/api/auth/logout.post.ts index ac6afe940..74c8d3151 100644 --- a/apps/backend-mock/api/auth/logout.post.ts +++ b/apps/backend-mock/api/auth/logout.post.ts @@ -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); diff --git a/apps/backend-mock/api/auth/refresh.post.ts b/apps/backend-mock/api/auth/refresh.post.ts index 7df4d34f5..7d8d3a51e 100644 --- a/apps/backend-mock/api/auth/refresh.post.ts +++ b/apps/backend-mock/api/auth/refresh.post.ts @@ -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) => { diff --git a/apps/backend-mock/api/demo/bigint.ts b/apps/backend-mock/api/demo/bigint.ts index 880cc5ea8..00d6c28c5 100644 --- a/apps/backend-mock/api/demo/bigint.ts +++ b/apps/backend-mock/api/demo/bigint.ts @@ -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) { diff --git a/apps/backend-mock/api/menu/all.ts b/apps/backend-mock/api/menu/all.ts index 580cee4f9..7923f7ca5 100644 --- a/apps/backend-mock/api/menu/all.ts +++ b/apps/backend-mock/api/menu/all.ts @@ -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); diff --git a/apps/backend-mock/api/status.ts b/apps/backend-mock/api/status.ts index 41773e1d5..43782095d 100644 --- a/apps/backend-mock/api/status.ts +++ b/apps/backend-mock/api/status.ts @@ -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)); diff --git a/apps/backend-mock/api/system/dept/.post.ts b/apps/backend-mock/api/system/dept/.post.ts index c529ea1bd..9a4896afa 100644 --- a/apps/backend-mock/api/system/dept/.post.ts +++ b/apps/backend-mock/api/system/dept/.post.ts @@ -1,3 +1,4 @@ +import { eventHandler } from 'h3'; import { verifyAccessToken } from '~/utils/jwt-utils'; import { sleep, diff --git a/apps/backend-mock/api/system/dept/[id].delete.ts b/apps/backend-mock/api/system/dept/[id].delete.ts index e48f051cc..eac0f5846 100644 --- a/apps/backend-mock/api/system/dept/[id].delete.ts +++ b/apps/backend-mock/api/system/dept/[id].delete.ts @@ -1,3 +1,4 @@ +import { eventHandler } from 'h3'; import { verifyAccessToken } from '~/utils/jwt-utils'; import { sleep, diff --git a/apps/backend-mock/api/system/dept/[id].put.ts b/apps/backend-mock/api/system/dept/[id].put.ts index aa55c0857..6805e1395 100644 --- a/apps/backend-mock/api/system/dept/[id].put.ts +++ b/apps/backend-mock/api/system/dept/[id].put.ts @@ -1,3 +1,4 @@ +import { eventHandler } from 'h3'; import { verifyAccessToken } from '~/utils/jwt-utils'; import { sleep, diff --git a/apps/backend-mock/api/system/dept/list.ts b/apps/backend-mock/api/system/dept/list.ts index ae819b622..a649a0d2f 100644 --- a/apps/backend-mock/api/system/dept/list.ts +++ b/apps/backend-mock/api/system/dept/list.ts @@ -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'; diff --git a/apps/backend-mock/api/system/menu/list.ts b/apps/backend-mock/api/system/menu/list.ts index 5328b2fdf..ce96bb14e 100644 --- a/apps/backend-mock/api/system/menu/list.ts +++ b/apps/backend-mock/api/system/menu/list.ts @@ -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'; diff --git a/apps/backend-mock/api/system/menu/name-exists.ts b/apps/backend-mock/api/system/menu/name-exists.ts index 5599c22b3..7d5551b3b 100644 --- a/apps/backend-mock/api/system/menu/name-exists.ts +++ b/apps/backend-mock/api/system/menu/name-exists.ts @@ -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 = {}; diff --git a/apps/backend-mock/api/system/menu/path-exists.ts b/apps/backend-mock/api/system/menu/path-exists.ts index 64774f790..f3c3be997 100644 --- a/apps/backend-mock/api/system/menu/path-exists.ts +++ b/apps/backend-mock/api/system/menu/path-exists.ts @@ -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 = { '/': 0 }; diff --git a/apps/backend-mock/api/system/role/list.ts b/apps/backend-mock/api/system/role/list.ts index 4d5f923e9..bad29a513 100644 --- a/apps/backend-mock/api/system/role/list.ts +++ b/apps/backend-mock/api/system/role/list.ts @@ -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'; diff --git a/apps/backend-mock/api/table/list.ts b/apps/backend-mock/api/table/list.ts index 3e6f705b8..6664b583e 100644 --- a/apps/backend-mock/api/table/list.ts +++ b/apps/backend-mock/api/table/list.ts @@ -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, + ); }); diff --git a/apps/backend-mock/api/test.get.ts b/apps/backend-mock/api/test.get.ts index ca4a500be..dc2ceef79 100644 --- a/apps/backend-mock/api/test.get.ts +++ b/apps/backend-mock/api/test.get.ts @@ -1 +1,3 @@ +import { defineEventHandler } from 'h3'; + export default defineEventHandler(() => 'Test get handler'); diff --git a/apps/backend-mock/api/test.post.ts b/apps/backend-mock/api/test.post.ts index 698cf211e..0e9e337a8 100644 --- a/apps/backend-mock/api/test.post.ts +++ b/apps/backend-mock/api/test.post.ts @@ -1 +1,3 @@ +import { defineEventHandler } from 'h3'; + export default defineEventHandler(() => 'Test post handler'); diff --git a/apps/backend-mock/api/upload.ts b/apps/backend-mock/api/upload.ts index 1bb9e602d..436b63cbf 100644 --- a/apps/backend-mock/api/upload.ts +++ b/apps/backend-mock/api/upload.ts @@ -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); diff --git a/apps/backend-mock/api/user/info.ts b/apps/backend-mock/api/user/info.ts index cfa2346cb..138cb4331 100644 --- a/apps/backend-mock/api/user/info.ts +++ b/apps/backend-mock/api/user/info.ts @@ -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); diff --git a/apps/backend-mock/middleware/1.api.ts b/apps/backend-mock/middleware/1.api.ts index bad9a41a1..339cda4db 100644 --- a/apps/backend-mock/middleware/1.api.ts +++ b/apps/backend-mock/middleware/1.api.ts @@ -1,3 +1,4 @@ +import { defineEventHandler } from 'h3'; import { forbiddenResponse, sleep } from '~/utils/response'; export default defineEventHandler(async (event) => { diff --git a/apps/backend-mock/routes/[...].ts b/apps/backend-mock/routes/[...].ts index 99f544b66..5a22563dc 100644 --- a/apps/backend-mock/routes/[...].ts +++ b/apps/backend-mock/routes/[...].ts @@ -1,3 +1,5 @@ +import { defineEventHandler } from 'h3'; + export default defineEventHandler(() => { return `

Hello Vben Admin

diff --git a/apps/backend-mock/utils/cookie-utils.ts b/apps/backend-mock/utils/cookie-utils.ts index 78f3aab73..187ce2f00 100644 --- a/apps/backend-mock/utils/cookie-utils.ts +++ b/apps/backend-mock/utils/cookie-utils.ts @@ -1,5 +1,7 @@ import type { EventHandlerRequest, H3Event } from 'h3'; +import { deleteCookie, getCookie, setCookie } from 'h3'; + export function clearRefreshTokenCookie(event: H3Event) { deleteCookie(event, 'jwt', { httpOnly: true, diff --git a/apps/backend-mock/utils/jwt-utils.ts b/apps/backend-mock/utils/jwt-utils.ts index 8cfc68436..718583070 100644 --- a/apps/backend-mock/utils/jwt-utils.ts +++ b/apps/backend-mock/utils/jwt-utils.ts @@ -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 { diff --git a/apps/backend-mock/utils/response.ts b/apps/backend-mock/utils/response.ts index 2a5a908f2..2d4242e98 100644 --- a/apps/backend-mock/utils/response.ts +++ b/apps/backend-mock/utils/response.ts @@ -1,5 +1,7 @@ import type { EventHandlerRequest, H3Event } from 'h3'; +import { setResponseStatus } from 'h3'; + export function useResponseSuccess(data: T) { return { code: 0, diff --git a/apps/web-antd/.env b/apps/web-antd/.env index 6b960186e..778a9fa1f 100644 --- a/apps/web-antd/.env +++ b/apps/web-antd/.env @@ -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== diff --git a/apps/web-antd/package.json b/apps/web-antd/package.json index a74ce8d0a..2c1694a97 100644 --- a/apps/web-antd/package.json +++ b/apps/web-antd/package.json @@ -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:", diff --git a/apps/web-antd/src/api/core/auth.ts b/apps/web-antd/src/api/core/auth.ts index ccb6da340..604644a1f 100644 --- a/apps/web-antd/src/api/core/auth.ts +++ b/apps/web-antd/src/api/core/auth.ts @@ -64,7 +64,11 @@ export namespace AuthApi { /** 登录 */ export async function loginApi(data: AuthApi.LoginParams) { - return requestClient.post('/system/auth/login', data); + return requestClient.post('/system/auth/login', data, { + headers: { + isEncrypt: false, + }, + }); } /** 刷新 accessToken */ diff --git a/apps/web-antd/src/api/erp/product/category/index.ts b/apps/web-antd/src/api/erp/product/category/index.ts index b16a91e3a..74e5465c1 100644 --- a/apps/web-antd/src/api/erp/product/category/index.ts +++ b/apps/web-antd/src/api/erp/product/category/index.ts @@ -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[]; // 子分类 } } diff --git a/apps/web-antd/src/api/erp/stock/out/index.ts b/apps/web-antd/src/api/erp/stock/out/index.ts index 2224e2ddb..a3873f948 100644 --- a/apps/web-antd/src/api/erp/stock/out/index.ts +++ b/apps/web-antd/src/api/erp/stock/out/index.ts @@ -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; // 备注 } /** 其它出库单分页查询参数 */ diff --git a/apps/web-antd/src/api/infra/file-config/index.ts b/apps/web-antd/src/api/infra/file-config/index.ts index 20a0ab3fa..1a2fb7707 100644 --- a/apps/web-antd/src/api/infra/file-config/index.ts +++ b/apps/web-antd/src/api/infra/file-config/index.ts @@ -16,6 +16,7 @@ export namespace InfraFileConfigApi { accessKey?: string; accessSecret?: string; pathStyle?: boolean; + enablePublicAccess?: boolean; domain: string; } diff --git a/apps/web-antd/src/api/request.ts b/apps/web-antd/src/api/request.ts index 7f78987b4..2568def87 100644 --- a/apps/web-antd/src/api/request.ts +++ b/apps/web-antd/src/api/request.ts @@ -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({ diff --git a/apps/web-antd/src/api/system/tenant/index.ts b/apps/web-antd/src/api/system/tenant/index.ts index c18a4dfab..6ee52e1d6 100644 --- a/apps/web-antd/src/api/system/tenant/index.ts +++ b/apps/web-antd/src/api/system/tenant/index.ts @@ -12,7 +12,7 @@ export namespace SystemTenantApi { contactMobile: string; accountCount: number; expireTime: Date; - website: string; + websites: string[]; status: number; } } diff --git a/apps/web-antd/src/components/Tinyflow/index.ts b/apps/web-antd/src/components/Tinyflow/index.ts index 33db19a5e..e014e38cc 100644 --- a/apps/web-antd/src/components/Tinyflow/index.ts +++ b/apps/web-antd/src/components/Tinyflow/index.ts @@ -1,2 +1,2 @@ -export { default as Tinyflow } from './tinyflow.vue'; +export { default as Tinyflow } from './Tinyflow.vue'; export * from './ui/typing'; diff --git a/apps/web-antd/src/views/ai/workflow/form/modules/workflow-design.vue b/apps/web-antd/src/views/ai/workflow/form/modules/workflow-design.vue index 6659b80be..fb4ec8cf2 100644 --- a/apps/web-antd/src/views/ai/workflow/form/modules/workflow-design.vue +++ b/apps/web-antd/src/views/ai/workflow/form/modules/workflow-design.vue @@ -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; diff --git a/apps/web-antd/src/views/bpm/processInstance/create/modules/form.vue b/apps/web-antd/src/views/bpm/processInstance/create/modules/form.vue index 29474006e..b818ebf63 100644 --- a/apps/web-antd/src/views/bpm/processInstance/create/modules/form.vue +++ b/apps/web-antd/src/views/bpm/processInstance/create/modules/form.vue @@ -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" > 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['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' }, + }, + ]; +} diff --git a/apps/web-antd/src/views/erp/product/category/index.vue b/apps/web-antd/src/views/erp/product/category/index.vue index 47c5b65b9..01fdede9e 100644 --- a/apps/web-antd/src/views/erp/product/category/index.vue +++ b/apps/web-antd/src/views/erp/product/category/index.vue @@ -1,34 +1,166 @@ diff --git a/apps/web-antd/src/views/erp/product/category/modules/form.vue b/apps/web-antd/src/views/erp/product/category/modules/form.vue new file mode 100644 index 000000000..ad6ffa89b --- /dev/null +++ b/apps/web-antd/src/views/erp/product/category/modules/form.vue @@ -0,0 +1,92 @@ + + + diff --git a/apps/web-antd/src/views/erp/product/product/data.ts b/apps/web-antd/src/views/erp/product/product/data.ts new file mode 100644 index 000000000..8b44beef8 --- /dev/null +++ b/apps/web-antd/src/views/erp/product/product/data.ts @@ -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' }, + }, + ]; +} diff --git a/apps/web-antd/src/views/erp/product/product/index.vue b/apps/web-antd/src/views/erp/product/product/index.vue index d52ab9c92..1748c0d09 100644 --- a/apps/web-antd/src/views/erp/product/product/index.vue +++ b/apps/web-antd/src/views/erp/product/product/index.vue @@ -1,34 +1,152 @@ diff --git a/apps/web-antd/src/views/erp/product/product/modules/form.vue b/apps/web-antd/src/views/erp/product/product/modules/form.vue new file mode 100644 index 000000000..cbaf8072c --- /dev/null +++ b/apps/web-antd/src/views/erp/product/product/modules/form.vue @@ -0,0 +1,86 @@ + + + diff --git a/apps/web-antd/src/views/erp/product/unit/data.ts b/apps/web-antd/src/views/erp/product/unit/data.ts new file mode 100644 index 000000000..7de094158 --- /dev/null +++ b/apps/web-antd/src/views/erp/product/unit/data.ts @@ -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' }, + }, + ]; +} diff --git a/apps/web-antd/src/views/erp/product/unit/index.vue b/apps/web-antd/src/views/erp/product/unit/index.vue index 70e5ebd61..1386c131f 100644 --- a/apps/web-antd/src/views/erp/product/unit/index.vue +++ b/apps/web-antd/src/views/erp/product/unit/index.vue @@ -1,34 +1,152 @@ diff --git a/apps/web-antd/src/views/erp/product/unit/modules/form.vue b/apps/web-antd/src/views/erp/product/unit/modules/form.vue new file mode 100644 index 000000000..4871e227c --- /dev/null +++ b/apps/web-antd/src/views/erp/product/unit/modules/form.vue @@ -0,0 +1,88 @@ + + + diff --git a/apps/web-antd/src/views/erp/purchase/order/index.vue b/apps/web-antd/src/views/erp/purchase/order/index.vue index 6331bd67c..67f7b9416 100644 --- a/apps/web-antd/src/views/erp/purchase/order/index.vue +++ b/apps/web-antd/src/views/erp/purchase/order/index.vue @@ -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', diff --git a/apps/web-antd/src/views/erp/purchase/order/modules/form.vue b/apps/web-antd/src/views/erp/purchase/order/modules/form.vue index 828acbe15..7805c0b22 100644 --- a/apps/web-antd/src/views/erp/purchase/order/modules/form.vue +++ b/apps/web-antd/src/views/erp/purchase/order/modules/form.vue @@ -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] : ''; diff --git a/apps/web-antd/src/views/erp/stock/in/modules/form.vue b/apps/web-antd/src/views/erp/stock/in/modules/form.vue index fffca0978..a18f6c1ce 100644 --- a/apps/web-antd/src/views/erp/stock/in/modules/form.vue +++ b/apps/web-antd/src/views/erp/stock/in/modules/form.vue @@ -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] : ''; diff --git a/apps/web-antd/src/views/erp/stock/out/data.ts b/apps/web-antd/src/views/erp/stock/out/data.ts new file mode 100644 index 000000000..ca2095c34 --- /dev/null +++ b/apps/web-antd/src/views/erp/stock/out/data.ts @@ -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' }, + }, + ]; +} diff --git a/apps/web-antd/src/views/erp/stock/out/index.vue b/apps/web-antd/src/views/erp/stock/out/index.vue index afbf0c8a5..b42896b37 100644 --- a/apps/web-antd/src/views/erp/stock/out/index.vue +++ b/apps/web-antd/src/views/erp/stock/out/index.vue @@ -1,34 +1,219 @@ diff --git a/apps/web-antd/src/views/erp/stock/out/modules/form.vue b/apps/web-antd/src/views/erp/stock/out/modules/form.vue new file mode 100644 index 000000000..7e8a84031 --- /dev/null +++ b/apps/web-antd/src/views/erp/stock/out/modules/form.vue @@ -0,0 +1,196 @@ + + + diff --git a/apps/web-antd/src/views/erp/stock/out/modules/stock-out-item-form.vue b/apps/web-antd/src/views/erp/stock/out/modules/stock-out-item-form.vue new file mode 100644 index 000000000..14ff4fe13 --- /dev/null +++ b/apps/web-antd/src/views/erp/stock/out/modules/stock-out-item-form.vue @@ -0,0 +1,367 @@ + + + + + diff --git a/apps/web-antd/src/views/infra/apiAccessLog/data.ts b/apps/web-antd/src/views/infra/apiAccessLog/data.ts index b49d55cee..e9cc48651 100644 --- a/apps/web-antd/src/views/infra/apiAccessLog/data.ts +++ b/apps/web-antd/src/views/infra/apiAccessLog/data.ts @@ -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 ''; }, }, { diff --git a/apps/web-antd/src/views/infra/apiErrorLog/data.ts b/apps/web-antd/src/views/infra/apiErrorLog/data.ts index ab78b277f..0d39dfe40 100644 --- a/apps/web-antd/src/views/infra/apiErrorLog/data.ts +++ b/apps/web-antd/src/views/infra/apiErrorLog/data.ts @@ -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, }); }, }, diff --git a/apps/web-antd/src/views/infra/codegen/index.vue b/apps/web-antd/src/views/infra/codegen/index.vue index 2473a0934..1783b2345 100644 --- a/apps/web-antd/src/views/infra/codegen/index.vue +++ b/apps/web-antd/src/views/infra/codegen/index.vue @@ -102,6 +102,7 @@ async function handleDeleteBatch() { }); try { await deleteCodegenTableList(checkedIds.value); + checkedIds.value = []; message.success($t('ui.actionMessage.deleteSuccess')); onRefresh(); } finally { diff --git a/apps/web-antd/src/views/infra/config/index.vue b/apps/web-antd/src/views/infra/config/index.vue index 68b7e9a3f..821552cec 100644 --- a/apps/web-antd/src/views/infra/config/index.vue +++ b/apps/web-antd/src/views/infra/config/index.vue @@ -84,6 +84,7 @@ async function handleDeleteBatch() { }); try { await deleteConfigList(checkedIds.value); + checkedIds.value = []; message.success($t('ui.actionMessage.deleteSuccess')); onRefresh(); } finally { diff --git a/apps/web-antd/src/views/infra/demo/demo01/index.vue b/apps/web-antd/src/views/infra/demo/demo01/index.vue index e92d20655..fa7e1cb4d 100644 --- a/apps/web-antd/src/views/infra/demo/demo01/index.vue +++ b/apps/web-antd/src/views/infra/demo/demo01/index.vue @@ -66,6 +66,7 @@ async function handleDeleteBatch() { }); try { await deleteDemo01ContactList(checkedIds.value); + checkedIds.value = []; message.success($t('ui.actionMessage.deleteSuccess')); onRefresh(); } finally { diff --git a/apps/web-antd/src/views/infra/demo/demo03/erp/index.vue b/apps/web-antd/src/views/infra/demo/demo03/erp/index.vue index 2c850ee9c..c47ff0d62 100644 --- a/apps/web-antd/src/views/infra/demo/demo03/erp/index.vue +++ b/apps/web-antd/src/views/infra/demo/demo03/erp/index.vue @@ -76,6 +76,7 @@ async function onDeleteBatch() { }); try { await deleteDemo03StudentList(checkedIds.value); + checkedIds.value = []; message.success($t('ui.actionMessage.deleteSuccess')); onRefresh(); } finally { diff --git a/apps/web-antd/src/views/infra/demo/demo03/erp/modules/demo03-course-list.vue b/apps/web-antd/src/views/infra/demo/demo03/erp/modules/demo03-course-list.vue index d5b15c914..d84959421 100644 --- a/apps/web-antd/src/views/infra/demo/demo03/erp/modules/demo03-course-list.vue +++ b/apps/web-antd/src/views/infra/demo/demo03/erp/modules/demo03-course-list.vue @@ -75,6 +75,7 @@ async function onDeleteBatch() { }); try { await deleteDemo03CourseList(checkedIds.value); + checkedIds.value = []; message.success($t('ui.actionMessage.deleteSuccess')); onRefresh(); } finally { diff --git a/apps/web-antd/src/views/infra/demo/demo03/erp/modules/demo03-grade-list.vue b/apps/web-antd/src/views/infra/demo/demo03/erp/modules/demo03-grade-list.vue index ab46b9745..9ce25c263 100644 --- a/apps/web-antd/src/views/infra/demo/demo03/erp/modules/demo03-grade-list.vue +++ b/apps/web-antd/src/views/infra/demo/demo03/erp/modules/demo03-grade-list.vue @@ -75,6 +75,7 @@ async function onDeleteBatch() { }); try { await deleteDemo03GradeList(checkedIds.value); + checkedIds.value = []; message.success($t('ui.actionMessage.deleteSuccess')); onRefresh(); } finally { diff --git a/apps/web-antd/src/views/infra/demo/demo03/inner/index.vue b/apps/web-antd/src/views/infra/demo/demo03/inner/index.vue index 79ce1b32b..c394f5fd9 100644 --- a/apps/web-antd/src/views/infra/demo/demo03/inner/index.vue +++ b/apps/web-antd/src/views/infra/demo/demo03/inner/index.vue @@ -75,6 +75,7 @@ async function onDeleteBatch() { }); try { await deleteDemo03StudentList(checkedIds.value); + checkedIds.value = []; message.success($t('ui.actionMessage.deleteSuccess')); onRefresh(); } finally { diff --git a/apps/web-antd/src/views/infra/demo/demo03/normal/index.vue b/apps/web-antd/src/views/infra/demo/demo03/normal/index.vue index 3dc77de61..e0b1c4dfb 100644 --- a/apps/web-antd/src/views/infra/demo/demo03/normal/index.vue +++ b/apps/web-antd/src/views/infra/demo/demo03/normal/index.vue @@ -78,6 +78,7 @@ async function onDeleteBatch() { }); try { await deleteDemo03StudentList(checkedIds.value); + checkedIds.value = []; message.success($t('ui.actionMessage.deleteSuccess')); onRefresh(); } finally { diff --git a/apps/web-antd/src/views/infra/demo/general/demo01/index.vue b/apps/web-antd/src/views/infra/demo/general/demo01/index.vue index a500736ac..d93c06c2a 100644 --- a/apps/web-antd/src/views/infra/demo/general/demo01/index.vue +++ b/apps/web-antd/src/views/infra/demo/general/demo01/index.vue @@ -123,6 +123,7 @@ async function handleDeleteBatch() { }); try { await deleteDemo01ContactList(checkedIds.value); + checkedIds.value = []; message.success($t('ui.actionMessage.deleteSuccess')); await getList(); } finally { diff --git a/apps/web-antd/src/views/infra/demo/general/demo03/erp/index.vue b/apps/web-antd/src/views/infra/demo/general/demo03/erp/index.vue index 62385532b..ce2e3a0f2 100644 --- a/apps/web-antd/src/views/infra/demo/general/demo03/erp/index.vue +++ b/apps/web-antd/src/views/infra/demo/general/demo03/erp/index.vue @@ -134,6 +134,7 @@ async function onDeleteBatch() { }); try { await deleteDemo03StudentList(checkedIds.value); + checkedIds.value = []; message.success($t('ui.actionMessage.deleteSuccess')); await getList(); } finally { diff --git a/apps/web-antd/src/views/infra/demo/general/demo03/erp/modules/demo03-course-list.vue b/apps/web-antd/src/views/infra/demo/general/demo03/erp/modules/demo03-course-list.vue index 47ed647c8..ae5a128d5 100644 --- a/apps/web-antd/src/views/infra/demo/general/demo03/erp/modules/demo03-course-list.vue +++ b/apps/web-antd/src/views/infra/demo/general/demo03/erp/modules/demo03-course-list.vue @@ -81,6 +81,7 @@ async function onDeleteBatch() { }); try { await deleteDemo03CourseList(checkedIds.value); + checkedIds.value = []; message.success($t('ui.actionMessage.deleteSuccess')); await getList(); } finally { diff --git a/apps/web-antd/src/views/infra/demo/general/demo03/erp/modules/demo03-grade-list.vue b/apps/web-antd/src/views/infra/demo/general/demo03/erp/modules/demo03-grade-list.vue index 0f8435c4b..4dbf52a89 100644 --- a/apps/web-antd/src/views/infra/demo/general/demo03/erp/modules/demo03-grade-list.vue +++ b/apps/web-antd/src/views/infra/demo/general/demo03/erp/modules/demo03-grade-list.vue @@ -81,6 +81,7 @@ async function onDeleteBatch() { }); try { await deleteDemo03GradeList(checkedIds.value); + checkedIds.value = []; message.success($t('ui.actionMessage.deleteSuccess')); await getList(); } finally { diff --git a/apps/web-antd/src/views/infra/demo/general/demo03/inner/index.vue b/apps/web-antd/src/views/infra/demo/general/demo03/inner/index.vue index b093a83de..7a9919859 100644 --- a/apps/web-antd/src/views/infra/demo/general/demo03/inner/index.vue +++ b/apps/web-antd/src/views/infra/demo/general/demo03/inner/index.vue @@ -130,6 +130,7 @@ async function onDeleteBatch() { }); try { await deleteDemo03StudentList(checkedIds.value); + checkedIds.value = []; message.success($t('ui.actionMessage.deleteSuccess')); await getList(); } finally { diff --git a/apps/web-antd/src/views/infra/demo/general/demo03/normal/index.vue b/apps/web-antd/src/views/infra/demo/general/demo03/normal/index.vue index 75db2f158..13e0572a3 100644 --- a/apps/web-antd/src/views/infra/demo/general/demo03/normal/index.vue +++ b/apps/web-antd/src/views/infra/demo/general/demo03/normal/index.vue @@ -124,6 +124,7 @@ async function onDeleteBatch() { }); try { await deleteDemo03StudentList(checkedIds.value); + checkedIds.value = []; message.success($t('ui.actionMessage.deleteSuccess')); await getList(); } finally { diff --git a/apps/web-antd/src/views/infra/file/index.vue b/apps/web-antd/src/views/infra/file/index.vue index cf92953c8..03b7057b6 100644 --- a/apps/web-antd/src/views/infra/file/index.vue +++ b/apps/web-antd/src/views/infra/file/index.vue @@ -84,6 +84,7 @@ async function handleDeleteBatch() { }); try { await deleteFileList(checkedIds.value); + checkedIds.value = []; message.success($t('ui.actionMessage.deleteSuccess')); onRefresh(); } finally { diff --git a/apps/web-antd/src/views/infra/fileConfig/data.ts b/apps/web-antd/src/views/infra/fileConfig/data.ts index 58cc4cd93..b28d53deb 100644 --- a/apps/web-antd/src/views/infra/fileConfig/data.ts +++ b/apps/web-antd/src/views/infra/fileConfig/data.ts @@ -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', diff --git a/apps/web-antd/src/views/infra/fileConfig/index.vue b/apps/web-antd/src/views/infra/fileConfig/index.vue index 019b3e25c..1667f7879 100644 --- a/apps/web-antd/src/views/infra/fileConfig/index.vue +++ b/apps/web-antd/src/views/infra/fileConfig/index.vue @@ -118,6 +118,7 @@ async function handleDeleteBatch() { }); try { await deleteFileConfigList(checkedIds.value); + checkedIds.value = []; message.success($t('ui.actionMessage.deleteSuccess')); onRefresh(); } finally { diff --git a/apps/web-antd/src/views/infra/job/index.vue b/apps/web-antd/src/views/infra/job/index.vue index 23798dc5f..8878913da 100644 --- a/apps/web-antd/src/views/infra/job/index.vue +++ b/apps/web-antd/src/views/infra/job/index.vue @@ -130,6 +130,7 @@ async function handleDeleteBatch() { }); try { await deleteJobList(checkedIds.value); + checkedIds.value = []; message.success($t('ui.actionMessage.deleteSuccess')); onRefresh(); } finally { diff --git a/apps/web-antd/src/views/system/dept/index.vue b/apps/web-antd/src/views/system/dept/index.vue index e3dbc8ffc..1dca1f7b6 100644 --- a/apps/web-antd/src/views/system/dept/index.vue +++ b/apps/web-antd/src/views/system/dept/index.vue @@ -93,6 +93,7 @@ async function handleDeleteBatch() { }); try { await deleteDeptList(checkedIds.value); + checkedIds.value = []; message.success($t('ui.actionMessage.deleteSuccess')); onRefresh(); } finally { diff --git a/apps/web-antd/src/views/system/dict/modules/data-grid.vue b/apps/web-antd/src/views/system/dict/modules/data-grid.vue index 57c6f87d3..a452368bd 100644 --- a/apps/web-antd/src/views/system/dict/modules/data-grid.vue +++ b/apps/web-antd/src/views/system/dict/modules/data-grid.vue @@ -90,6 +90,7 @@ async function handleDeleteBatch() { }); try { await deleteDictDataList(checkedIds.value); + checkedIds.value = []; message.success($t('ui.actionMessage.deleteSuccess')); onRefresh(); } finally { diff --git a/apps/web-antd/src/views/system/dict/modules/type-grid.vue b/apps/web-antd/src/views/system/dict/modules/type-grid.vue index 0104303d6..5e185130a 100644 --- a/apps/web-antd/src/views/system/dict/modules/type-grid.vue +++ b/apps/web-antd/src/views/system/dict/modules/type-grid.vue @@ -88,6 +88,7 @@ async function handleDeleteBatch() { }); try { await deleteDictTypeList(checkedIds.value); + checkedIds.value = []; message.success($t('ui.actionMessage.deleteSuccess')); onRefresh(); } finally { diff --git a/apps/web-antd/src/views/system/mail/account/index.vue b/apps/web-antd/src/views/system/mail/account/index.vue index 0d4626952..b8a19b8eb 100644 --- a/apps/web-antd/src/views/system/mail/account/index.vue +++ b/apps/web-antd/src/views/system/mail/account/index.vue @@ -76,6 +76,7 @@ async function handleDeleteBatch() { }); try { await deleteMailAccountList(checkedIds.value); + checkedIds.value = []; message.success($t('ui.actionMessage.deleteSuccess')); onRefresh(); } finally { diff --git a/apps/web-antd/src/views/system/mail/template/index.vue b/apps/web-antd/src/views/system/mail/template/index.vue index 21907b968..2e89d3173 100644 --- a/apps/web-antd/src/views/system/mail/template/index.vue +++ b/apps/web-antd/src/views/system/mail/template/index.vue @@ -88,6 +88,7 @@ async function handleDeleteBatch() { }); try { await deleteMailTemplateList(checkedIds.value); + checkedIds.value = []; message.success($t('ui.actionMessage.deleteSuccess')); onRefresh(); } finally { diff --git a/apps/web-antd/src/views/system/notice/index.vue b/apps/web-antd/src/views/system/notice/index.vue index 014ca535f..4bca09bcd 100644 --- a/apps/web-antd/src/views/system/notice/index.vue +++ b/apps/web-antd/src/views/system/notice/index.vue @@ -77,6 +77,7 @@ async function handleDeleteBatch() { }); try { await deleteNoticeList(checkedIds.value); + checkedIds.value = []; message.success($t('ui.actionMessage.deleteSuccess')); onRefresh(); } finally { diff --git a/apps/web-antd/src/views/system/notify/template/index.vue b/apps/web-antd/src/views/system/notify/template/index.vue index 9408af8f2..e1e437295 100644 --- a/apps/web-antd/src/views/system/notify/template/index.vue +++ b/apps/web-antd/src/views/system/notify/template/index.vue @@ -94,6 +94,7 @@ async function handleDeleteBatch() { }); try { await deleteNotifyTemplateList(checkedIds.value); + checkedIds.value = []; message.success($t('ui.actionMessage.deleteSuccess')); onRefresh(); } finally { diff --git a/apps/web-antd/src/views/system/post/index.vue b/apps/web-antd/src/views/system/post/index.vue index 305222f81..52dee5663 100644 --- a/apps/web-antd/src/views/system/post/index.vue +++ b/apps/web-antd/src/views/system/post/index.vue @@ -83,6 +83,7 @@ async function handleDeleteBatch() { }); try { await deletePostList(checkedIds.value); + checkedIds.value = []; message.success($t('ui.actionMessage.deleteSuccess')); onRefresh(); } finally { diff --git a/apps/web-antd/src/views/system/role/index.vue b/apps/web-antd/src/views/system/role/index.vue index b04805ee4..f411b8fd7 100644 --- a/apps/web-antd/src/views/system/role/index.vue +++ b/apps/web-antd/src/views/system/role/index.vue @@ -96,6 +96,7 @@ async function handleDeleteBatch() { }); try { await deleteRoleList(checkedIds.value); + checkedIds.value = []; message.success($t('ui.actionMessage.deleteSuccess')); onRefresh(); } finally { diff --git a/apps/web-antd/src/views/system/sms/channel/index.vue b/apps/web-antd/src/views/system/sms/channel/index.vue index ee8ae780e..77780e918 100644 --- a/apps/web-antd/src/views/system/sms/channel/index.vue +++ b/apps/web-antd/src/views/system/sms/channel/index.vue @@ -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', diff --git a/apps/web-antd/src/views/system/sms/template/index.vue b/apps/web-antd/src/views/system/sms/template/index.vue index 5daa00d1b..8fde83edd 100644 --- a/apps/web-antd/src/views/system/sms/template/index.vue +++ b/apps/web-antd/src/views/system/sms/template/index.vue @@ -94,6 +94,7 @@ async function handleDeleteBatch() { }); try { await deleteSmsTemplateList(checkedIds.value); + checkedIds.value = []; message.success($t('ui.actionMessage.deleteSuccess')); onRefresh(); } finally { diff --git a/apps/web-antd/src/views/system/tenant/data.ts b/apps/web-antd/src/views/system/tenant/data.ts index cdc5e2c6a..e9f363761 100644 --- a/apps/web-antd/src/views/system/tenant/data.ts +++ b/apps/web-antd/src/views/system/tenant/data.ts @@ -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: '绑定域名', }, { diff --git a/apps/web-antd/src/views/system/tenant/index.vue b/apps/web-antd/src/views/system/tenant/index.vue index 463f33a6d..2ffb1e72b 100644 --- a/apps/web-antd/src/views/system/tenant/index.vue +++ b/apps/web-antd/src/views/system/tenant/index.vue @@ -95,6 +95,7 @@ async function handleDeleteBatch() { }); try { await deleteTenantList(checkedIds.value); + checkedIds.value = []; message.success($t('ui.actionMessage.deleteSuccess')); onRefresh(); } finally { diff --git a/apps/web-antd/src/views/system/tenant/modules/form.vue b/apps/web-antd/src/views/system/tenant/modules/form.vue index 1f9b3bef9..b7924b04e 100644 --- a/apps/web-antd/src/views/system/tenant/modules/form.vue +++ b/apps/web-antd/src/views/system/tenant/modules/form.vue @@ -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({ }); diff --git a/apps/web-antd/src/views/system/tenantPackage/index.vue b/apps/web-antd/src/views/system/tenantPackage/index.vue index 1e967c534..438e1fe62 100644 --- a/apps/web-antd/src/views/system/tenantPackage/index.vue +++ b/apps/web-antd/src/views/system/tenantPackage/index.vue @@ -76,6 +76,7 @@ async function handleDeleteBatch() { }); try { await deleteTenantPackageList(checkedIds.value); + checkedIds.value = []; message.success($t('ui.actionMessage.deleteSuccess')); onRefresh(); } finally { diff --git a/apps/web-antd/src/views/system/user/index.vue b/apps/web-antd/src/views/system/user/index.vue index a0275ece9..45ff181e5 100644 --- a/apps/web-antd/src/views/system/user/index.vue +++ b/apps/web-antd/src/views/system/user/index.vue @@ -118,6 +118,7 @@ async function handleDeleteBatch() { }); try { await deleteUserList(checkedIds.value); + checkedIds.value = []; message.success($t('ui.actionMessage.deleteSuccess')); onRefresh(); } finally { diff --git a/apps/web-ele/.env b/apps/web-ele/.env index f72f25d44..eca5ebda5 100644 --- a/apps/web-ele/.env +++ b/apps/web-ele/.env @@ -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== diff --git a/apps/web-ele/.env.development b/apps/web-ele/.env.development index 02edf8dd7..77c13d398 100644 --- a/apps/web-ele/.env.development +++ b/apps/web-ele/.env.development @@ -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' diff --git a/apps/web-ele/.env.production b/apps/web-ele/.env.production index 910fd64cc..ac0408ec4 100644 --- a/apps/web-ele/.env.production +++ b/apps/web-ele/.env.production @@ -21,3 +21,5 @@ VITE_INJECT_APP_LOADING=true # 打包后是否生成dist.zip VITE_ARCHIVER=true + +VITE_MALL_H5_DOMAIN='http://mall.yudao.iocoder.cn' diff --git a/apps/web-ele/package.json b/apps/web-ele/package.json index 43a43dac7..ff2e2dd56 100644 --- a/apps/web-ele/package.json +++ b/apps/web-ele/package.json @@ -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:" diff --git a/apps/web-ele/src/api/core/auth.ts b/apps/web-ele/src/api/core/auth.ts index ccb6da340..604644a1f 100644 --- a/apps/web-ele/src/api/core/auth.ts +++ b/apps/web-ele/src/api/core/auth.ts @@ -64,7 +64,11 @@ export namespace AuthApi { /** 登录 */ export async function loginApi(data: AuthApi.LoginParams) { - return requestClient.post('/system/auth/login', data); + return requestClient.post('/system/auth/login', data, { + headers: { + isEncrypt: false, + }, + }); } /** 刷新 accessToken */ diff --git a/apps/web-ele/src/api/infra/file-config/index.ts b/apps/web-ele/src/api/infra/file-config/index.ts index 20a0ab3fa..1a2fb7707 100644 --- a/apps/web-ele/src/api/infra/file-config/index.ts +++ b/apps/web-ele/src/api/infra/file-config/index.ts @@ -16,6 +16,7 @@ export namespace InfraFileConfigApi { accessKey?: string; accessSecret?: string; pathStyle?: boolean; + enablePublicAccess?: boolean; domain: string; } diff --git a/apps/web-ele/src/api/mall/promotion/combination/combinationActivity.ts b/apps/web-ele/src/api/mall/promotion/combination/combinationActivity.ts index c0d53d6b2..486f05aa6 100644 --- a/apps/web-ele/src/api/mall/promotion/combination/combinationActivity.ts +++ b/apps/web-ele/src/api/mall/promotion/combination/combinationActivity.ts @@ -48,6 +48,8 @@ export namespace MallCombinationActivityApi { combinationPrice?: number; /** 商品列表 */ products: CombinationProduct[]; + /** 图片 */ + picUrl?: string; } /** 扩展 SKU 配置 */ diff --git a/apps/web-ele/src/api/mall/promotion/diy/page.ts b/apps/web-ele/src/api/mall/promotion/diy/page.ts index daa5e4b06..afdface5a 100644 --- a/apps/web-ele/src/api/mall/promotion/diy/page.ts +++ b/apps/web-ele/src/api/mall/promotion/diy/page.ts @@ -52,7 +52,7 @@ export function deleteDiyPage(id: number) { /** 获得装修页面属性 */ export function getDiyPageProperty(id: number) { - return requestClient.get(`/promotion/diy-page/get-property?id=${id}`); + return requestClient.get(`/promotion/diy-page/get-property?id=${id}`); } /** 更新装修页面属性 */ diff --git a/apps/web-ele/src/api/mall/promotion/seckill/seckillActivity.ts b/apps/web-ele/src/api/mall/promotion/seckill/seckillActivity.ts index e11e8828a..9a7e121dd 100644 --- a/apps/web-ele/src/api/mall/promotion/seckill/seckillActivity.ts +++ b/apps/web-ele/src/api/mall/promotion/seckill/seckillActivity.ts @@ -55,6 +55,8 @@ export namespace MallSeckillActivityApi { seckillPrice?: number; /** 秒杀商品列表 */ products?: SeckillProduct[]; + /** 图片 */ + picUrl?: string; } /** 扩展 SKU 配置 */ diff --git a/apps/web-ele/src/api/request.ts b/apps/web-ele/src/api/request.ts index af7d95ab9..97462d84a 100644 --- a/apps/web-ele/src/api/request.ts +++ b/apps/web-ele/src/api/request.ts @@ -12,6 +12,7 @@ import { RequestClient, } from '@vben/request'; import { useAccessStore } from '@vben/stores'; +import { createApiEncrypt } from '@vben/utils'; import { ElMessage } from 'element-plus'; @@ -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({ diff --git a/apps/web-ele/src/api/system/tenant/index.ts b/apps/web-ele/src/api/system/tenant/index.ts index c18a4dfab..6ee52e1d6 100644 --- a/apps/web-ele/src/api/system/tenant/index.ts +++ b/apps/web-ele/src/api/system/tenant/index.ts @@ -12,7 +12,7 @@ export namespace SystemTenantApi { contactMobile: string; accountCount: number; expireTime: Date; - website: string; + websites: string[]; status: number; } } diff --git a/apps/web-ele/src/assets/imgs/diy/app-nav-bar-mp.png b/apps/web-ele/src/assets/imgs/diy/app-nav-bar-mp.png new file mode 100644 index 000000000..c982804c7 Binary files /dev/null and b/apps/web-ele/src/assets/imgs/diy/app-nav-bar-mp.png differ diff --git a/apps/web-ele/src/assets/imgs/diy/statusBar.png b/apps/web-ele/src/assets/imgs/diy/statusBar.png new file mode 100644 index 000000000..b85562e42 Binary files /dev/null and b/apps/web-ele/src/assets/imgs/diy/statusBar.png differ diff --git a/apps/web-ele/src/bootstrap.ts b/apps/web-ele/src/bootstrap.ts index c366b2784..9b31950f4 100644 --- a/apps/web-ele/src/bootstrap.ts +++ b/apps/web-ele/src/bootstrap.ts @@ -1,4 +1,5 @@ import { createApp, watchEffect } from 'vue'; +import VueDOMPurifyHTML from 'vue-dompurify-html'; import { registerAccessDirective } from '@vben/access'; import { registerLoadingDirective } from '@vben/common-ui'; @@ -34,7 +35,7 @@ async function bootstrap(namespace: string) { // zIndex: 2000, // }); const app = createApp(App); - + app.use(VueDOMPurifyHTML); // 注册Element Plus提供的v-loading指令 app.directive('loading', ElLoading.directive); diff --git a/apps/web-ele/src/components/app-link-input/app-link-select-dialog.vue b/apps/web-ele/src/components/app-link-input/app-link-select-dialog.vue new file mode 100644 index 000000000..06d71be76 --- /dev/null +++ b/apps/web-ele/src/components/app-link-input/app-link-select-dialog.vue @@ -0,0 +1,241 @@ + + + diff --git a/apps/web-ele/src/components/app-link-input/data.ts b/apps/web-ele/src/components/app-link-input/data.ts new file mode 100644 index 000000000..550b88d76 --- /dev/null +++ b/apps/web-ele/src/components/app-link-input/data.ts @@ -0,0 +1,236 @@ +// APP 链接分组 +export interface AppLinkGroup { + // 分组名称 + name: string; + // 链接列表 + links: AppLink[]; +} + +// APP 链接 +export interface AppLink { + // 链接名称 + name: string; + // 链接地址 + path: string; + // 链接的类型 + type?: APP_LINK_TYPE_ENUM; +} + +// APP 链接类型(需要特殊处理,例如商品详情) +export enum APP_LINK_TYPE_ENUM { + // 拼团活动 + ACTIVITY_COMBINATION, + // 积分商城活动 + ACTIVITY_POINT, + // 秒杀活动 + ACTIVITY_SECKILL, + // 文章详情 + ARTICLE_DETAIL, + // 优惠券详情 + COUPON_DETAIL, + // 自定义页面详情 + DIY_PAGE_DETAIL, + // 品类列表 + PRODUCT_CATEGORY_LIST, + // 拼团商品详情 + PRODUCT_DETAIL_COMBINATION, + // 商品详情 + PRODUCT_DETAIL_NORMAL, + // 秒杀商品详情 + PRODUCT_DETAIL_SECKILL, + // 商品列表 + PRODUCT_LIST, +} + +// APP 链接列表(做一下持久化?) +export const APP_LINK_GROUP_LIST = [ + { + name: '商城', + links: [ + { + name: '首页', + path: '/pages/index/index', + }, + { + name: '商品分类', + path: '/pages/index/category', + type: APP_LINK_TYPE_ENUM.PRODUCT_CATEGORY_LIST, + }, + { + name: '购物车', + path: '/pages/index/cart', + }, + { + name: '个人中心', + path: '/pages/index/user', + }, + { + name: '商品搜索', + path: '/pages/index/search', + }, + { + name: '自定义页面', + path: '/pages/index/page', + type: APP_LINK_TYPE_ENUM.DIY_PAGE_DETAIL, + }, + { + name: '客服', + path: '/pages/chat/index', + }, + { + name: '系统设置', + path: '/pages/public/setting', + }, + { + name: '常见问题', + path: '/pages/public/faq', + }, + ], + }, + { + name: '商品', + links: [ + { + name: '商品列表', + path: '/pages/goods/list', + type: APP_LINK_TYPE_ENUM.PRODUCT_LIST, + }, + { + name: '商品详情', + path: '/pages/goods/index', + type: APP_LINK_TYPE_ENUM.PRODUCT_DETAIL_NORMAL, + }, + { + name: '拼团商品详情', + path: '/pages/goods/groupon', + type: APP_LINK_TYPE_ENUM.PRODUCT_DETAIL_COMBINATION, + }, + { + name: '秒杀商品详情', + path: '/pages/goods/seckill', + type: APP_LINK_TYPE_ENUM.PRODUCT_DETAIL_SECKILL, + }, + ], + }, + { + name: '营销活动', + links: [ + { + name: '拼团订单', + path: '/pages/activity/groupon/order', + }, + { + name: '营销商品', + path: '/pages/activity/index', + }, + { + name: '拼团活动', + path: '/pages/activity/groupon/list', + type: APP_LINK_TYPE_ENUM.ACTIVITY_COMBINATION, + }, + { + name: '秒杀活动', + path: '/pages/activity/seckill/list', + type: APP_LINK_TYPE_ENUM.ACTIVITY_SECKILL, + }, + { + name: '积分商城活动', + path: '/pages/activity/point/list', + type: APP_LINK_TYPE_ENUM.ACTIVITY_POINT, + }, + { + name: '签到中心', + path: '/pages/app/sign', + }, + { + name: '优惠券中心', + path: '/pages/coupon/list', + }, + { + name: '优惠券详情', + path: '/pages/coupon/detail', + type: APP_LINK_TYPE_ENUM.COUPON_DETAIL, + }, + { + name: '文章详情', + path: '/pages/public/richtext', + type: APP_LINK_TYPE_ENUM.ARTICLE_DETAIL, + }, + ], + }, + { + name: '分销商城', + links: [ + { + name: '分销中心', + path: '/pages/commission/index', + }, + { + name: '推广商品', + path: '/pages/commission/goods', + }, + { + name: '分销订单', + path: '/pages/commission/order', + }, + { + name: '我的团队', + path: '/pages/commission/team', + }, + ], + }, + { + name: '支付', + links: [ + { + name: '充值余额', + path: '/pages/pay/recharge', + }, + { + name: '充值记录', + path: '/pages/pay/recharge-log', + }, + ], + }, + { + name: '用户中心', + links: [ + { + name: '用户信息', + path: '/pages/user/info', + }, + { + name: '用户订单', + path: '/pages/order/list', + }, + { + name: '售后订单', + path: '/pages/order/aftersale/list', + }, + { + name: '商品收藏', + path: '/pages/user/goods-collect', + }, + { + name: '浏览记录', + path: '/pages/user/goods-log', + }, + { + name: '地址管理', + path: '/pages/user/address/list', + }, + { + name: '用户佣金', + path: '/pages/user/wallet/commission', + }, + { + name: '用户余额', + path: '/pages/user/wallet/money', + }, + { + name: '用户积分', + path: '/pages/user/wallet/score', + }, + ], + }, +] as AppLinkGroup[]; diff --git a/apps/web-ele/src/components/app-link-input/index.vue b/apps/web-ele/src/components/app-link-input/index.vue new file mode 100644 index 000000000..5b608c4e1 --- /dev/null +++ b/apps/web-ele/src/components/app-link-input/index.vue @@ -0,0 +1,48 @@ + + diff --git a/apps/web-ele/src/components/color-input/index.vue b/apps/web-ele/src/components/color-input/index.vue new file mode 100644 index 000000000..16170c230 --- /dev/null +++ b/apps/web-ele/src/components/color-input/index.vue @@ -0,0 +1,38 @@ + + + + + diff --git a/apps/web-ele/src/components/diy-editor/components/component-container-property.vue b/apps/web-ele/src/components/diy-editor/components/component-container-property.vue new file mode 100644 index 000000000..24062a191 --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/component-container-property.vue @@ -0,0 +1,200 @@ + + + + + diff --git a/apps/web-ele/src/components/diy-editor/components/component-container.vue b/apps/web-ele/src/components/diy-editor/components/component-container.vue new file mode 100644 index 000000000..5a9b803d1 --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/component-container.vue @@ -0,0 +1,269 @@ + + + + diff --git a/apps/web-ele/src/components/diy-editor/components/component-library.vue b/apps/web-ele/src/components/diy-editor/components/component-library.vue new file mode 100644 index 000000000..628c1c559 --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/component-library.vue @@ -0,0 +1,221 @@ + + + + + diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/Carousel/config.ts b/apps/web-ele/src/components/diy-editor/components/mobile/Carousel/config.ts new file mode 100644 index 000000000..749d36a32 --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/Carousel/config.ts @@ -0,0 +1,61 @@ +import type { + ComponentStyle, + DiyComponent, +} from '#/components/diy-editor/util'; + +/** 轮播图属性 */ +export interface CarouselProperty { + // 类型:默认 | 卡片 + type: 'card' | 'default'; + // 指示器样式:点 | 数字 + indicator: 'dot' | 'number'; + // 是否自动播放 + autoplay: boolean; + // 播放间隔 + interval: number; + // 轮播内容 + items: CarouselItemProperty[]; + // 组件样式 + style: ComponentStyle; +} +// 轮播内容属性 +export interface CarouselItemProperty { + // 类型:图片 | 视频 + type: 'img' | 'video'; + // 图片链接 + imgUrl: string; + // 视频链接 + videoUrl: string; + // 跳转链接 + url: string; +} + +// 定义组件 +export const component = { + id: 'Carousel', + name: '轮播图', + icon: 'system-uicons:carousel', + property: { + type: 'default', + indicator: 'dot', + autoplay: false, + interval: 3, + items: [ + { + type: 'img', + imgUrl: 'https://static.iocoder.cn/mall/banner-01.jpg', + videoUrl: '', + }, + { + type: 'img', + imgUrl: 'https://static.iocoder.cn/mall/banner-02.jpg', + videoUrl: '', + }, + ] as CarouselItemProperty[], + style: { + bgType: 'color', + bgColor: '#fff', + marginBottom: 8, + } as ComponentStyle, + }, +} as DiyComponent; diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/Carousel/index.vue b/apps/web-ele/src/components/diy-editor/components/mobile/Carousel/index.vue new file mode 100644 index 000000000..df256770c --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/Carousel/index.vue @@ -0,0 +1,50 @@ + + + + diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/Carousel/property.vue b/apps/web-ele/src/components/diy-editor/components/mobile/Carousel/property.vue new file mode 100644 index 000000000..21da40439 --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/Carousel/property.vue @@ -0,0 +1,133 @@ + + + + + diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/Divider/config.ts b/apps/web-ele/src/components/diy-editor/components/mobile/Divider/config.ts new file mode 100644 index 000000000..7147ef9b2 --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/Divider/config.ts @@ -0,0 +1,29 @@ +import type { DiyComponent } from '#/components/diy-editor/util'; + +/** 分割线属性 */ +export interface DividerProperty { + // 高度 + height: number; + // 线宽 + lineWidth: number; + // 边距类型 + paddingType: 'horizontal' | 'none'; + // 颜色 + lineColor: string; + // 类型 + borderType: 'dashed' | 'dotted' | 'none' | 'solid'; +} + +// 定义组件 +export const component = { + id: 'Divider', + name: '分割线', + icon: 'tdesign:component-divider-vertical', + property: { + height: 30, + lineWidth: 1, + paddingType: 'none', + lineColor: '#dcdfe6', + borderType: 'solid', + }, +} as DiyComponent; diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/Divider/index.vue b/apps/web-ele/src/components/diy-editor/components/mobile/Divider/index.vue new file mode 100644 index 000000000..1736db389 --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/Divider/index.vue @@ -0,0 +1,29 @@ + + + + + diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/Divider/property.vue b/apps/web-ele/src/components/diy-editor/components/mobile/Divider/property.vue new file mode 100644 index 000000000..7746b73bd --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/Divider/property.vue @@ -0,0 +1,106 @@ + + + + + diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/Popover/config.ts b/apps/web-ele/src/components/diy-editor/components/mobile/Popover/config.ts new file mode 100644 index 000000000..b842d19f5 --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/Popover/config.ts @@ -0,0 +1,26 @@ +import type { DiyComponent } from '#/components/diy-editor/util'; + +/** 弹窗广告属性 */ +export interface PopoverProperty { + list: PopoverItemProperty[]; +} + +export interface PopoverItemProperty { + // 图片地址 + imgUrl: string; + // 跳转连接 + url: string; + // 显示类型:仅显示一次、每次启动都会显示 + showType: 'always' | 'once'; +} + +// 定义组件 +export const component = { + id: 'Popover', + name: '弹窗广告', + icon: 'carbon:popup', + position: 'fixed', + property: { + list: [{ showType: 'once' }], + }, +} as DiyComponent; diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/Popover/index.vue b/apps/web-ele/src/components/diy-editor/components/mobile/Popover/index.vue new file mode 100644 index 000000000..bc724eca9 --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/Popover/index.vue @@ -0,0 +1,44 @@ + + + + diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/Popover/property.vue b/apps/web-ele/src/components/diy-editor/components/mobile/Popover/property.vue new file mode 100644 index 000000000..0d69fd9db --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/Popover/property.vue @@ -0,0 +1,58 @@ + + + + + diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/coupon-card/component.tsx b/apps/web-ele/src/components/diy-editor/components/mobile/coupon-card/component.tsx new file mode 100644 index 000000000..d4dbfee44 --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/coupon-card/component.tsx @@ -0,0 +1,4 @@ +// 导出所有优惠券相关组件 +export { CouponDiscount } from './coupon-discount'; +export { CouponDiscountDesc } from './coupon-discount-desc'; +export { CouponValidTerm } from './coupon-validTerm'; diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/coupon-card/config.ts b/apps/web-ele/src/components/diy-editor/components/mobile/coupon-card/config.ts new file mode 100644 index 000000000..ddfc1857a --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/coupon-card/config.ts @@ -0,0 +1,50 @@ +import type { + ComponentStyle, + DiyComponent, +} from '#/components/diy-editor/util'; + +/** 商品卡片属性 */ +export interface CouponCardProperty { + // 列数 + columns: number; + // 背景图 + bgImg: string; + // 文字颜色 + textColor: string; + // 按钮样式 + button: { + // 背景颜色 + bgColor: string; + // 颜色 + color: string; + }; + // 间距 + space: number; + // 优惠券编号列表 + couponIds: number[]; + // 组件样式 + style: ComponentStyle; +} + +// 定义组件 +export const component = { + id: 'CouponCard', + name: '优惠券', + icon: 'ep:ticket', + property: { + columns: 1, + bgImg: '', + textColor: '#E9B461', + button: { + color: '#434343', + bgColor: '', + }, + space: 0, + couponIds: [], + style: { + bgType: 'color', + bgColor: '', + marginBottom: 8, + } as ComponentStyle, + }, +} as DiyComponent; diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/coupon-card/coupon-discount-desc.tsx b/apps/web-ele/src/components/diy-editor/components/mobile/coupon-card/coupon-discount-desc.tsx new file mode 100644 index 000000000..634cb88c6 --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/coupon-card/coupon-discount-desc.tsx @@ -0,0 +1,35 @@ +import type { MallCouponTemplateApi } from '#/api/mall/promotion/coupon/couponTemplate'; + +import { defineComponent } from 'vue'; + +import { floatToFixed2 } from '@vben/utils'; + +import { PromotionDiscountTypeEnum } from '#/utils/constants'; + +// 优惠描述 +export const CouponDiscountDesc = defineComponent({ + name: 'CouponDiscountDesc', + props: { + coupon: { + type: Object as () => MallCouponTemplateApi.CouponTemplate, + required: true, + }, + }, + setup(props) { + const coupon = props.coupon as MallCouponTemplateApi.CouponTemplate; + // 使用条件 + const useCondition = + coupon.usePrice > 0 ? `满${floatToFixed2(coupon.usePrice)}元,` : ''; + // 优惠描述 + const discountDesc = + coupon.discountType === PromotionDiscountTypeEnum.PRICE.type + ? `减${floatToFixed2(coupon.discountPrice)}元` + : `打${coupon.discountPercent / 10}折`; + return () => ( +
+ {useCondition} + {discountDesc} +
+ ); + }, +}); diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/coupon-card/coupon-discount.tsx b/apps/web-ele/src/components/diy-editor/components/mobile/coupon-card/coupon-discount.tsx new file mode 100644 index 000000000..bde532e4a --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/coupon-card/coupon-discount.tsx @@ -0,0 +1,35 @@ +import type { MallCouponTemplateApi } from '#/api/mall/promotion/coupon/couponTemplate'; + +import { defineComponent } from 'vue'; + +import { floatToFixed2 } from '@vben/utils'; + +import { PromotionDiscountTypeEnum } from '#/utils/constants'; + +// 优惠值 +export const CouponDiscount = defineComponent({ + name: 'CouponDiscount', + props: { + coupon: { + type: Object as () => MallCouponTemplateApi.CouponTemplate, + required: true, + }, + }, + setup(props) { + const coupon = props.coupon as MallCouponTemplateApi.CouponTemplate; + // 折扣 + let value = `${coupon.discountPercent / 10}`; + let suffix = ' 折'; + // 满减 + if (coupon.discountType === PromotionDiscountTypeEnum.PRICE.type) { + value = floatToFixed2(coupon.discountPrice); + suffix = ' 元'; + } + return () => ( +
+ {value} + {suffix} +
+ ); + }, +}); diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/coupon-card/coupon-validTerm.tsx b/apps/web-ele/src/components/diy-editor/components/mobile/coupon-card/coupon-validTerm.tsx new file mode 100644 index 000000000..c96903f15 --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/coupon-card/coupon-validTerm.tsx @@ -0,0 +1,29 @@ +import type { MallCouponTemplateApi } from '#/api/mall/promotion/coupon/couponTemplate'; + +import { defineComponent } from 'vue'; + +import { formatDate } from '@vben/utils'; + +import { CouponTemplateValidityTypeEnum } from '#/utils/constants'; + +// 有效期 +export const CouponValidTerm = defineComponent({ + name: 'CouponValidTerm', + props: { + coupon: { + type: Object as () => MallCouponTemplateApi.CouponTemplate, + required: true, + }, + }, + setup(props) { + const coupon = props.coupon as MallCouponTemplateApi.CouponTemplate; + const text = + coupon.validityType === CouponTemplateValidityTypeEnum.DATE.type + ? `有效期:${formatDate(coupon.validStartTime, 'YYYY-MM-DD')} 至 ${formatDate( + coupon.validEndTime, + 'YYYY-MM-DD', + )}` + : `领取后第 ${coupon.fixedStartTerm} - ${coupon.fixedEndTerm} 天内可用`; + return () =>
{text}
; + }, +}); diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/coupon-card/index.vue b/apps/web-ele/src/components/diy-editor/components/mobile/coupon-card/index.vue new file mode 100644 index 000000000..eb5fbb1a2 --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/coupon-card/index.vue @@ -0,0 +1,162 @@ + + + diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/coupon-card/property.vue b/apps/web-ele/src/components/diy-editor/components/mobile/coupon-card/property.vue new file mode 100644 index 000000000..55332dede --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/coupon-card/property.vue @@ -0,0 +1,161 @@ + + + + + diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/floating-action-button/config.ts b/apps/web-ele/src/components/diy-editor/components/mobile/floating-action-button/config.ts new file mode 100644 index 000000000..7972bd3bc --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/floating-action-button/config.ts @@ -0,0 +1,36 @@ +import type { DiyComponent } from '#/components/diy-editor/util'; + +// 悬浮按钮属性 +export interface FloatingActionButtonProperty { + // 展开方向 + direction: 'horizontal' | 'vertical'; + // 是否显示文字 + showText: boolean; + // 按钮列表 + list: FloatingActionButtonItemProperty[]; +} + +// 悬浮按钮项属性 +export interface FloatingActionButtonItemProperty { + // 图片地址 + imgUrl: string; + // 跳转连接 + url: string; + // 文字 + text: string; + // 文字颜色 + textColor: string; +} + +// 定义组件 +export const component = { + id: 'FloatingActionButton', + name: '悬浮按钮', + icon: 'tabler:float-right', + position: 'fixed', + property: { + direction: 'vertical', + showText: true, + list: [{ textColor: '#fff' }], + }, +} as DiyComponent; diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/floating-action-button/index.vue b/apps/web-ele/src/components/diy-editor/components/mobile/floating-action-button/index.vue new file mode 100644 index 000000000..eb6563766 --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/floating-action-button/index.vue @@ -0,0 +1,92 @@ + + + + diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/floating-action-button/property.vue b/apps/web-ele/src/components/diy-editor/components/mobile/floating-action-button/property.vue new file mode 100644 index 000000000..2be023a87 --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/floating-action-button/property.vue @@ -0,0 +1,66 @@ + + + + + diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/hot-zone/components/hot-zone-edit-dialog/controller.ts b/apps/web-ele/src/components/diy-editor/components/mobile/hot-zone/components/hot-zone-edit-dialog/controller.ts new file mode 100644 index 000000000..93a264b5e --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/hot-zone/components/hot-zone-edit-dialog/controller.ts @@ -0,0 +1,175 @@ +import type { StyleValue } from 'vue'; + +import type { HotZoneItemProperty } from '#/components/diy-editor/components/mobile/HotZone/config'; + +// 热区的最小宽高 +export const HOT_ZONE_MIN_SIZE = 100; + +// 控制的类型 +export enum CONTROL_TYPE_ENUM { + LEFT, + TOP, + WIDTH, + HEIGHT, +} + +// 定义热区的控制点 +export interface ControlDot { + position: string; + types: CONTROL_TYPE_ENUM[]; + style: StyleValue; +} + +// 热区的8个控制点 +export const CONTROL_DOT_LIST = [ + { + position: '左上角', + types: [ + CONTROL_TYPE_ENUM.LEFT, + CONTROL_TYPE_ENUM.TOP, + CONTROL_TYPE_ENUM.WIDTH, + CONTROL_TYPE_ENUM.HEIGHT, + ], + style: { left: '-5px', top: '-5px', cursor: 'nwse-resize' }, + }, + { + position: '上方中间', + types: [CONTROL_TYPE_ENUM.TOP, CONTROL_TYPE_ENUM.HEIGHT], + style: { + left: '50%', + top: '-5px', + cursor: 'n-resize', + transform: 'translateX(-50%)', + }, + }, + { + position: '右上角', + types: [ + CONTROL_TYPE_ENUM.TOP, + CONTROL_TYPE_ENUM.WIDTH, + CONTROL_TYPE_ENUM.HEIGHT, + ], + style: { right: '-5px', top: '-5px', cursor: 'nesw-resize' }, + }, + { + position: '右侧中间', + types: [CONTROL_TYPE_ENUM.WIDTH], + style: { + right: '-5px', + top: '50%', + cursor: 'e-resize', + transform: 'translateX(-50%)', + }, + }, + { + position: '右下角', + types: [CONTROL_TYPE_ENUM.WIDTH, CONTROL_TYPE_ENUM.HEIGHT], + style: { right: '-5px', bottom: '-5px', cursor: 'nwse-resize' }, + }, + { + position: '下方中间', + types: [CONTROL_TYPE_ENUM.HEIGHT], + style: { + left: '50%', + bottom: '-5px', + cursor: 's-resize', + transform: 'translateX(-50%)', + }, + }, + { + position: '左下角', + types: [ + CONTROL_TYPE_ENUM.LEFT, + CONTROL_TYPE_ENUM.WIDTH, + CONTROL_TYPE_ENUM.HEIGHT, + ], + style: { left: '-5px', bottom: '-5px', cursor: 'nesw-resize' }, + }, + { + position: '左侧中间', + types: [CONTROL_TYPE_ENUM.LEFT, CONTROL_TYPE_ENUM.WIDTH], + style: { + left: '-5px', + top: '50%', + cursor: 'w-resize', + transform: 'translateX(-50%)', + }, + }, +] as ControlDot[]; + +// region 热区的缩放 +// 热区的缩放比例 +export const HOT_ZONE_SCALE_RATE = 2; +// 缩小:缩回适合手机屏幕的大小 +export const zoomOut = (list?: HotZoneItemProperty[]) => { + return ( + list?.map((hotZone) => ({ + ...hotZone, + left: (hotZone.left /= HOT_ZONE_SCALE_RATE), + top: (hotZone.top /= HOT_ZONE_SCALE_RATE), + width: (hotZone.width /= HOT_ZONE_SCALE_RATE), + height: (hotZone.height /= HOT_ZONE_SCALE_RATE), + })) || [] + ); +}; +// 放大:作用是为了方便在电脑屏幕上编辑 +export const zoomIn = (list?: HotZoneItemProperty[]) => { + return ( + list?.map((hotZone) => ({ + ...hotZone, + left: (hotZone.left *= HOT_ZONE_SCALE_RATE), + top: (hotZone.top *= HOT_ZONE_SCALE_RATE), + width: (hotZone.width *= HOT_ZONE_SCALE_RATE), + height: (hotZone.height *= HOT_ZONE_SCALE_RATE), + })) || [] + ); +}; +// endregion + +/** + * 封装热区拖拽 + * + * 注:为什么不使用vueuse的useDraggable。在本场景下,其使用方式比较复杂 + * @param hotZone 热区 + * @param downEvent 鼠标按下事件 + * @param callback 回调函数 + */ +export const useDraggable = ( + hotZone: HotZoneItemProperty, + downEvent: MouseEvent, + callback: ( + left: number, + top: number, + width: number, + height: number, + moveWidth: number, + moveHeight: number, + ) => void, +) => { + // 阻止事件冒泡 + downEvent.stopPropagation(); + + // 移动前的鼠标坐标 + const { clientX: startX, clientY: startY } = downEvent; + // 移动前的热区坐标、大小 + const { left, top, width, height } = hotZone; + + // 监听鼠标移动 + const handleMouseMove = (e: MouseEvent) => { + // 移动宽度 + const moveWidth = e.clientX - startX; + // 移动高度 + const moveHeight = e.clientY - startY; + // 移动回调 + callback(left, top, width, height, moveWidth, moveHeight); + }; + + // 松开鼠标后,结束拖拽 + const handleMouseUp = () => { + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', handleMouseUp); + }; + + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseup', handleMouseUp); +}; diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/hot-zone/components/hot-zone-edit-dialog/index.vue b/apps/web-ele/src/components/diy-editor/components/mobile/hot-zone/components/hot-zone-edit-dialog/index.vue new file mode 100644 index 000000000..0a72633d6 --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/hot-zone/components/hot-zone-edit-dialog/index.vue @@ -0,0 +1,284 @@ + + + + + diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/hot-zone/config.ts b/apps/web-ele/src/components/diy-editor/components/mobile/hot-zone/config.ts new file mode 100644 index 000000000..71e39d9cd --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/hot-zone/config.ts @@ -0,0 +1,46 @@ +import type { + ComponentStyle, + DiyComponent, +} from '#/components/diy-editor/util'; + +/** 热区属性 */ +export interface HotZoneProperty { + // 图片地址 + imgUrl: string; + // 导航菜单列表 + list: HotZoneItemProperty[]; + // 组件样式 + style: ComponentStyle; +} + +/** 热区项目属性 */ +export interface HotZoneItemProperty { + // 链接的名称 + name: string; + // 链接 + url: string; + // 宽 + width: number; + // 高 + height: number; + // 上 + top: number; + // 左 + left: number; +} + +// 定义组件 +export const component = { + id: 'HotZone', + name: '热区', + icon: 'tabler:hand-click', + property: { + imgUrl: '', + list: [] as HotZoneItemProperty[], + style: { + bgType: 'color', + bgColor: '#fff', + marginBottom: 8, + } as ComponentStyle, + }, +} as DiyComponent; diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/hot-zone/index.vue b/apps/web-ele/src/components/diy-editor/components/mobile/hot-zone/index.vue new file mode 100644 index 000000000..dd36b550f --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/hot-zone/index.vue @@ -0,0 +1,47 @@ + + + + + diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/hot-zone/property.vue b/apps/web-ele/src/components/diy-editor/components/mobile/hot-zone/property.vue new file mode 100644 index 000000000..e3c602f65 --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/hot-zone/property.vue @@ -0,0 +1,81 @@ + + + + + diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/image-bar/config.ts b/apps/web-ele/src/components/diy-editor/components/mobile/image-bar/config.ts new file mode 100644 index 000000000..53a87403c --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/image-bar/config.ts @@ -0,0 +1,30 @@ +import type { + ComponentStyle, + DiyComponent, +} from '#/components/diy-editor/util'; + +/** 图片展示属性 */ +export interface ImageBarProperty { + // 图片链接 + imgUrl: string; + // 跳转链接 + url: string; + // 组件样式 + style: ComponentStyle; +} + +// 定义组件 +export const component = { + id: 'ImageBar', + name: '图片展示', + icon: 'ep:picture', + property: { + imgUrl: '', + url: '', + style: { + bgType: 'color', + bgColor: '#fff', + marginBottom: 8, + } as ComponentStyle, + }, +} as DiyComponent; diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/image-bar/index.vue b/apps/web-ele/src/components/diy-editor/components/mobile/image-bar/index.vue new file mode 100644 index 000000000..4562f7b32 --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/image-bar/index.vue @@ -0,0 +1,31 @@ + + + + diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/image-bar/property.vue b/apps/web-ele/src/components/diy-editor/components/mobile/image-bar/property.vue new file mode 100644 index 000000000..ce1b3ad5b --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/image-bar/property.vue @@ -0,0 +1,41 @@ + + + + + diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/index.ts b/apps/web-ele/src/components/diy-editor/components/mobile/index.ts new file mode 100644 index 000000000..454e06a0b --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/index.ts @@ -0,0 +1,69 @@ +import { defineAsyncComponent } from 'vue'; + +/* + * 组件注册 + * + * 组件规范: + * 1. 每个子目录就是一个独立的组件,每个目录包括以下三个文件: + * 2. config.ts:组件配置,必选,用于定义组件、组件默认的属性、定义属性的类型 + * 3. index.vue:组件展示,用于展示组件的渲染效果。可以不提供,如 Page(页面设置),只需要属性配置表单即可 + * 4. property.vue:组件属性表单,用于配置组件,必选, + * + * 注: + * 组件ID以config.ts中配置的id为准,与组件目录的名称无关,但还是建议组件目录的名称与组件ID保持一致 + */ + +// 导入组件界面模块 +const viewModules: Record = import.meta.glob('./*/*.vue'); +// 导入配置模块 +const configModules: Record = import.meta.glob('./*/config.ts', { + eager: true, +}); + +// 界面模块 +const components: Record = {}; +// 组件配置模块 +const componentConfigs: Record = {}; + +// 组件界面的类型 +type ViewType = 'index' | 'property'; + +/** + * 注册组件的界面模块 + * + * @param componentId 组件ID + * @param configPath 配置模块的文件路径 + * @param viewType 组件界面的类型 + */ +const registerComponentViewModule = ( + componentId: string, + configPath: string, + viewType: ViewType, +) => { + const viewPath = configPath.replace('config.ts', `${viewType}.vue`); + const viewModule = viewModules[viewPath]; + if (viewModule) { + // 定义异步组件 + components[componentId] = defineAsyncComponent(viewModule); + } +}; + +// 注册 +Object.keys(configModules).forEach((modulePath: string) => { + const component = configModules[modulePath].component; + const componentId = component?.id; + if (componentId) { + // 注册组件 + componentConfigs[componentId] = component; + // 注册预览界面 + registerComponentViewModule(componentId, modulePath, 'index'); + // 注册属性配置表单 + registerComponentViewModule( + `${componentId}Property`, + modulePath, + 'property', + ); + } +}); + +export { componentConfigs, components }; diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/magic-cube/config.ts b/apps/web-ele/src/components/diy-editor/components/mobile/magic-cube/config.ts new file mode 100644 index 000000000..b364fbb35 --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/magic-cube/config.ts @@ -0,0 +1,56 @@ +import type { + ComponentStyle, + DiyComponent, +} from '#/components/diy-editor/util'; + +/** 广告魔方属性 */ +export interface MagicCubeProperty { + // 上圆角 + borderRadiusTop: number; + // 下圆角 + borderRadiusBottom: number; + // 间隔 + space: number; + // 导航菜单列表 + list: MagicCubeItemProperty[]; + // 组件样式 + style: ComponentStyle; +} + +/** 广告魔方项目属性 */ +export interface MagicCubeItemProperty { + // 图标链接 + imgUrl: string; + // 链接 + url: string; + // 宽 + width: number; + // 高 + height: number; + // 上 + top: number; + // 左 + left: number; + // 右 + right: number; + // 下 + bottom: number; +} + +// 定义组件 +export const component = { + id: 'MagicCube', + name: '广告魔方', + icon: 'bi:columns', + property: { + borderRadiusTop: 0, + borderRadiusBottom: 0, + space: 0, + list: [], + style: { + bgType: 'color', + bgColor: '#fff', + marginBottom: 8, + } as ComponentStyle, + }, +} as DiyComponent; diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/magic-cube/index.vue b/apps/web-ele/src/components/diy-editor/components/mobile/magic-cube/index.vue new file mode 100644 index 000000000..5a3aa7e57 --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/magic-cube/index.vue @@ -0,0 +1,84 @@ + + + + + diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/magic-cube/property.vue b/apps/web-ele/src/components/diy-editor/components/mobile/magic-cube/property.vue new file mode 100644 index 000000000..80efbae66 --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/magic-cube/property.vue @@ -0,0 +1,90 @@ + + + + + diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/menu-grid/config.ts b/apps/web-ele/src/components/diy-editor/components/mobile/menu-grid/config.ts new file mode 100644 index 000000000..4ae675a49 --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/menu-grid/config.ts @@ -0,0 +1,83 @@ +import type { + ComponentStyle, + DiyComponent, +} from '#/components/diy-editor/util'; + +import { cloneDeep } from '@vben/utils'; + +/** 宫格导航属性 */ +export interface MenuGridProperty { + // 列数 + column: number; + // 导航菜单列表 + list: MenuGridItemProperty[]; + // 组件样式 + style: ComponentStyle; +} + +/** 宫格导航项目属性 */ +export interface MenuGridItemProperty { + // 图标链接 + iconUrl: string; + // 标题 + title: string; + // 标题颜色 + titleColor: string; + // 副标题 + subtitle: string; + // 副标题颜色 + subtitleColor: string; + // 链接 + url: string; + // 角标 + badge: { + // 角标背景颜色 + bgColor: string; + // 是否显示 + show: boolean; + // 角标文字 + text: string; + // 角标文字颜色 + textColor: string; + }; +} + +export const EMPTY_MENU_GRID_ITEM_PROPERTY = { + title: '标题', + titleColor: '#333', + subtitle: '副标题', + subtitleColor: '#bbb', + badge: { + show: false, + textColor: '#fff', + bgColor: '#FF6000', + }, +} as MenuGridItemProperty; + +// 定义组件 +export const component = { + id: 'MenuGrid', + name: '宫格导航', + icon: 'bi:grid-3x3-gap', + property: { + column: 3, + list: [cloneDeep(EMPTY_MENU_GRID_ITEM_PROPERTY)], + style: { + bgType: 'color', + bgColor: '#fff', + marginBottom: 8, + marginLeft: 8, + marginRight: 8, + padding: 8, + paddingTop: 8, + paddingRight: 8, + paddingBottom: 8, + paddingLeft: 8, + borderRadius: 8, + borderTopLeftRadius: 8, + borderTopRightRadius: 8, + borderBottomRightRadius: 8, + borderBottomLeftRadius: 8, + } as ComponentStyle, + }, +} as DiyComponent; diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/menu-grid/index.vue b/apps/web-ele/src/components/diy-editor/components/mobile/menu-grid/index.vue new file mode 100644 index 000000000..2036ede96 --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/menu-grid/index.vue @@ -0,0 +1,47 @@ + + + + + diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/menu-grid/property.vue b/apps/web-ele/src/components/diy-editor/components/mobile/menu-grid/property.vue new file mode 100644 index 000000000..88039d352 --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/menu-grid/property.vue @@ -0,0 +1,92 @@ + + + + + diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/menu-list/config.ts b/apps/web-ele/src/components/diy-editor/components/mobile/menu-list/config.ts new file mode 100644 index 000000000..4be256749 --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/menu-list/config.ts @@ -0,0 +1,52 @@ +import type { + ComponentStyle, + DiyComponent, +} from '#/components/diy-editor/util'; + +import { cloneDeep } from '@vben/utils'; + +/** 列表导航属性 */ +export interface MenuListProperty { + // 导航菜单列表 + list: MenuListItemProperty[]; + // 组件样式 + style: ComponentStyle; +} + +/** 列表导航项目属性 */ +export interface MenuListItemProperty { + // 图标链接 + iconUrl: string; + // 标题 + title: string; + // 标题颜色 + titleColor: string; + // 副标题 + subtitle: string; + // 副标题颜色 + subtitleColor: string; + // 链接 + url: string; +} + +export const EMPTY_MENU_LIST_ITEM_PROPERTY = { + title: '标题', + titleColor: '#333', + subtitle: '副标题', + subtitleColor: '#bbb', +}; + +// 定义组件 +export const component = { + id: 'MenuList', + name: '列表导航', + icon: 'fa-solid:list', + property: { + list: [cloneDeep(EMPTY_MENU_LIST_ITEM_PROPERTY)], + style: { + bgType: 'color', + bgColor: '#fff', + marginBottom: 8, + } as ComponentStyle, + }, +} as DiyComponent; diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/menu-list/index.vue b/apps/web-ele/src/components/diy-editor/components/mobile/menu-list/index.vue new file mode 100644 index 000000000..12670dcb0 --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/menu-list/index.vue @@ -0,0 +1,40 @@ + + + + + diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/menu-list/property.vue b/apps/web-ele/src/components/diy-editor/components/mobile/menu-list/property.vue new file mode 100644 index 000000000..a98edc254 --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/menu-list/property.vue @@ -0,0 +1,66 @@ + + + + + diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/menu-swiper/config.ts b/apps/web-ele/src/components/diy-editor/components/mobile/menu-swiper/config.ts new file mode 100644 index 000000000..535287d40 --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/menu-swiper/config.ts @@ -0,0 +1,70 @@ +import type { + ComponentStyle, + DiyComponent, +} from '#/components/diy-editor/util'; + +import { cloneDeep } from '@vben/utils'; + +/** 菜单导航属性 */ +export interface MenuSwiperProperty { + // 布局: 图标+文字 | 图标 + layout: 'icon' | 'iconText'; + // 行数 + row: number; + // 列数 + column: number; + // 导航菜单列表 + list: MenuSwiperItemProperty[]; + // 组件样式 + style: ComponentStyle; +} +/** 菜单导航项目属性 */ +export interface MenuSwiperItemProperty { + // 图标链接 + iconUrl: string; + // 标题 + title: string; + // 标题颜色 + titleColor: string; + // 链接 + url: string; + // 角标 + badge: { + // 角标背景颜色 + bgColor: string; + // 是否显示 + show: boolean; + // 角标文字 + text: string; + // 角标文字颜色 + textColor: string; + }; +} + +export const EMPTY_MENU_SWIPER_ITEM_PROPERTY = { + title: '标题', + titleColor: '#333', + badge: { + show: false, + textColor: '#fff', + bgColor: '#FF6000', + }, +} as MenuSwiperItemProperty; + +// 定义组件 +export const component = { + id: 'MenuSwiper', + name: '菜单导航', + icon: 'bi:grid-3x2-gap', + property: { + layout: 'iconText', + row: 1, + column: 3, + list: [cloneDeep(EMPTY_MENU_SWIPER_ITEM_PROPERTY)], + style: { + bgType: 'color', + bgColor: '#fff', + marginBottom: 8, + } as ComponentStyle, + }, +} as DiyComponent; diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/menu-swiper/index.vue b/apps/web-ele/src/components/diy-editor/components/mobile/menu-swiper/index.vue new file mode 100644 index 000000000..0d69c7c86 --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/menu-swiper/index.vue @@ -0,0 +1,137 @@ + + + + + diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/menu-swiper/property.vue b/apps/web-ele/src/components/diy-editor/components/mobile/menu-swiper/property.vue new file mode 100644 index 000000000..62dd6e92d --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/menu-swiper/property.vue @@ -0,0 +1,103 @@ + + + + + diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/navigation-bar/components/cell-property.vue b/apps/web-ele/src/components/diy-editor/components/mobile/navigation-bar/components/cell-property.vue new file mode 100644 index 000000000..b34e00917 --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/navigation-bar/components/cell-property.vue @@ -0,0 +1,143 @@ + + + + + diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/navigation-bar/config.ts b/apps/web-ele/src/components/diy-editor/components/mobile/navigation-bar/config.ts new file mode 100644 index 000000000..38325d5a9 --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/navigation-bar/config.ts @@ -0,0 +1,82 @@ +import type { DiyComponent } from '#/components/diy-editor/util'; + +/** 顶部导航栏属性 */ +export interface NavigationBarProperty { + // 背景类型 + bgType: 'color' | 'img'; + // 背景颜色 + bgColor: string; + // 图片链接 + bgImg: string; + // 样式类型:默认 | 沉浸式 + styleType: 'inner' | 'normal'; + // 常驻显示 + alwaysShow: boolean; + // 小程序单元格列表 + mpCells: NavigationBarCellProperty[]; + // 其它平台单元格列表 + otherCells: NavigationBarCellProperty[]; + // 本地变量 + _local: { + // 预览顶部导航(小程序) + previewMp: boolean; + // 预览顶部导航(非小程序) + previewOther: boolean; + }; +} + +/** 顶部导航栏 - 单元格 属性 */ +export interface NavigationBarCellProperty { + // 类型:文字 | 图片 | 搜索框 + type: 'image' | 'search' | 'text'; + // 宽度 + width: number; + // 高度 + height: number; + // 顶部位置 + top: number; + // 左侧位置 + left: number; + // 文字内容 + text: string; + // 文字颜色 + textColor: string; + // 图片地址 + imgUrl: string; + // 图片链接 + url: string; + // 搜索框:提示文字 + placeholder: string; + // 搜索框:边框圆角半径 + borderRadius: number; +} + +// 定义组件 +export const component = { + id: 'NavigationBar', + name: '顶部导航栏', + icon: 'tabler:layout-navbar', + property: { + bgType: 'color', + bgColor: '#fff', + bgImg: '', + styleType: 'normal', + alwaysShow: true, + mpCells: [ + { + type: 'text', + textColor: '#111111', + }, + ], + otherCells: [ + { + type: 'text', + textColor: '#111111', + }, + ], + _local: { + previewMp: true, + previewOther: false, + }, + }, +} as DiyComponent; diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/navigation-bar/index.vue b/apps/web-ele/src/components/diy-editor/components/mobile/navigation-bar/index.vue new file mode 100644 index 000000000..4a32acb80 --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/navigation-bar/index.vue @@ -0,0 +1,112 @@ + + + diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/navigation-bar/property.vue b/apps/web-ele/src/components/diy-editor/components/mobile/navigation-bar/property.vue new file mode 100644 index 000000000..ce3e3f698 --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/navigation-bar/property.vue @@ -0,0 +1,117 @@ + + + + + diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/notice-bar/config.ts b/apps/web-ele/src/components/diy-editor/components/mobile/notice-bar/config.ts new file mode 100644 index 000000000..31dc13794 --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/notice-bar/config.ts @@ -0,0 +1,49 @@ +import type { + ComponentStyle, + DiyComponent, +} from '#/components/diy-editor/util'; + +/** 公告栏属性 */ +export interface NoticeBarProperty { + // 图标地址 + iconUrl: string; + // 公告内容列表 + contents: NoticeContentProperty[]; + // 背景颜色 + backgroundColor: string; + // 文字颜色 + textColor: string; + // 组件样式 + style: ComponentStyle; +} + +/** 内容属性 */ +export interface NoticeContentProperty { + // 内容文字 + text: string; + // 链接地址 + url: string; +} + +// 定义组件 +export const component = { + id: 'NoticeBar', + name: '公告栏', + icon: 'ep:bell', + property: { + iconUrl: 'http://mall.yudao.iocoder.cn/static/images/xinjian.png', + contents: [ + { + text: '', + url: '', + }, + ], + backgroundColor: '#fff', + textColor: '#333', + style: { + bgType: 'color', + bgColor: '#fff', + marginBottom: 8, + } as ComponentStyle, + }, +} as DiyComponent; diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/notice-bar/index.vue b/apps/web-ele/src/components/diy-editor/components/mobile/notice-bar/index.vue new file mode 100644 index 000000000..589e89373 --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/notice-bar/index.vue @@ -0,0 +1,38 @@ + + + + + diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/notice-bar/property.vue b/apps/web-ele/src/components/diy-editor/components/mobile/notice-bar/property.vue new file mode 100644 index 000000000..935e5ec4d --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/notice-bar/property.vue @@ -0,0 +1,61 @@ + + + + + diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/page-config/config.ts b/apps/web-ele/src/components/diy-editor/components/mobile/page-config/config.ts new file mode 100644 index 000000000..29855bb73 --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/page-config/config.ts @@ -0,0 +1,23 @@ +import type { DiyComponent } from '#/components/diy-editor/util'; + +/** 页面设置属性 */ +export interface PageConfigProperty { + // 页面描述 + description: string; + // 页面背景颜色 + backgroundColor: string; + // 页面背景图片 + backgroundImage: string; +} + +// 定义页面组件 +export const component = { + id: 'PageConfig', + name: '页面设置', + icon: 'ep:document', + property: { + description: '', + backgroundColor: '#f5f5f5', + backgroundImage: '', + }, +} as DiyComponent; diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/page-config/property.vue b/apps/web-ele/src/components/diy-editor/components/mobile/page-config/property.vue new file mode 100644 index 000000000..4dca2797e --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/page-config/property.vue @@ -0,0 +1,46 @@ + + + + + diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/product-card/config.ts b/apps/web-ele/src/components/diy-editor/components/mobile/product-card/config.ts new file mode 100644 index 000000000..ea3c9e4da --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/product-card/config.ts @@ -0,0 +1,100 @@ +import type { + ComponentStyle, + DiyComponent, +} from '#/components/diy-editor/util'; + +/** 商品卡片属性 */ +export interface ProductCardProperty { + // 布局类型:单列大图 | 单列小图 | 双列 + layoutType: 'oneColBigImg' | 'oneColSmallImg' | 'twoCol'; + // 商品字段 + fields: { + // 商品简介 + introduction: ProductCardFieldProperty; + // 商品市场价 + marketPrice: ProductCardFieldProperty; + // 商品名称 + name: ProductCardFieldProperty; + // 商品价格 + price: ProductCardFieldProperty; + // 商品销量 + salesCount: ProductCardFieldProperty; + // 商品库存 + stock: ProductCardFieldProperty; + }; + // 角标 + badge: { + // 角标图片 + imgUrl: string; + // 是否显示 + show: boolean; + }; + // 按钮 + btnBuy: { + // 文字按钮:背景渐变起始颜色 + bgBeginColor: string; + // 文字按钮:背景渐变结束颜色 + bgEndColor: string; + // 图片按钮:图片地址 + imgUrl: string; + // 文字 + text: string; + // 类型:文字 | 图片 + type: 'img' | 'text'; + }; + // 上圆角 + borderRadiusTop: number; + // 下圆角 + borderRadiusBottom: number; + // 间距 + space: number; + // 商品编号列表 + spuIds: number[]; + // 组件样式 + style: ComponentStyle; +} +// 商品字段 +export interface ProductCardFieldProperty { + // 是否显示 + show: boolean; + // 颜色 + color: string; +} + +// 定义组件 +export const component = { + id: 'ProductCard', + name: '商品卡片', + icon: 'fluent:text-column-two-left-24-filled', + property: { + layoutType: 'oneColBigImg', + fields: { + name: { show: true, color: '#000' }, + introduction: { show: true, color: '#999' }, + price: { show: true, color: '#ff3000' }, + marketPrice: { show: true, color: '#c4c4c4' }, + salesCount: { show: true, color: '#c4c4c4' }, + stock: { show: false, color: '#c4c4c4' }, + }, + badge: { show: false, imgUrl: '' }, + btnBuy: { + type: 'text', + text: '立即购买', + // todo: @owen 根据主题色配置 + bgBeginColor: '#FF6000', + bgEndColor: '#FE832A', + imgUrl: '', + }, + borderRadiusTop: 6, + borderRadiusBottom: 6, + space: 8, + spuIds: [], + style: { + bgType: 'color', + bgColor: '', + marginLeft: 8, + marginRight: 8, + marginBottom: 8, + } as ComponentStyle, + }, +} as DiyComponent; diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/product-card/index.vue b/apps/web-ele/src/components/diy-editor/components/mobile/product-card/index.vue new file mode 100644 index 000000000..b1cf736e8 --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/product-card/index.vue @@ -0,0 +1,190 @@ + + + + diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/product-card/property.vue b/apps/web-ele/src/components/diy-editor/components/mobile/product-card/property.vue new file mode 100644 index 000000000..e0b3dfb77 --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/product-card/property.vue @@ -0,0 +1,177 @@ + + + + + diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/product-list/config.ts b/apps/web-ele/src/components/diy-editor/components/mobile/product-list/config.ts new file mode 100644 index 000000000..0f68a515f --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/product-list/config.ts @@ -0,0 +1,67 @@ +import type { + ComponentStyle, + DiyComponent, +} from '#/components/diy-editor/util'; + +/** 商品栏属性 */ +export interface ProductListProperty { + // 布局类型:双列 | 三列 | 水平滑动 + layoutType: 'horizSwiper' | 'threeCol' | 'twoCol'; + // 商品字段 + fields: { + // 商品名称 + name: ProductListFieldProperty; + // 商品价格 + price: ProductListFieldProperty; + }; + // 角标 + badge: { + // 角标图片 + imgUrl: string; + // 是否显示 + show: boolean; + }; + // 上圆角 + borderRadiusTop: number; + // 下圆角 + borderRadiusBottom: number; + // 间距 + space: number; + // 商品编号列表 + spuIds: number[]; + // 组件样式 + style: ComponentStyle; +} +// 商品字段 +export interface ProductListFieldProperty { + // 是否显示 + show: boolean; + // 颜色 + color: string; +} + +// 定义组件 +export const component = { + id: 'ProductList', + name: '商品栏', + icon: 'fluent:text-column-two-24-filled', + property: { + layoutType: 'twoCol', + fields: { + name: { show: true, color: '#000' }, + price: { show: true, color: '#ff3000' }, + }, + badge: { show: false, imgUrl: '' }, + borderRadiusTop: 8, + borderRadiusBottom: 8, + space: 8, + spuIds: [], + style: { + bgType: 'color', + bgColor: '', + marginLeft: 8, + marginRight: 8, + marginBottom: 8, + } as ComponentStyle, + }, +} as DiyComponent; diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/product-list/index.vue b/apps/web-ele/src/components/diy-editor/components/mobile/product-list/index.vue new file mode 100644 index 000000000..0e7ec6e1b --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/product-list/index.vue @@ -0,0 +1,150 @@ + + + + diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/product-list/property.vue b/apps/web-ele/src/components/diy-editor/components/mobile/product-list/property.vue new file mode 100644 index 000000000..f4e6d9686 --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/product-list/property.vue @@ -0,0 +1,119 @@ + + + + + diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/promotion-article/config.ts b/apps/web-ele/src/components/diy-editor/components/mobile/promotion-article/config.ts new file mode 100644 index 000000000..9ad0304bc --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/promotion-article/config.ts @@ -0,0 +1,28 @@ +import type { + ComponentStyle, + DiyComponent, +} from '#/components/diy-editor/util'; + +/** 营销文章属性 */ +export interface PromotionArticleProperty { + // 文章编号 + id: number; + // 组件样式 + style: ComponentStyle; +} + +// 定义组件 +export const component = { + id: 'PromotionArticle', + name: '营销文章', + icon: 'ph:article-medium', + property: { + style: { + bgType: 'color', + bgColor: '', + marginLeft: 8, + marginRight: 8, + marginBottom: 8, + } as ComponentStyle, + }, +} as DiyComponent; diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/promotion-article/index.vue b/apps/web-ele/src/components/diy-editor/components/mobile/promotion-article/index.vue new file mode 100644 index 000000000..10e9f5aa3 --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/promotion-article/index.vue @@ -0,0 +1,33 @@ + + + + diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/promotion-article/property.vue b/apps/web-ele/src/components/diy-editor/components/mobile/promotion-article/property.vue new file mode 100644 index 000000000..82d3f35b4 --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/promotion-article/property.vue @@ -0,0 +1,68 @@ + + + + + diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/promotion-combination/config.ts b/apps/web-ele/src/components/diy-editor/components/mobile/promotion-combination/config.ts new file mode 100644 index 000000000..fba3a4a85 --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/promotion-combination/config.ts @@ -0,0 +1,99 @@ +import type { + ComponentStyle, + DiyComponent, +} from '#/components/diy-editor/util'; + +/** 拼团属性 */ +export interface PromotionCombinationProperty { + // 布局类型:单列 | 三列 + layoutType: 'oneColBigImg' | 'oneColSmallImg' | 'twoCol'; + // 商品字段 + fields: { + // 商品简介 + introduction: PromotionCombinationFieldProperty; + // 市场价 + marketPrice: PromotionCombinationFieldProperty; + // 商品名称 + name: PromotionCombinationFieldProperty; + // 商品价格 + price: PromotionCombinationFieldProperty; + // 商品销量 + salesCount: PromotionCombinationFieldProperty; + // 商品库存 + stock: PromotionCombinationFieldProperty; + }; + // 角标 + badge: { + // 角标图片 + imgUrl: string; + // 是否显示 + show: boolean; + }; + // 按钮 + btnBuy: { + // 文字按钮:背景渐变起始颜色 + bgBeginColor: string; + // 文字按钮:背景渐变结束颜色 + bgEndColor: string; + // 图片按钮:图片地址 + imgUrl: string; + // 文字 + text: string; + // 类型:文字 | 图片 + type: 'img' | 'text'; + }; + // 上圆角 + borderRadiusTop: number; + // 下圆角 + borderRadiusBottom: number; + // 间距 + space: number; + // 拼团活动编号 + activityIds: number[]; + // 组件样式 + style: ComponentStyle; +} + +// 商品字段 +export interface PromotionCombinationFieldProperty { + // 是否显示 + show: boolean; + // 颜色 + color: string; +} + +// 定义组件 +export const component = { + id: 'PromotionCombination', + name: '拼团', + icon: 'mdi:account-group', + property: { + layoutType: 'oneColBigImg', + fields: { + name: { show: true, color: '#000' }, + introduction: { show: true, color: '#999' }, + price: { show: true, color: '#ff3000' }, + marketPrice: { show: true, color: '#c4c4c4' }, + salesCount: { show: true, color: '#c4c4c4' }, + stock: { show: false, color: '#c4c4c4' }, + }, + badge: { show: false, imgUrl: '' }, + btnBuy: { + type: 'text', + text: '去拼团', + bgBeginColor: '#FF6000', + bgEndColor: '#FE832A', + imgUrl: '', + }, + borderRadiusTop: 8, + borderRadiusBottom: 8, + space: 8, + style: { + bgType: 'color', + bgColor: '', + marginLeft: 8, + marginRight: 8, + marginBottom: 8, + } as ComponentStyle, + }, +} as DiyComponent; diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/promotion-combination/index.vue b/apps/web-ele/src/components/diy-editor/components/mobile/promotion-combination/index.vue new file mode 100644 index 000000000..82f1f5bd7 --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/promotion-combination/index.vue @@ -0,0 +1,233 @@ + + + + diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/promotion-combination/property.vue b/apps/web-ele/src/components/diy-editor/components/mobile/promotion-combination/property.vue new file mode 100644 index 000000000..da57011de --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/promotion-combination/property.vue @@ -0,0 +1,190 @@ + + + + + diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/promotion-point/config.ts b/apps/web-ele/src/components/diy-editor/components/mobile/promotion-point/config.ts new file mode 100644 index 000000000..4484f3125 --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/promotion-point/config.ts @@ -0,0 +1,99 @@ +import type { + ComponentStyle, + DiyComponent, +} from '#/components/diy-editor/util'; + +/** 积分商城属性 */ +export interface PromotionPointProperty { + // 布局类型:单列 | 三列 + layoutType: 'oneColBigImg' | 'oneColSmallImg' | 'twoCol'; + // 商品字段 + fields: { + // 商品简介 + introduction: PromotionPointFieldProperty; + // 市场价 + marketPrice: PromotionPointFieldProperty; + // 商品名称 + name: PromotionPointFieldProperty; + // 商品价格 + price: PromotionPointFieldProperty; + // 商品销量 + salesCount: PromotionPointFieldProperty; + // 商品库存 + stock: PromotionPointFieldProperty; + }; + // 角标 + badge: { + // 角标图片 + imgUrl: string; + // 是否显示 + show: boolean; + }; + // 按钮 + btnBuy: { + // 文字按钮:背景渐变起始颜色 + bgBeginColor: string; + // 文字按钮:背景渐变结束颜色 + bgEndColor: string; + // 图片按钮:图片地址 + imgUrl: string; + // 文字 + text: string; + // 类型:文字 | 图片 + type: 'img' | 'text'; + }; + // 上圆角 + borderRadiusTop: number; + // 下圆角 + borderRadiusBottom: number; + // 间距 + space: number; + // 秒杀活动编号 + activityIds: number[]; + // 组件样式 + style: ComponentStyle; +} + +// 商品字段 +export interface PromotionPointFieldProperty { + // 是否显示 + show: boolean; + // 颜色 + color: string; +} + +// 定义组件 +export const component = { + id: 'PromotionPoint', + name: '积分商城', + icon: 'ep:present', + property: { + layoutType: 'oneColBigImg', + fields: { + name: { show: true, color: '#000' }, + introduction: { show: true, color: '#999' }, + price: { show: true, color: '#ff3000' }, + marketPrice: { show: true, color: '#c4c4c4' }, + salesCount: { show: true, color: '#c4c4c4' }, + stock: { show: false, color: '#c4c4c4' }, + }, + badge: { show: false, imgUrl: '' }, + btnBuy: { + type: 'text', + text: '立即兑换', + bgBeginColor: '#FF6000', + bgEndColor: '#FE832A', + imgUrl: '', + }, + borderRadiusTop: 8, + borderRadiusBottom: 8, + space: 8, + style: { + bgType: 'color', + bgColor: '', + marginLeft: 8, + marginRight: 8, + marginBottom: 8, + } as ComponentStyle, + }, +} as DiyComponent; diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/promotion-point/index.vue b/apps/web-ele/src/components/diy-editor/components/mobile/promotion-point/index.vue new file mode 100644 index 000000000..3ff8336dd --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/promotion-point/index.vue @@ -0,0 +1,234 @@ + + + + diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/promotion-point/property.vue b/apps/web-ele/src/components/diy-editor/components/mobile/promotion-point/property.vue new file mode 100644 index 000000000..c028feacd --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/promotion-point/property.vue @@ -0,0 +1,180 @@ + + + + + diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/promotion-seckill/config.ts b/apps/web-ele/src/components/diy-editor/components/mobile/promotion-seckill/config.ts new file mode 100644 index 000000000..fcba6c7e0 --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/promotion-seckill/config.ts @@ -0,0 +1,99 @@ +import type { + ComponentStyle, + DiyComponent, +} from '#/components/diy-editor/util'; + +/** 秒杀属性 */ +export interface PromotionSeckillProperty { + // 布局类型:单列 | 三列 + layoutType: 'oneColBigImg' | 'oneColSmallImg' | 'twoCol'; + // 商品字段 + fields: { + // 商品简介 + introduction: PromotionSeckillFieldProperty; + // 市场价 + marketPrice: PromotionSeckillFieldProperty; + // 商品名称 + name: PromotionSeckillFieldProperty; + // 商品价格 + price: PromotionSeckillFieldProperty; + // 商品销量 + salesCount: PromotionSeckillFieldProperty; + // 商品库存 + stock: PromotionSeckillFieldProperty; + }; + // 角标 + badge: { + // 角标图片 + imgUrl: string; + // 是否显示 + show: boolean; + }; + // 按钮 + btnBuy: { + // 文字按钮:背景渐变起始颜色 + bgBeginColor: string; + // 文字按钮:背景渐变结束颜色 + bgEndColor: string; + // 图片按钮:图片地址 + imgUrl: string; + // 文字 + text: string; + // 类型:文字 | 图片 + type: 'img' | 'text'; + }; + // 上圆角 + borderRadiusTop: number; + // 下圆角 + borderRadiusBottom: number; + // 间距 + space: number; + // 秒杀活动编号 + activityIds: number[]; + // 组件样式 + style: ComponentStyle; +} + +// 商品字段 +export interface PromotionSeckillFieldProperty { + // 是否显示 + show: boolean; + // 颜色 + color: string; +} + +// 定义组件 +export const component = { + id: 'PromotionSeckill', + name: '秒杀', + icon: 'mdi:calendar-time', + property: { + layoutType: 'oneColBigImg', + fields: { + name: { show: true, color: '#000' }, + introduction: { show: true, color: '#999' }, + price: { show: true, color: '#ff3000' }, + marketPrice: { show: true, color: '#c4c4c4' }, + salesCount: { show: true, color: '#c4c4c4' }, + stock: { show: false, color: '#c4c4c4' }, + }, + badge: { show: false, imgUrl: '' }, + btnBuy: { + type: 'text', + text: '立即秒杀', + bgBeginColor: '#FF6000', + bgEndColor: '#FE832A', + imgUrl: '', + }, + borderRadiusTop: 8, + borderRadiusBottom: 8, + space: 8, + style: { + bgType: 'color', + bgColor: '', + marginLeft: 8, + marginRight: 8, + marginBottom: 8, + } as ComponentStyle, + }, +} as DiyComponent; diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/promotion-seckill/index.vue b/apps/web-ele/src/components/diy-editor/components/mobile/promotion-seckill/index.vue new file mode 100644 index 000000000..b497437b0 --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/promotion-seckill/index.vue @@ -0,0 +1,229 @@ + + + + diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/promotion-seckill/property.vue b/apps/web-ele/src/components/diy-editor/components/mobile/promotion-seckill/property.vue new file mode 100644 index 000000000..3dd246f02 --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/promotion-seckill/property.vue @@ -0,0 +1,197 @@ + + + + + diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/search-bar/config.ts b/apps/web-ele/src/components/diy-editor/components/mobile/search-bar/config.ts new file mode 100644 index 000000000..64ab098a4 --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/search-bar/config.ts @@ -0,0 +1,46 @@ +import type { + ComponentStyle, + DiyComponent, +} from '#/components/diy-editor/util'; + +/** 搜索框属性 */ +export interface SearchProperty { + height: number; // 搜索栏高度 + showScan: boolean; // 显示扫一扫 + borderRadius: number; // 框体样式 + placeholder: string; // 占位文字 + placeholderPosition: PlaceholderPosition; // 占位文字位置 + backgroundColor: string; // 框体颜色 + textColor: string; // 字体颜色 + hotKeywords: string[]; // 热词 + style: ComponentStyle; +} + +// 文字位置 +export type PlaceholderPosition = 'center' | 'left'; + +// 定义组件 +export const component = { + id: 'SearchBar', + name: '搜索框', + icon: 'ep:search', + property: { + height: 28, + showScan: false, + borderRadius: 0, + placeholder: '搜索商品', + placeholderPosition: 'left', + backgroundColor: 'rgb(238, 238, 238)', + textColor: 'rgb(150, 151, 153)', + hotKeywords: [], + style: { + bgType: 'color', + bgColor: '#fff', + marginBottom: 8, + paddingTop: 8, + paddingRight: 8, + paddingBottom: 8, + paddingLeft: 8, + } as ComponentStyle, + }, +} as DiyComponent; diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/search-bar/index.vue b/apps/web-ele/src/components/diy-editor/components/mobile/search-bar/index.vue new file mode 100644 index 000000000..e7c770304 --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/search-bar/index.vue @@ -0,0 +1,83 @@ + + + + + diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/search-bar/property.vue b/apps/web-ele/src/components/diy-editor/components/mobile/search-bar/property.vue new file mode 100644 index 000000000..149f52fce --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/search-bar/property.vue @@ -0,0 +1,120 @@ + + + + + diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/tab-bar/config.ts b/apps/web-ele/src/components/diy-editor/components/mobile/tab-bar/config.ts new file mode 100644 index 000000000..bc9488e1c --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/tab-bar/config.ts @@ -0,0 +1,172 @@ +import type { DiyComponent } from '#/components/diy-editor/util'; + +/** 底部导航菜单属性 */ +export interface TabBarProperty { + // 选项列表 + items: TabBarItemProperty[]; + // 主题 + theme: string; + // 样式 + style: TabBarStyle; +} + +// 选项属性 +export interface TabBarItemProperty { + // 标签文字 + text: string; + // 链接 + url: string; + // 默认图标链接 + iconUrl: string; + // 选中的图标链接 + activeIconUrl: string; +} + +// 样式 +export interface TabBarStyle { + // 背景类型 + bgType: 'color' | 'img'; + // 背景颜色 + bgColor: string; + // 图片链接 + bgImg: string; + // 默认颜色 + color: string; + // 选中的颜色 + activeColor: string; +} + +// 定义组件 +export const component = { + id: 'TabBar', + name: '底部导航', + icon: 'fluent:table-bottom-row-16-filled', + property: { + theme: 'red', + style: { + bgType: 'color', + bgColor: '#fff', + color: '#282828', + activeColor: '#fc4141', + }, + items: [ + { + text: '首页', + url: '/pages/index/index', + iconUrl: 'http://mall.yudao.iocoder.cn/static/images/1-001.png', + activeIconUrl: 'http://mall.yudao.iocoder.cn/static/images/1-002.png', + }, + { + text: '分类', + url: '/pages/index/category?id=3', + iconUrl: 'http://mall.yudao.iocoder.cn/static/images/2-001.png', + activeIconUrl: 'http://mall.yudao.iocoder.cn/static/images/2-002.png', + }, + { + text: '购物车', + url: '/pages/index/cart', + iconUrl: 'http://mall.yudao.iocoder.cn/static/images/3-001.png', + activeIconUrl: 'http://mall.yudao.iocoder.cn/static/images/3-002.png', + }, + { + text: '我的', + url: '/pages/index/user', + iconUrl: 'http://mall.yudao.iocoder.cn/static/images/4-001.png', + activeIconUrl: 'http://mall.yudao.iocoder.cn/static/images/4-002.png', + }, + ], + }, +} as DiyComponent; + +export const THEME_LIST = [ + { + id: 'red', + name: '中国红', + icon: 'icon-park-twotone:theme', + color: '#d10019', + }, + { + id: 'orange', + name: '桔橙', + icon: 'icon-park-twotone:theme', + color: '#f37b1d', + }, + { + id: 'gold', + name: '明黄', + icon: 'icon-park-twotone:theme', + color: '#fbbd08', + }, + { + id: 'green', + name: '橄榄绿', + icon: 'icon-park-twotone:theme', + color: '#8dc63f', + }, + { + id: 'cyan', + name: '天青', + icon: 'icon-park-twotone:theme', + color: '#1cbbb4', + }, + { + id: 'blue', + name: '海蓝', + icon: 'icon-park-twotone:theme', + color: '#0081ff', + }, + { + id: 'purple', + name: '姹紫', + icon: 'icon-park-twotone:theme', + color: '#6739b6', + }, + { + id: 'brightRed', + name: '嫣红', + icon: 'icon-park-twotone:theme', + color: '#e54d42', + }, + { + id: 'forestGreen', + name: '森绿', + icon: 'icon-park-twotone:theme', + color: '#39b54a', + }, + { + id: 'mauve', + name: '木槿', + icon: 'icon-park-twotone:theme', + color: '#9c26b0', + }, + { + id: 'pink', + name: '桃粉', + icon: 'icon-park-twotone:theme', + color: '#e03997', + }, + { + id: 'brown', + name: '棕褐', + icon: 'icon-park-twotone:theme', + color: '#a5673f', + }, + { + id: 'grey', + name: '玄灰', + icon: 'icon-park-twotone:theme', + color: '#8799a3', + }, + { + id: 'gray', + name: '草灰', + icon: 'icon-park-twotone:theme', + color: '#aaaaaa', + }, + { + id: 'black', + name: '墨黑', + icon: 'icon-park-twotone:theme', + color: '#333333', + }, +]; diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/tab-bar/index.vue b/apps/web-ele/src/components/diy-editor/components/mobile/tab-bar/index.vue new file mode 100644 index 000000000..9c1ddd834 --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/tab-bar/index.vue @@ -0,0 +1,79 @@ + + + diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/tab-bar/property.vue b/apps/web-ele/src/components/diy-editor/components/mobile/tab-bar/property.vue new file mode 100644 index 000000000..fab41b648 --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/tab-bar/property.vue @@ -0,0 +1,131 @@ + + + + + diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/title-bar/config.ts b/apps/web-ele/src/components/diy-editor/components/mobile/title-bar/config.ts new file mode 100644 index 000000000..64d49ad6c --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/title-bar/config.ts @@ -0,0 +1,76 @@ +import type { + ComponentStyle, + DiyComponent, +} from '#/components/diy-editor/util'; + +/** 标题栏属性 */ +export interface TitleBarProperty { + // 背景图 + bgImgUrl: string; + // 偏移 + marginLeft: number; + // 显示位置 + textAlign: 'center' | 'left'; + // 主标题 + title: string; + // 副标题 + description: string; + // 标题大小 + titleSize: number; + // 描述大小 + descriptionSize: number; + // 标题粗细 + titleWeight: number; + // 描述粗细 + descriptionWeight: number; + // 标题颜色 + titleColor: string; + // 描述颜色 + descriptionColor: string; + // 高度 + height: number; + // 查看更多 + more: { + // 是否显示查看更多 + show: false; + // 自定义文字 + text: string; + // 样式选择 + type: 'all' | 'icon' | 'text'; + // 链接 + url: string; + }; + // 组件样式 + style: ComponentStyle; +} + +// 定义组件 +export const component = { + id: 'TitleBar', + name: '标题栏', + icon: 'material-symbols:line-start', + property: { + title: '主标题', + description: '副标题', + titleSize: 16, + descriptionSize: 12, + titleWeight: 400, + textAlign: 'left', + descriptionWeight: 200, + titleColor: 'rgba(50, 50, 51, 10)', + descriptionColor: 'rgba(150, 151, 153, 10)', + marginLeft: 0, + height: 40, + more: { + // 查看更多 + show: false, + type: 'icon', + text: '查看更多', + url: '', + }, + style: { + bgType: 'color', + bgColor: '#fff', + } as ComponentStyle, + }, +} as DiyComponent; diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/title-bar/index.vue b/apps/web-ele/src/components/diy-editor/components/mobile/title-bar/index.vue new file mode 100644 index 000000000..8ee389d88 --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/title-bar/index.vue @@ -0,0 +1,88 @@ + + + diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/title-bar/property.vue b/apps/web-ele/src/components/diy-editor/components/mobile/title-bar/property.vue new file mode 100644 index 000000000..45a4147c0 --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/title-bar/property.vue @@ -0,0 +1,168 @@ + + + + diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/user-card/config.ts b/apps/web-ele/src/components/diy-editor/components/mobile/user-card/config.ts new file mode 100644 index 000000000..e674a0a32 --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/user-card/config.ts @@ -0,0 +1,24 @@ +import type { + ComponentStyle, + DiyComponent, +} from '#/components/diy-editor/util'; + +/** 用户卡片属性 */ +export interface UserCardProperty { + // 组件样式 + style: ComponentStyle; +} + +// 定义组件 +export const component = { + id: 'UserCard', + name: '用户卡片', + icon: 'mdi:user-card-details', + property: { + style: { + bgType: 'color', + bgColor: '', + marginBottom: 8, + } as ComponentStyle, + }, +} as DiyComponent; diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/user-card/index.vue b/apps/web-ele/src/components/diy-editor/components/mobile/user-card/index.vue new file mode 100644 index 000000000..9cb312f37 --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/user-card/index.vue @@ -0,0 +1,35 @@ + + + + diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/user-card/property.vue b/apps/web-ele/src/components/diy-editor/components/mobile/user-card/property.vue new file mode 100644 index 000000000..38f5d2b5d --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/user-card/property.vue @@ -0,0 +1,20 @@ + + + + + diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/user-coupon/config.ts b/apps/web-ele/src/components/diy-editor/components/mobile/user-coupon/config.ts new file mode 100644 index 000000000..efcb7f2ef --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/user-coupon/config.ts @@ -0,0 +1,26 @@ +import type { + ComponentStyle, + DiyComponent, +} from '#/components/diy-editor/util'; + +/** 用户卡券属性 */ +export interface UserCouponProperty { + // 组件样式 + style: ComponentStyle; +} + +// 定义组件 +export const component = { + id: 'UserCoupon', + name: '用户卡券', + icon: 'ep:ticket', + property: { + style: { + bgType: 'color', + bgColor: '', + marginLeft: 8, + marginRight: 8, + marginBottom: 8, + } as ComponentStyle, + }, +} as DiyComponent; diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/user-coupon/index.vue b/apps/web-ele/src/components/diy-editor/components/mobile/user-coupon/index.vue new file mode 100644 index 000000000..2df2df328 --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/user-coupon/index.vue @@ -0,0 +1,17 @@ + + + + diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/user-coupon/property.vue b/apps/web-ele/src/components/diy-editor/components/mobile/user-coupon/property.vue new file mode 100644 index 000000000..c9fb1fbfb --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/user-coupon/property.vue @@ -0,0 +1,20 @@ + + + + + diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/user-order/config.ts b/apps/web-ele/src/components/diy-editor/components/mobile/user-order/config.ts new file mode 100644 index 000000000..0c2e825a3 --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/user-order/config.ts @@ -0,0 +1,26 @@ +import type { + ComponentStyle, + DiyComponent, +} from '#/components/diy-editor/util'; + +/** 用户订单属性 */ +export interface UserOrderProperty { + // 组件样式 + style: ComponentStyle; +} + +// 定义组件 +export const component = { + id: 'UserOrder', + name: '用户订单', + icon: 'ep:list', + property: { + style: { + bgType: 'color', + bgColor: '', + marginLeft: 8, + marginRight: 8, + marginBottom: 8, + } as ComponentStyle, + }, +} as DiyComponent; diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/user-order/index.vue b/apps/web-ele/src/components/diy-editor/components/mobile/user-order/index.vue new file mode 100644 index 000000000..ab527acee --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/user-order/index.vue @@ -0,0 +1,17 @@ + + + + diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/user-order/property.vue b/apps/web-ele/src/components/diy-editor/components/mobile/user-order/property.vue new file mode 100644 index 000000000..d403c325b --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/user-order/property.vue @@ -0,0 +1,20 @@ + + + + + diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/user-wallet/config.ts b/apps/web-ele/src/components/diy-editor/components/mobile/user-wallet/config.ts new file mode 100644 index 000000000..8393760b4 --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/user-wallet/config.ts @@ -0,0 +1,26 @@ +import type { + ComponentStyle, + DiyComponent, +} from '#/components/diy-editor/util'; + +/** 用户资产属性 */ +export interface UserWalletProperty { + // 组件样式 + style: ComponentStyle; +} + +// 定义组件 +export const component = { + id: 'UserWallet', + name: '用户资产', + icon: 'ep:wallet-filled', + property: { + style: { + bgType: 'color', + bgColor: '', + marginLeft: 8, + marginRight: 8, + marginBottom: 8, + } as ComponentStyle, + }, +} as DiyComponent; diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/user-wallet/index.vue b/apps/web-ele/src/components/diy-editor/components/mobile/user-wallet/index.vue new file mode 100644 index 000000000..7581e54c9 --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/user-wallet/index.vue @@ -0,0 +1,17 @@ + + + + diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/user-wallet/property.vue b/apps/web-ele/src/components/diy-editor/components/mobile/user-wallet/property.vue new file mode 100644 index 000000000..851e7f2f1 --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/user-wallet/property.vue @@ -0,0 +1,20 @@ + + + + + diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/video-player/config.ts b/apps/web-ele/src/components/diy-editor/components/mobile/video-player/config.ts new file mode 100644 index 000000000..dc2dd8866 --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/video-player/config.ts @@ -0,0 +1,40 @@ +import type { + ComponentStyle, + DiyComponent, +} from '#/components/diy-editor/util'; + +/** 视频播放属性 */ +export interface VideoPlayerProperty { + // 视频链接 + videoUrl: string; + // 封面链接 + posterUrl: string; + // 是否自动播放 + autoplay: boolean; + // 组件样式 + style: VideoPlayerStyle; +} + +// 视频播放样式 +export interface VideoPlayerStyle extends ComponentStyle { + // 视频高度 + height: number; +} + +// 定义组件 +export const component = { + id: 'VideoPlayer', + name: '视频播放', + icon: 'ep:video-play', + property: { + videoUrl: '', + posterUrl: '', + autoplay: false, + style: { + bgType: 'color', + bgColor: '#fff', + marginBottom: 8, + height: 300, + } as VideoPlayerStyle, + }, +} as DiyComponent; diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/video-player/index.vue b/apps/web-ele/src/components/diy-editor/components/mobile/video-player/index.vue new file mode 100644 index 000000000..ddd448d96 --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/video-player/index.vue @@ -0,0 +1,36 @@ + + + + diff --git a/apps/web-ele/src/components/diy-editor/components/mobile/video-player/property.vue b/apps/web-ele/src/components/diy-editor/components/mobile/video-player/property.vue new file mode 100644 index 000000000..d07835f25 --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/components/mobile/video-player/property.vue @@ -0,0 +1,62 @@ + + + + + diff --git a/apps/web-ele/src/components/diy-editor/index.vue b/apps/web-ele/src/components/diy-editor/index.vue new file mode 100644 index 000000000..7f7ce0e38 --- /dev/null +++ b/apps/web-ele/src/components/diy-editor/index.vue @@ -0,0 +1,659 @@ + +