generated from template/vite-react-template
	初始化应用
This commit is contained in:
		
							
								
								
									
										1
									
								
								.npmrc
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								.npmrc
									
									
									
									
									
								
							| @@ -1,3 +1,4 @@ | ||||
| //npm.xiongxiao.me/:_authToken=${ME_NPM_TOKEN} | ||||
| //registry.npmjs.org/:_authToken=${NPM_TOKEN} | ||||
| ignore-workspace-root-check=true | ||||
| auto-approve-builds = true | ||||
							
								
								
									
										31
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										31
									
								
								README.md
									
									
									
									
									
								
							| @@ -1 +1,30 @@ | ||||
| # vite-react-template | ||||
| ## env | ||||
|  | ||||
| 安装 node 环境 | ||||
|  | ||||
| 安装后安装一些基本运行库 | ||||
|  | ||||
| ```sh | ||||
| npm i -g bun pnpm | ||||
| ``` | ||||
|  | ||||
| ## build | ||||
|  | ||||
| 打包程序 | ||||
|  | ||||
| ```sh | ||||
| pnpm install | ||||
| pnpm build | ||||
| ``` | ||||
|  | ||||
| ## backend server | ||||
|  | ||||
| 启动服务 | ||||
|  | ||||
| ```sh | ||||
| pnpm server | ||||
| ``` | ||||
|  | ||||
| ## 访问地址 | ||||
|  | ||||
| `http://localhost:3004/root/tickets/` | ||||
|   | ||||
							
								
								
									
										13
									
								
								backend/.cnb.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								backend/.cnb.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| # .cnb.yml | ||||
| $: | ||||
|   vscode: | ||||
|     - docker: | ||||
|         image: docker.cnb.cool/kevisual/dev-env:latest | ||||
|       services: | ||||
|         - vscode | ||||
|         - docker | ||||
|       imports: https://cnb.cool/kevisual/env/-/blob/main/env.yml | ||||
|       # 开发环境启动后会执行的任务 | ||||
|       stages: | ||||
|         - name: pnpm install | ||||
|           script: pnpm install | ||||
							
								
								
									
										70
									
								
								backend/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								backend/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,70 @@ | ||||
| node_modules | ||||
|  | ||||
| # mac | ||||
| .DS_Store | ||||
|  | ||||
| .env* | ||||
| !.env*example | ||||
|  | ||||
| dist | ||||
| build | ||||
| logs | ||||
|  | ||||
| .turbo | ||||
|  | ||||
| pack-dist | ||||
|  | ||||
| # astro | ||||
| .astro | ||||
|  | ||||
| # next | ||||
| .next | ||||
|  | ||||
| # nuxt | ||||
| .nuxt | ||||
|  | ||||
| # vercel | ||||
| .vercel | ||||
|  | ||||
| # vuepress | ||||
| .vuepress/dist | ||||
|  | ||||
| # coverage | ||||
| coverage/ | ||||
|  | ||||
| # typescript | ||||
| *.tsbuildinfo | ||||
|  | ||||
| # debug logs | ||||
| *.log | ||||
| *.tmp | ||||
|  | ||||
| # vscode | ||||
| .vscode/* | ||||
| !.vscode/settings.json | ||||
| !.vscode/tasks.json | ||||
| !.vscode/launch.json | ||||
| !.vscode/extensions.json | ||||
|  | ||||
| # idea | ||||
| .idea | ||||
|  | ||||
| # system | ||||
| Thumbs.db | ||||
| ehthumbs.db | ||||
| Desktop.ini | ||||
|  | ||||
| # temp files | ||||
| *.tmp | ||||
| *.temp | ||||
|  | ||||
| # local development | ||||
| *.local | ||||
|  | ||||
| public/r | ||||
|  | ||||
| .pnpm-store | ||||
|  | ||||
| storage/ | ||||
|  | ||||
| pages | ||||
							
								
								
									
										3
									
								
								backend/.npmrc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								backend/.npmrc
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| //npm.xiongxiao.me/:_authToken=${ME_NPM_TOKEN} | ||||
| //registry.npmjs.org/:_authToken=${NPM_TOKEN} | ||||
| ignore-workspace-root-check=true | ||||
							
								
								
									
										25
									
								
								backend/bun.config.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								backend/bun.config.mjs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| // @ts-check | ||||
| import { resolvePath } from '@kevisual/use-config/env'; | ||||
| import { execSync } from 'node:child_process'; | ||||
|  | ||||
| const entry = 'src/index.ts'; | ||||
| const naming = 'app'; | ||||
| const external = ['sequelize', 'pg', 'sqlite3', 'minio', '@kevisual/router', 'pm2']; | ||||
| /** | ||||
|  * @type {import('bun').BuildConfig} | ||||
|  */ | ||||
| await Bun.build({ | ||||
|   target: 'node', | ||||
|   format: 'esm', | ||||
|   entrypoints: [resolvePath(entry, { meta: import.meta })], | ||||
|   outdir: resolvePath('./dist', { meta: import.meta }), | ||||
|   naming: { | ||||
|     entry: `${naming}.js`, | ||||
|   }, | ||||
|   external: external, | ||||
|   env: 'KEVISUAL_*', | ||||
| }); | ||||
|  | ||||
| // const cmd = `dts -i src/index.ts -o app.d.ts`; | ||||
| const cmd = `dts -i ${entry} -o ${naming}.d.ts`; | ||||
| execSync(cmd, { stdio: 'inherit' }); | ||||
							
								
								
									
										22
									
								
								backend/kevisual.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								backend/kevisual.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| { | ||||
|   "sync": { | ||||
|     ".gitignore": { | ||||
|       "url": "https://kevisual.xiongxiao.me/root/ai/code/config/gitignore/node.txt" | ||||
|     }, | ||||
|     ".npmrc": { | ||||
|       "url": "https://kevisual.xiongxiao.me/root/ai/code/config/npm/.npmrc" | ||||
|     }, | ||||
|     "tsconfig.json": { | ||||
|       "url": "https://kevisual.xiongxiao.me/root/ai/code/config/ts/backend.json" | ||||
|     }, | ||||
|     "bun.config.mjs": { | ||||
|       "url": "https://kevisual.xiongxiao.me/root/ai/code/config/bun/bun.config.mjs" | ||||
|     }, | ||||
|     ".cnb.yml": { | ||||
|       "url": "https://kevisual.xiongxiao.me/root/ai/code/config/cnb/dev.yml" | ||||
|     }, | ||||
|     "package.json": { | ||||
|       "url": "https://kevisual.xiongxiao.me/root/ai/code/config/npm/back-01-base/package.json" | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										64
									
								
								backend/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								backend/package.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,64 @@ | ||||
| { | ||||
|   "name": "ticket-services", | ||||
|   "version": "0.0.1", | ||||
|   "description": "", | ||||
|   "main": "index.js", | ||||
|   "basename": "/root/ticket-services", | ||||
|   "app": { | ||||
|     "key": "ticket-services", | ||||
|     "entry": "dist/app.js", | ||||
|     "type": "system-app" | ||||
|   }, | ||||
|   "files": [ | ||||
|     "dist" | ||||
|   ], | ||||
|   "scripts": { | ||||
|     "dev": "cross-env NODE_TLS_REJECT_UNAUTHORIZED=0  bun --watch src/dev.ts ", | ||||
|     "build": "rimraf dist && bun run bun.config.mjs", | ||||
|     "test": "tsx  test/**/*.ts", | ||||
|     "clean": "rm -rf dist", | ||||
|     "pub": "npm run build && envision pack -p -u", | ||||
|     "cmd": "tsx cmd/index.ts " | ||||
|   }, | ||||
|   "keywords": [], | ||||
|   "author": "abearxiong <xiongxiao@xiongxiao.me>", | ||||
|   "license": "MIT", | ||||
|   "type": "module", | ||||
|   "types": "types/index.d.ts", | ||||
|   "publishConfig": { | ||||
|     "access": "public" | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "@kevisual/code-center-module": "0.0.23", | ||||
|     "@kevisual/router": "0.0.23", | ||||
|     "@kevisual/use-config": "^1.0.19", | ||||
|     "cookie": "^1.0.2", | ||||
|     "dayjs": "^1.11.13", | ||||
|     "formidable": "^3.5.4", | ||||
|     "lodash-es": "^4.17.21" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@kevisual/context": "^0.0.3", | ||||
|     "@kevisual/local-proxy": "^0.0.6", | ||||
|     "@kevisual/types": "^0.0.10", | ||||
|     "@kevisual/use-config": "^1.0.19", | ||||
|     "@types/bun": "^1.2.16", | ||||
|     "@types/crypto-js": "^4.2.2", | ||||
|     "@types/formidable": "^3.4.5", | ||||
|     "@types/lodash-es": "^4.17.12", | ||||
|     "@types/node": "^24.0.3", | ||||
|     "commander": "^14.0.0", | ||||
|     "concurrently": "^9.1.2", | ||||
|     "cross-env": "^7.0.3", | ||||
|     "inquire": "^0.4.8", | ||||
|     "ioredis": "^5.6.1", | ||||
|     "nodemon": "^3.1.10", | ||||
|     "pg": "^8.16.1", | ||||
|     "rimraf": "^6.0.1", | ||||
|     "sequelize": "^6.37.7", | ||||
|     "sqlite3": "^5.1.7", | ||||
|     "tape": "^5.9.0", | ||||
|     "typescript": "^5.8.3" | ||||
|   }, | ||||
|   "packageManager": "pnpm@10.12.1" | ||||
| } | ||||
							
								
								
									
										3572
									
								
								backend/pnpm-lock.yaml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										3572
									
								
								backend/pnpm-lock.yaml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										2
									
								
								backend/pnpm-workspace.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								backend/pnpm-workspace.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| onlyBuiltDependencies: | ||||
|   - sqlite3 | ||||
							
								
								
									
										7
									
								
								backend/src/app.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								backend/src/app.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| import { App } from '@kevisual/router'; | ||||
|  | ||||
| import { useContextKey } from '@kevisual/context'; | ||||
|  | ||||
| export const app = useContextKey<App>('app', () => { | ||||
|   return new App(); | ||||
| }); | ||||
							
								
								
									
										1
									
								
								backend/src/dev.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								backend/src/dev.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| import './index.ts'; | ||||
							
								
								
									
										15
									
								
								backend/src/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								backend/src/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| import { proxyRoute, initProxy } from '@kevisual/local-proxy/proxy.ts'; | ||||
| initProxy({ | ||||
|   pagesDir: './pages', | ||||
|   watch: true, | ||||
|   home: '/root/tickets', | ||||
| }); | ||||
| import { app } from './app.ts'; | ||||
|  | ||||
| import './routes/ticket/list.ts'; | ||||
|  | ||||
| app.listen(3004, () => { | ||||
|   console.log('Server is running on http://localhost:3004'); | ||||
| }); | ||||
|  | ||||
| app.onServerRequest(proxyRoute); | ||||
							
								
								
									
										9
									
								
								backend/src/modules/sequelize.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								backend/src/modules/sequelize.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| import { Sequelize } from 'sequelize'; | ||||
| import path from 'node:path'; | ||||
| const sqlitePath = path.join(process.cwd(), 'storage', 'ticket', 'db.sqlite'); | ||||
|  | ||||
| export const sequelize = new Sequelize({ | ||||
|   dialect: 'sqlite', | ||||
|   storage: sqlitePath, | ||||
|   logging: false, | ||||
| }); | ||||
							
								
								
									
										126
									
								
								backend/src/routes/ticket/list.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										126
									
								
								backend/src/routes/ticket/list.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,126 @@ | ||||
| import { app } from '@/app.ts'; | ||||
| // import mockTickets from './mock/data.ts'; | ||||
| import { TicketModel } from './model.ts'; | ||||
| import { Sequelize, Op, WhereOptions } from 'sequelize'; | ||||
|  | ||||
| // 定义搜索参数类型 | ||||
| type SearchTicketsParams = { | ||||
|   current?: number; | ||||
|   pageSize?: number; | ||||
|   search?: string; | ||||
|   status?: string; | ||||
|   type?: string; | ||||
|   sort?: string; | ||||
|   order?: 'asc' | 'desc'; | ||||
|   uid?: string; | ||||
|   startDate?: string; | ||||
|   endDate?: string; | ||||
| }; | ||||
|  | ||||
| app | ||||
|   .route({ | ||||
|     path: 'ticket', | ||||
|     key: 'list', | ||||
|   }) | ||||
|   .define(async (ctx) => { | ||||
|     const searchForm = ctx.query?.data || {} as SearchTicketsParams; | ||||
|     const {  | ||||
|       current = 1,  | ||||
|       pageSize = 10, | ||||
|       search, | ||||
|       status, | ||||
|       type, | ||||
|       sort = 'createdAt', | ||||
|       order = 'desc', | ||||
|       uid, | ||||
|       startDate, | ||||
|       endDate | ||||
|     } = searchForm; | ||||
|      | ||||
|     // 构建查询条件 | ||||
|     let where: any = {}; | ||||
|      | ||||
|     // 如果提供了搜索关键词,同时搜索标题和描述 | ||||
|     if (search) { | ||||
|       where[Op.or] = [ | ||||
|         { title: { [Op.like]: `%${search}%` } }, | ||||
|         { description: { [Op.like]: `%${search}%` } } | ||||
|       ]; | ||||
|     } | ||||
|      | ||||
|     // 添加其他过滤条件 | ||||
|     if (status) where.status = status; | ||||
|     if (type) where.type = type; | ||||
|     if (uid) where.uid = uid; | ||||
|      | ||||
|     // 日期范围过滤 | ||||
|     if (startDate || endDate) { | ||||
|       const dateFilter: any = {}; | ||||
|       if (startDate) dateFilter[Op.gte] = new Date(startDate); | ||||
|       if (endDate) dateFilter[Op.lte] = new Date(endDate); | ||||
|       where.createdAt = dateFilter; | ||||
|     } | ||||
|  | ||||
|     // 验证排序字段是否有效,防止SQL注入 | ||||
|     const validSortFields = ['id', 'title', 'status', 'type', 'price', 'createdAt', 'updatedAt']; | ||||
|     const sortField = validSortFields.includes(sort) ? sort : 'createdAt'; | ||||
|     const sortOrder = order?.toUpperCase() === 'ASC' ? 'ASC' : 'DESC'; | ||||
|      | ||||
|     const { rows, count } = await TicketModel.findAndCountAll({ | ||||
|       where, | ||||
|       offset: (current - 1) * pageSize, | ||||
|       limit: pageSize, | ||||
|       order: [[sortField, sortOrder]], | ||||
|     }); | ||||
|      | ||||
|     ctx.body = { | ||||
|       list: rows, | ||||
|       pagination: { | ||||
|         total: count, | ||||
|         current: Number(current), | ||||
|         pageSize: Number(pageSize), | ||||
|         totalPages: Math.ceil(count / Number(pageSize)), | ||||
|       }, | ||||
|     }; | ||||
|   }) | ||||
|   .addTo(app); | ||||
|  | ||||
| app | ||||
|   .route({ | ||||
|     path: 'ticket', | ||||
|     key: 'update', | ||||
|   }) | ||||
|   .define(async (ctx) => { | ||||
|     const data = ctx.query?.data || {}; | ||||
|     const { id, ...updateData } = data; | ||||
|     if (!id) { | ||||
|       ctx.throw(400, 'ID is required for update'); | ||||
|     } | ||||
|     const ticket = await TicketModel.findByPk(id); | ||||
|     if (!ticket) { | ||||
|       ctx.throw(404, 'Ticket not found'); | ||||
|     } | ||||
|     await ticket.update(updateData); | ||||
|     ctx.body = ticket; | ||||
|   }) | ||||
|   .addTo(app); | ||||
|  | ||||
| app | ||||
|   .route({ | ||||
|     path: 'ticket', | ||||
|     key: 'delete', | ||||
|   }) | ||||
|   .define(async (ctx) => { | ||||
|     const data = ctx.query?.data || {}; | ||||
|     const { id } = data; | ||||
|     if (!id) { | ||||
|       ctx.throw(400, 'ID is required for deletion'); | ||||
|     } | ||||
|     const ticket = await TicketModel.findByPk(id); | ||||
|     if (!ticket) { | ||||
|       ctx.throw(404, 'Ticket not found'); | ||||
|     } | ||||
|     await ticket.destroy(); | ||||
|     ctx.body = { message: 'Ticket deleted successfully' }; | ||||
|   }) | ||||
|   .addTo(app); | ||||
							
								
								
									
										57
									
								
								backend/src/routes/ticket/mock/data.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								backend/src/routes/ticket/mock/data.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,57 @@ | ||||
| const ticketStatus = ['rule', 'people'] as const; | ||||
|  | ||||
| export type Ticket = { | ||||
|   id: string; | ||||
|   type: string; | ||||
|   title: string; | ||||
|   description: string; | ||||
|   status: (typeof ticketStatus)[number]; | ||||
|   price: string; | ||||
|   createdAt: string; | ||||
|   updatedAt: string; | ||||
|   uid: string; | ||||
| }; | ||||
|  | ||||
| // 票务类型列表 | ||||
| const ticketTypes = ['演唱会', '话剧', '电影', '展览', '体育赛事']; | ||||
|  | ||||
| // 生成随机日期字符串(过去30天内) | ||||
| const generateRandomDate = () => { | ||||
|   const now = new Date(); | ||||
|   const pastDays = Math.floor(Math.random() * 30); | ||||
|   const date = new Date(now.getTime() - pastDays * 24 * 60 * 60 * 1000); | ||||
|   return date.toISOString(); | ||||
| }; | ||||
|  | ||||
| // 生成随机价格(100-2000元) | ||||
| const generateRandomPrice = () => { | ||||
|   return `¥${Math.floor(Math.random() * 1900 + 100)}`; | ||||
| }; | ||||
|  | ||||
| // 生成随机ID | ||||
| const generateRandomId = () => { | ||||
|   return Math.random().toString(36).substring(2, 10); | ||||
| }; | ||||
|  | ||||
| // 生成20条模拟数据 | ||||
| export const mockTickets: Ticket[] = Array.from({ length: 20 }, (_, index) => { | ||||
|   const createdAt = generateRandomDate(); | ||||
|   const updatedAt = new Date(new Date(createdAt).getTime() + Math.random() * 24 * 60 * 60 * 1000).toISOString(); | ||||
|   const type = ticketTypes[Math.floor(Math.random() * ticketTypes.length)]; | ||||
|   const status = ticketStatus[Math.floor(Math.random() * ticketStatus.length)]; | ||||
|    | ||||
|   return { | ||||
|     id: `ticket-${generateRandomId()}`, | ||||
|     type, | ||||
|     title: `${type}票务-${index + 1}`, | ||||
|     description: `这是一张${type}的门票,编号为${index + 1},提供优质观演体验。`, | ||||
|     status, | ||||
|     price: generateRandomPrice(), | ||||
|     createdAt, | ||||
|     updatedAt, | ||||
|     uid: `user-${generateRandomId()}`, | ||||
|   }; | ||||
| }); | ||||
|  | ||||
| // 导出默认数据 | ||||
| export default mockTickets; | ||||
							
								
								
									
										80
									
								
								backend/src/routes/ticket/model.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								backend/src/routes/ticket/model.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,80 @@ | ||||
| import { DataTypes, Model, UUIDV4 } from 'sequelize'; | ||||
| import { sequelize } from '@/modules/sequelize.ts'; | ||||
|  | ||||
| // 定义票据状态类型 | ||||
| const ticketStatus = ['rule', 'people'] as const; | ||||
| export type Ticket = { | ||||
|   id: string; | ||||
|   type: string; | ||||
|   title: string; | ||||
|   description: string; | ||||
|   status: (typeof ticketStatus)[number]; | ||||
|   price: string; | ||||
|   createdAt: string; | ||||
|   updatedAt: string; | ||||
|   uid: string; | ||||
| }; | ||||
|  | ||||
| // 创建Ticket模型类 | ||||
| export class TicketModel extends Model<Ticket> implements Ticket { | ||||
|   declare id: string; | ||||
|   declare type: string; | ||||
|   declare title: string; | ||||
|   declare description: string; | ||||
|   declare status: (typeof ticketStatus)[number]; | ||||
|   declare price: string; | ||||
|   declare createdAt: string; | ||||
|   declare updatedAt: string; | ||||
|   declare uid: string; | ||||
| } | ||||
|  | ||||
| // 初始化模型 | ||||
| TicketModel.init( | ||||
|   { | ||||
|     id: { | ||||
|       type: DataTypes.UUID, | ||||
|       defaultValue: UUIDV4, | ||||
|       primaryKey: true, | ||||
|     }, | ||||
|     type: { | ||||
|       type: DataTypes.STRING, | ||||
|       allowNull: false, | ||||
|     }, | ||||
|     title: { | ||||
|       type: DataTypes.STRING, | ||||
|       allowNull: false, | ||||
|     }, | ||||
|     description: { | ||||
|       type: DataTypes.TEXT, | ||||
|       allowNull: true, | ||||
|     }, | ||||
|     status: { | ||||
|       type: DataTypes.STRING, | ||||
|       allowNull: false, | ||||
|       defaultValue: 'rule', | ||||
|     }, | ||||
|     price: { | ||||
|       type: DataTypes.STRING, | ||||
|       allowNull: false, | ||||
|     }, | ||||
|     uid: { | ||||
|       type: DataTypes.STRING, | ||||
|       allowNull: false, | ||||
|     }, | ||||
|     createdAt: { | ||||
|       type: DataTypes.DATE, | ||||
|       allowNull: false, | ||||
|     }, | ||||
|     updatedAt: { | ||||
|       type: DataTypes.DATE, | ||||
|       allowNull: false, | ||||
|     }, | ||||
|   }, | ||||
|   { | ||||
|     sequelize, | ||||
|     tableName: 'tickets', | ||||
|     timestamps: true, | ||||
|   }, | ||||
| ); | ||||
|  | ||||
| await TicketModel.sync({ alter: true }) | ||||
							
								
								
									
										18
									
								
								backend/src/test/insert-demos.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								backend/src/test/insert-demos.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| import { mockTickets } from './../routes/ticket/mock/data.ts'; | ||||
|  | ||||
| import { TicketModel } from '@/routes/ticket/model.ts'; | ||||
|  | ||||
| export const main = async () => { | ||||
|   // Clear existing data | ||||
|   // await TicketModel.destroy({ where: {}, truncate: true }); | ||||
|  | ||||
|   // Insert mock tickets | ||||
|   for (const ticket of mockTickets) { | ||||
|     delete ticket.id; // Remove id to let Sequelize generate it | ||||
|     await TicketModel.create(ticket); | ||||
|   } | ||||
|  | ||||
|   console.log('Mock tickets inserted successfully.'); | ||||
| }; | ||||
|  | ||||
| main() | ||||
							
								
								
									
										18
									
								
								backend/tsconfig.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								backend/tsconfig.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| { | ||||
|   "extends": "@kevisual/types/json/backend.json", | ||||
|   "compilerOptions": { | ||||
|     "baseUrl": ".", | ||||
|     "typeRoots": [ | ||||
|       "./node_modules/@types", | ||||
|       "./node_modules/@kevisual" | ||||
|     ], | ||||
|     "paths": { | ||||
|       "@/*": [ | ||||
|         "src/*" | ||||
|       ] | ||||
|     }, | ||||
|   }, | ||||
|   "include": [ | ||||
|     "src/**/*", | ||||
|   ], | ||||
| } | ||||
							
								
								
									
										39
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										39
									
								
								package.json
									
									
									
									
									
								
							| @@ -3,15 +3,16 @@ | ||||
|   "private": true, | ||||
|   "version": "0.0.1", | ||||
|   "type": "module", | ||||
|   "basename": "/", | ||||
|   "basename": "/root/tickets", | ||||
|   "scripts": { | ||||
|     "dev": "vite", | ||||
|     "build": "vite build", | ||||
|     "build:css": "tailwindcss -i ./src/index.css -o ./dist/render.css --minify", | ||||
|     "postbuild2": "pnpm build:css", | ||||
|     "postbuild": "rsync -av --delete ./dist/* ./backend/pages/root/tickets", | ||||
|     "preview": "vite preview", | ||||
|     "pub": "envision deploy ./dist -k vite-react -v 0.0.1", | ||||
|     "dev:lib": "turbo dev" | ||||
|     "serve": "cd backend && bun --watch src/dev.ts" | ||||
|   }, | ||||
|   "files": [ | ||||
|     "dist" | ||||
| @@ -19,33 +20,37 @@ | ||||
|   "author": "abearxiong <xiongxiao@xiongxiao.me>", | ||||
|   "license": "MIT", | ||||
|   "dependencies": { | ||||
|     "@kevisual/router": "0.0.9", | ||||
|     "@ant-design/icons": "^6.0.0", | ||||
|     "@ant-design/v5-patch-for-react-19": "^1.0.3", | ||||
|     "@kevisual/router": "0.0.22", | ||||
|     "antd": "^5.26.1", | ||||
|     "clsx": "^2.1.1", | ||||
|     "dayjs": "^1.11.13", | ||||
|     "lodash-es": "^4.17.21", | ||||
|     "lucide-react": "^0.487.0", | ||||
|     "lucide-react": "^0.518.0", | ||||
|     "nanoid": "^5.1.5", | ||||
|     "react": "^19.1.0", | ||||
|     "react-dom": "^19.1.0", | ||||
|     "react-toastify": "^11.0.5", | ||||
|     "zustand": "^5.0.3" | ||||
|     "zustand": "^5.0.5" | ||||
|   }, | ||||
|   "publishConfig": { | ||||
|     "access": "public" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@kevisual/query": "0.0.15", | ||||
|     "@kevisual/types": "^0.0.6", | ||||
|     "@tailwindcss/vite": "^4.1.1", | ||||
|     "@types/node": "^22.13.17", | ||||
|     "@types/react": "^19.1.0", | ||||
|     "@types/react-dom": "^19.1.1", | ||||
|     "@kevisual/query": "0.0.29", | ||||
|     "@kevisual/types": "^0.0.10", | ||||
|     "@tailwindcss/vite": "^4.1.10", | ||||
|     "@types/node": "^24.0.3", | ||||
|     "@types/react": "^19.1.8", | ||||
|     "@types/react-dom": "^19.1.6", | ||||
|     "@vitejs/plugin-basic-ssl": "^2.0.0", | ||||
|     "@vitejs/plugin-react": "^4.3.4", | ||||
|     "commander": "^13.1.0", | ||||
|     "tailwindcss": "^4.1.1", | ||||
|     "typescript": "^5.8.2", | ||||
|     "vite": "^6.2.4" | ||||
|     "@vitejs/plugin-react": "^4.5.2", | ||||
|     "commander": "^14.0.0", | ||||
|     "tailwindcss": "^4.1.10", | ||||
|     "typescript": "^5.8.3", | ||||
|     "vite": "^6.3.5" | ||||
|   }, | ||||
|   "packageManager": "pnpm@10.7.1" | ||||
|   "packageManager": "pnpm@10.12.1", | ||||
|   "pnpm": {} | ||||
| } | ||||
							
								
								
									
										5300
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										5300
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										7
									
								
								pnpm-workspace.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								pnpm-workspace.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| packages: | ||||
|   - backend | ||||
|  | ||||
| onlyBuiltDependencies: | ||||
|   - sqlite3 | ||||
|   - "@tailwindcss/oxide" | ||||
|   - esbuild | ||||
							
								
								
									
										15
									
								
								prompts/ticket.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								prompts/ticket.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| # ticket | ||||
|  | ||||
| ### 整体的开发大纲 | ||||
|  | ||||
| 前端 | ||||
|  | ||||
| - 前端的工单页面 | ||||
|   - 表格页面 | ||||
|   - 弹窗表单 | ||||
|   - 数据获取 | ||||
|  | ||||
| 后端 | ||||
|  | ||||
| - 后端的工单列表 | ||||
|   - 工单的增删改查 | ||||
| @@ -1,6 +1,5 @@ | ||||
| import { createRoot } from 'react-dom/client'; | ||||
| import { App } from './pages/App.tsx'; | ||||
|  | ||||
| import { App } from './pages/App'; | ||||
| // import './index.css'; | ||||
|  | ||||
| createRoot(document.getElementById('root')!).render(<App />); | ||||
|   | ||||
| @@ -1,3 +1,3 @@ | ||||
| import { QueryClient } from '@kevisual/query'; | ||||
| import { Query } from '@kevisual/query'; | ||||
|  | ||||
| export const query = new QueryClient(); | ||||
| export const query = new Query(); | ||||
|   | ||||
| @@ -1,87 +1,13 @@ | ||||
| import { useState } from 'react'; | ||||
| import { Box, Button, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, IconButton } from '@mui/material'; | ||||
| import { Edit, Delete } from 'lucide-react'; | ||||
| import { ModalForm } from './ModalForm'; | ||||
|  | ||||
| // 定义工单类型 | ||||
| interface Ticket { | ||||
|   id: number; | ||||
|   title: string; | ||||
|   status: string; | ||||
|   priority: string; | ||||
|   createTime: string; | ||||
| } | ||||
| import { ToastContainer } from 'react-toastify'; | ||||
| import { ConfigProvider } from 'antd'; | ||||
| import '@ant-design/v5-patch-for-react-19'; | ||||
|  | ||||
| import { Ticket } from './ticket/Ticket'; | ||||
| export const App = () => { | ||||
|   const [tickets, setTickets] = useState<Ticket[]>([]); | ||||
|   const [open, setOpen] = useState(false); | ||||
|   const [selectedTicket, setSelectedTicket] = useState<Ticket | null>(null); | ||||
|  | ||||
|   const handleCreate = () => { | ||||
|     setSelectedTicket(null); | ||||
|     setOpen(true); | ||||
|   }; | ||||
|  | ||||
|   const handleEdit = (ticket: Ticket) => { | ||||
|     setSelectedTicket(ticket); | ||||
|     setOpen(true); | ||||
|   }; | ||||
|  | ||||
|   const handleDelete = (id: number) => { | ||||
|     setTickets(tickets.filter((ticket) => ticket.id !== id)); | ||||
|   }; | ||||
|  | ||||
|   const handleSave = (data: Ticket) => { | ||||
|     if (selectedTicket) { | ||||
|       setTickets(tickets.map((t) => (t.id === selectedTicket.id ? data : t))); | ||||
|     } else { | ||||
|       setTickets([...tickets, { ...data, id: Date.now() }]); | ||||
|     } | ||||
|     setOpen(false); | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <Box className='p-4'> | ||||
|       <Box className='mb-4 flex justify-between'> | ||||
|         <h1 className='text-2xl font-bold'>工单管理</h1> | ||||
|         <Button variant='contained' onClick={handleCreate}> | ||||
|           创建工单 | ||||
|         </Button> | ||||
|       </Box> | ||||
|  | ||||
|       <TableContainer component={Paper}> | ||||
|         <Table> | ||||
|           <TableHead> | ||||
|             <TableRow> | ||||
|               <TableCell>标题</TableCell> | ||||
|               <TableCell>状态</TableCell> | ||||
|               <TableCell>优先级</TableCell> | ||||
|               <TableCell>创建时间</TableCell> | ||||
|               <TableCell>操作</TableCell> | ||||
|             </TableRow> | ||||
|           </TableHead> | ||||
|           <TableBody> | ||||
|             {tickets.map((ticket) => ( | ||||
|               <TableRow key={ticket.id}> | ||||
|                 <TableCell>{ticket.title}</TableCell> | ||||
|                 <TableCell>{ticket.status}</TableCell> | ||||
|                 <TableCell>{ticket.priority}</TableCell> | ||||
|                 <TableCell>{ticket.createTime}</TableCell> | ||||
|                 <TableCell> | ||||
|                   <IconButton onClick={() => handleEdit(ticket)}> | ||||
|                     <Edit className='w-4 h-4' /> | ||||
|                   </IconButton> | ||||
|                   <IconButton onClick={() => handleDelete(ticket.id)}> | ||||
|                     <Delete className='w-4 h-4' /> | ||||
|                   </IconButton> | ||||
|                 </TableCell> | ||||
|               </TableRow> | ||||
|             ))} | ||||
|           </TableBody> | ||||
|         </Table> | ||||
|       </TableContainer> | ||||
|  | ||||
|       <ModalForm open={open} onClose={() => setOpen(false)} onSave={handleSave} ticket={selectedTicket} /> | ||||
|     </Box> | ||||
|     <ConfigProvider> | ||||
|       <Ticket /> | ||||
|       <ToastContainer /> | ||||
|     </ConfigProvider> | ||||
|   ); | ||||
| }; | ||||
|   | ||||
| @@ -1,121 +0,0 @@ | ||||
| import { useEffect } from 'react'; | ||||
| import { useForm, Controller } from 'react-hook-form'; | ||||
| import { | ||||
|   Dialog, | ||||
|   DialogTitle, | ||||
|   DialogContent, | ||||
|   DialogActions, | ||||
|   Button, | ||||
|   TextField, | ||||
|   MenuItem, | ||||
|   Box | ||||
| } from '@mui/material'; | ||||
|  | ||||
| interface Ticket { | ||||
|   id: number; | ||||
|   title: string; | ||||
|   status: string; | ||||
|   priority: string; | ||||
|   createTime: string; | ||||
| } | ||||
|  | ||||
| interface ModalFormProps { | ||||
|   open: boolean; | ||||
|   onClose: () => void; | ||||
|   onSave: (data: Ticket) => void; | ||||
|   ticket: Ticket | null; | ||||
| } | ||||
|  | ||||
| export const ModalForm = ({ open, onClose, onSave, ticket }: ModalFormProps) => { | ||||
|   const { control, handleSubmit, reset } = useForm<Ticket>({ | ||||
|     defaultValues: { | ||||
|       title: '', | ||||
|       status: '待处理', | ||||
|       priority: '中', | ||||
|       createTime: new Date().toLocaleString() | ||||
|     } | ||||
|   }); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     if (ticket) { | ||||
|       reset(ticket); | ||||
|     } else { | ||||
|       reset({ | ||||
|         title: '', | ||||
|         status: '待处理', | ||||
|         priority: '中', | ||||
|         createTime: new Date().toLocaleString() | ||||
|       }); | ||||
|     } | ||||
|   }, [ticket, reset]); | ||||
|  | ||||
|   const onSubmit = (data: Ticket) => { | ||||
|     onSave(data); | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth> | ||||
|       <DialogTitle>{ticket ? '编辑工单' : '创建工单'}</DialogTitle> | ||||
|       <form onSubmit={handleSubmit(onSubmit)}> | ||||
|         <DialogContent> | ||||
|           <Box className="space-y-4"> | ||||
|             <Controller | ||||
|               name="title" | ||||
|               control={control} | ||||
|               rules={{ required: '请输入标题' }} | ||||
|               render={({ field, fieldState }) => ( | ||||
|                 <TextField | ||||
|                   {...field} | ||||
|                   label="标题" | ||||
|                   fullWidth | ||||
|                   error={!!fieldState.error} | ||||
|                   helperText={fieldState.error?.message} | ||||
|                 /> | ||||
|               )} | ||||
|             /> | ||||
|  | ||||
|             <Controller | ||||
|               name="status" | ||||
|               control={control} | ||||
|               render={({ field }) => ( | ||||
|                 <TextField | ||||
|                   {...field} | ||||
|                   select | ||||
|                   label="状态" | ||||
|                   fullWidth | ||||
|                 > | ||||
|                   <MenuItem value="待处理">待处理</MenuItem> | ||||
|                   <MenuItem value="处理中">处理中</MenuItem> | ||||
|                   <MenuItem value="已完成">已完成</MenuItem> | ||||
|                 </TextField> | ||||
|               )} | ||||
|             /> | ||||
|  | ||||
|             <Controller | ||||
|               name="priority" | ||||
|               control={control} | ||||
|               render={({ field }) => ( | ||||
|                 <TextField | ||||
|                   {...field} | ||||
|                   select | ||||
|                   label="优先级" | ||||
|                   fullWidth | ||||
|                 > | ||||
|                   <MenuItem value="高">高</MenuItem> | ||||
|                   <MenuItem value="中">中</MenuItem> | ||||
|                   <MenuItem value="低">低</MenuItem> | ||||
|                 </TextField> | ||||
|               )} | ||||
|             /> | ||||
|           </Box> | ||||
|         </DialogContent> | ||||
|         <DialogActions> | ||||
|           <Button onClick={onClose}>取消</Button> | ||||
|           <Button type="submit" variant="contained"> | ||||
|             保存 | ||||
|           </Button> | ||||
|         </DialogActions> | ||||
|       </form> | ||||
|     </Dialog> | ||||
|   ); | ||||
| }; | ||||
							
								
								
									
										12
									
								
								src/pages/define/type.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/pages/define/type.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| const ticketStatus = ['rule', 'people'] as const; | ||||
| export type Ticket = { | ||||
|   id: string; | ||||
|   type: string; | ||||
|   title: string; | ||||
|   description: string; | ||||
|   status: (typeof ticketStatus)[number]; | ||||
|   price: string; | ||||
|   createdAt: string; | ||||
|   updatedAt: string; | ||||
|   uid: string; | ||||
| }; | ||||
							
								
								
									
										307
									
								
								src/pages/ticket/Ticket.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										307
									
								
								src/pages/ticket/Ticket.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,307 @@ | ||||
| import { useEffect, useState } from 'react'; | ||||
| import { Modal, Table, Form, Input, Button, Space, Select, DatePicker, message, Popconfirm } from 'antd'; | ||||
| import { SearchOutlined, PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons'; | ||||
| import { useTicketStore } from '@/store'; | ||||
| import type { Ticket as TicketType } from '@/pages/define/type'; | ||||
| import type { ColumnsType } from 'antd/es/table'; | ||||
| import dayjs from 'dayjs'; | ||||
|  | ||||
| export const Ticket = () => { | ||||
|   const [searchForm] = Form.useForm(); | ||||
|   const store = useTicketStore(); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     fetchTickets(); | ||||
|   }, [store.searchForm]); | ||||
|  | ||||
|   const fetchTickets = async () => { | ||||
|     try { | ||||
|       store.setLoading(true); | ||||
|       await store.getTickets(store.searchForm); | ||||
|     } catch (error) { | ||||
|       message.error('获取票据列表失败'); | ||||
|     } finally { | ||||
|       store.setLoading(false); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const handleSearch = async (values: any) => { | ||||
|     store.setSearchForm({ | ||||
|       ...store.searchForm, | ||||
|       ...values, | ||||
|       current: 1, | ||||
|       startDate: values.dateRange?.[0]?.format('YYYY-MM-DD'), | ||||
|       endDate: values.dateRange?.[1]?.format('YYYY-MM-DD'), | ||||
|     }); | ||||
|   }; | ||||
|  | ||||
|   const handleReset = () => { | ||||
|     searchForm.resetFields(); | ||||
|     store.setSearchForm({ | ||||
|       current: 1, | ||||
|       pageSize: 10, | ||||
|     }); | ||||
|   }; | ||||
|  | ||||
|   const handleAdd = () => { | ||||
|     store.setFormData(null); | ||||
|     store.setShowEdit(true); | ||||
|   }; | ||||
|  | ||||
|   const handleEdit = (record: TicketType) => { | ||||
|     store.setFormData(record); | ||||
|     store.setShowEdit(true); | ||||
|   }; | ||||
|  | ||||
|   const handleDelete = async (id: string) => { | ||||
|     try { | ||||
|       store.setLoading(true); | ||||
|       await store.deleteTicket(id); | ||||
|     } catch (error) { | ||||
|     } finally { | ||||
|       store.setLoading(false); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const columns: ColumnsType<TicketType> = [ | ||||
|     { | ||||
|       title: 'ID', | ||||
|       dataIndex: 'id', | ||||
|       key: 'id', | ||||
|       width: 320, | ||||
|     }, | ||||
|     { | ||||
|       title: '类型', | ||||
|       dataIndex: 'type', | ||||
|       key: 'type', | ||||
|       width: 120, | ||||
|     }, | ||||
|     { | ||||
|       title: '标题', | ||||
|       dataIndex: 'title', | ||||
|       key: 'title', | ||||
|       width: 200, | ||||
|     }, | ||||
|     { | ||||
|       title: '描述', | ||||
|       dataIndex: 'description', | ||||
|       key: 'description', | ||||
|       ellipsis: true, | ||||
|     }, | ||||
|     { | ||||
|       title: '状态', | ||||
|       dataIndex: 'status', | ||||
|       key: 'status', | ||||
|       width: 100, | ||||
|     }, | ||||
|     { | ||||
|       title: '价格', | ||||
|       dataIndex: 'price', | ||||
|       key: 'price', | ||||
|       width: 100, | ||||
|     }, | ||||
|     { | ||||
|       title: '创建时间', | ||||
|       dataIndex: 'createdAt', | ||||
|       key: 'createdAt', | ||||
|       width: 180, | ||||
|       render: (text) => (text ? dayjs(text).format('YYYY-MM-DD HH:mm') : '-'), | ||||
|     }, | ||||
|     { | ||||
|       title: '更新时间', | ||||
|       dataIndex: 'updatedAt', | ||||
|       key: 'updatedAt', | ||||
|       width: 180, | ||||
|       render: (text) => (text ? dayjs(text).format('YYYY-MM-DD HH:mm') : '-'), | ||||
|     }, | ||||
|     { | ||||
|       title: '操作', | ||||
|       key: 'action', | ||||
|       width: 150, | ||||
|       render: (_, record) => ( | ||||
|         <Space size='middle'> | ||||
|           <Button type='text' icon={<EditOutlined />} onClick={() => handleEdit(record)} /> | ||||
|           <Popconfirm title='确定要删除这条记录吗?' onConfirm={() => handleDelete(record.id)} okText='确定' cancelText='取消'> | ||||
|             <Button type='text' danger icon={<DeleteOutlined />} /> | ||||
|           </Popconfirm> | ||||
|         </Space> | ||||
|       ), | ||||
|     }, | ||||
|   ]; | ||||
|  | ||||
|   return ( | ||||
|     <div className='ticket-container p-4 h-full overflow-auto scrollbar'> | ||||
|       <h1>票据管理系统</h1> | ||||
|  | ||||
|       {/* 搜索表单 */} | ||||
|       <div className='search-form-container' style={{ marginBottom: 16, padding: 16, background: '#f9f9f9', borderRadius: 4 }}> | ||||
|         <Form form={searchForm} layout='inline' onFinish={handleSearch}> | ||||
|           <Form.Item name='search' label='关键词'> | ||||
|             <Input placeholder='标题/描述' allowClear /> | ||||
|           </Form.Item> | ||||
|           <Form.Item name='type' label='类型'> | ||||
|             <Select | ||||
|               placeholder='选择类型' | ||||
|               allowClear | ||||
|               style={{ width: 120 }} | ||||
|               options={[ | ||||
|                 { value: 'type1', label: '类型1' }, | ||||
|                 { value: 'type2', label: '类型2' }, | ||||
|               ]} | ||||
|             /> | ||||
|           </Form.Item> | ||||
|           <Form.Item name='status' label='状态'> | ||||
|             <Select | ||||
|               placeholder='选择状态' | ||||
|               allowClear | ||||
|               style={{ width: 120 }} | ||||
|               options={[ | ||||
|                 { value: 'rule', label: '规则' }, | ||||
|                 { value: 'people', label: '人工' }, | ||||
|               ]} | ||||
|             /> | ||||
|           </Form.Item> | ||||
|           <Form.Item name='dateRange' label='日期范围'> | ||||
|             <DatePicker.RangePicker /> | ||||
|           </Form.Item> | ||||
|           <Form.Item> | ||||
|             <Space> | ||||
|               <Button type='primary' htmlType='submit' icon={<SearchOutlined />}> | ||||
|                 搜索 | ||||
|               </Button> | ||||
|               <Button onClick={handleReset}>重置</Button> | ||||
|             </Space> | ||||
|           </Form.Item> | ||||
|         </Form> | ||||
|       </div> | ||||
|  | ||||
|       {/* 操作按钮 */} | ||||
|       <div style={{ marginBottom: 16 }}> | ||||
|         <Button type='primary' icon={<PlusOutlined />} onClick={handleAdd}> | ||||
|           新建票据 | ||||
|         </Button> | ||||
|       </div> | ||||
|  | ||||
|       {/* 表格 */} | ||||
|       <Table | ||||
|         columns={columns} | ||||
|         dataSource={store.tickets} | ||||
|         rowKey='id' | ||||
|         loading={store.loading} | ||||
|         pagination={{ | ||||
|           ...store.pagination, | ||||
|           current: store.searchForm.current, | ||||
|           pageSize: store.searchForm.pageSize, | ||||
|           onChange: (page, pageSize) => { | ||||
|             store.setSearchForm({ | ||||
|               ...store.searchForm, | ||||
|               current: page, | ||||
|               pageSize, | ||||
|             }); | ||||
|           }, | ||||
|           showSizeChanger: true, | ||||
|           showQuickJumper: true, | ||||
|           showTotal: (total) => `共 ${total} 条`, | ||||
|         }} | ||||
|       /> | ||||
|  | ||||
|       {/* 引入弹窗表单组件 */} | ||||
|       <TicketModelForm /> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export const TicketModelForm = () => { | ||||
|   const [form] = Form.useForm(); | ||||
|   const store = useTicketStore(); | ||||
|   const isEdit = !!store.formData?.id; | ||||
|  | ||||
|   useEffect(() => { | ||||
|     if (!store.showEdit) { | ||||
|       return; | ||||
|     } | ||||
|     if (store.formData) { | ||||
|       form.setFieldsValue(store.formData); | ||||
|     } else { | ||||
|       // form.resetFields(); | ||||
|       form.setFieldsValue({ title: '', type: '', status: '', price: '', description: '' }); | ||||
|     } | ||||
|   }, [store.showEdit, store.formData, form]); | ||||
|  | ||||
|   const handleSubmit = async () => { | ||||
|     try { | ||||
|       const values = await form.validateFields(); | ||||
|       store.setLoading(true); | ||||
|  | ||||
|       if (isEdit) { | ||||
|         // 编辑模式 | ||||
|         await store.updateTicket({ | ||||
|           ...store.formData, | ||||
|           ...values, | ||||
|         }); | ||||
|       } else { | ||||
|         // 新增模式 - 假设后端会生成ID,这里模拟API调用 | ||||
|         const newTicket = { | ||||
|           ...values, | ||||
|         }; | ||||
|  | ||||
|         // 这里应该是一个创建票据的API调用 | ||||
|         // 暂时使用更新方法模拟 | ||||
|         await store.updateTicket(newTicket as TicketType); | ||||
|       } | ||||
|  | ||||
|       // 关闭弹窗并刷新列表 | ||||
|       store.setShowEdit(false); | ||||
|       store.getTickets(store.searchForm); | ||||
|     } catch (error) { | ||||
|       console.error('表单提交错误:', error); | ||||
|       message.error('操作失败,请检查表单'); | ||||
|     } finally { | ||||
|       store.setLoading(false); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
|     <Modal | ||||
|       title={isEdit ? '编辑票据' : '新增票据'} | ||||
|       open={store.showEdit} | ||||
|       onCancel={() => store.setShowEdit(false)} | ||||
|       onOk={handleSubmit} | ||||
|       confirmLoading={store.loading} | ||||
|       maskClosable={false}> | ||||
|       <Form form={form} layout='vertical' initialValues={store.formData || {}}> | ||||
|         <Form.Item name='title' label='标题' rules={[{ required: true, message: '请输入标题' }]}> | ||||
|           <Input placeholder='请输入标题' /> | ||||
|         </Form.Item> | ||||
|  | ||||
|         <Form.Item name='type' label='类型' rules={[{ required: true, message: '请选择类型' }]}> | ||||
|           <Select | ||||
|             placeholder='请选择类型' | ||||
|             options={[ | ||||
|               { value: 'type1', label: '类型1' }, | ||||
|               { value: 'type2', label: '类型2' }, | ||||
|             ]} | ||||
|           /> | ||||
|         </Form.Item> | ||||
|  | ||||
|         <Form.Item name='status' label='状态' rules={[{ required: true, message: '请选择状态' }]}> | ||||
|           <Select | ||||
|             placeholder='请选择状态' | ||||
|             options={[ | ||||
|               { value: 'rule', label: '规则' }, | ||||
|               { value: 'people', label: '人工' }, | ||||
|             ]} | ||||
|           /> | ||||
|         </Form.Item> | ||||
|  | ||||
|         <Form.Item name='price' label='价格' rules={[{ required: true, message: '请输入价格' }]}> | ||||
|           <Input placeholder='请输入价格' /> | ||||
|         </Form.Item> | ||||
|  | ||||
|         <Form.Item name='description' label='描述' rules={[{ required: true, message: '请输入描述' }]}> | ||||
|           <Input.TextArea rows={4} placeholder='请输入描述' /> | ||||
|         </Form.Item> | ||||
|       </Form> | ||||
|     </Modal> | ||||
|   ); | ||||
| }; | ||||
							
								
								
									
										85
									
								
								src/store/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								src/store/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,85 @@ | ||||
| import { Ticket } from '@/pages/define/type'; | ||||
| import { create } from 'zustand'; | ||||
| import { query } from '@/modules/query'; | ||||
| import { toast } from 'react-toastify'; | ||||
|  | ||||
| type SearchTicketsParams = { | ||||
|   current?: number; | ||||
|   pageSize?: number; | ||||
|   search?: string; | ||||
|   status?: string; | ||||
|   type?: string; | ||||
|   sort?: string; | ||||
|   order?: 'asc' | 'desc'; | ||||
|   uid?: string; | ||||
|   startDate?: string; | ||||
|   endDate?: string; | ||||
| }; | ||||
| type TicketStore = { | ||||
|   tickets: Ticket[]; | ||||
|   getTickets: (opts: SearchTicketsParams) => Promise<void>; | ||||
|   updateTicket: (ticket: Ticket) => Promise<void>; | ||||
|   deleteTicket: (id: string) => Promise<void>; | ||||
|   showEdit: boolean; | ||||
|   setShowEdit: (show: boolean) => void; | ||||
|   formData: Ticket | null; | ||||
|   setFormData: (data: Ticket | null) => void; | ||||
|   loading: boolean; | ||||
|   setLoading: (loading: boolean) => void; | ||||
|   searchForm: Partial<SearchTicketsParams>; | ||||
|   setSearchForm: (form: Partial<SearchTicketsParams>) => void; | ||||
|   pagination: { | ||||
|     current?: number; | ||||
|     total?: number; | ||||
|   }; | ||||
|   setPagination: (pagination: { current?: number; total?: number }) => void; | ||||
| }; | ||||
| export const useTicketStore = create<TicketStore>((set, get) => { | ||||
|   return { | ||||
|     tickets: [], | ||||
|     searchForm: { current: 1, pageSize: 10 }, | ||||
|     setSearchForm: (form) => set({ searchForm: form }), | ||||
|     pagination: { current: 1, total: 0 }, | ||||
|     setPagination: (pagination) => set({ pagination }), | ||||
|     getTickets: async (data) => { | ||||
|       const res = await query.post({ path: 'ticket', key: 'list', data }); | ||||
|       if (res.code === 200) { | ||||
|         const pagination = res.data.pagination || {}; | ||||
|         set({ tickets: res.data.list, pagination }); | ||||
|       } | ||||
|     }, | ||||
|     updateTicket: async (ticket: Ticket) => { | ||||
|       const res = await query.post({ | ||||
|         path: 'ticket', | ||||
|         key: 'update', | ||||
|         data: ticket, | ||||
|       }); | ||||
|       if (res.code === 200) { | ||||
|         toast.success('Ticket updated successfully'); | ||||
|         // get().getTickets(get().searchForm); | ||||
|       } else { | ||||
|         toast.error('Failed to update ticket'); | ||||
|       } | ||||
|     }, | ||||
|     deleteTicket: async (id: string) => { | ||||
|       const res = await query.post({ | ||||
|         path: 'ticket', | ||||
|         key: 'delete', | ||||
|         data: { id }, | ||||
|       }); | ||||
|       if (res.code === 200) { | ||||
|         toast.success('删除成功'); | ||||
|         // Refresh the ticket list after deletion | ||||
|         get().getTickets(get().searchForm); | ||||
|       } else { | ||||
|         toast.error('删除失败'); | ||||
|       } | ||||
|     }, | ||||
|     showEdit: false, | ||||
|     setShowEdit: (show) => set({ showEdit: show }), | ||||
|     formData: null, | ||||
|     setFormData: (data) => set({ formData: data }), | ||||
|     loading: false, | ||||
|     setLoading: (loading) => set({ loading }), | ||||
|   }; | ||||
| }); | ||||
| @@ -8,36 +8,26 @@ const version = pkgs.version || '0.0.1'; | ||||
| const isDev = process.env.NODE_ENV === 'development'; | ||||
| const basename = isDev ? '/' : pkgs?.basename || '/'; | ||||
|  | ||||
| const checkJsh = () => { | ||||
|   return process.env.SHELL === '/bin/jsh'; | ||||
| }; | ||||
| const isJsh = checkJsh(); | ||||
| const plugins = [react(), ]; | ||||
| const plugins = [react(), tailwindcss() ]; | ||||
|  | ||||
| if (!isJsh) { | ||||
|   const basicSsl = await import('@vitejs/plugin-basic-ssl'); | ||||
|   const tailwindcss = await import('@tailwindcss/vite'); | ||||
|   const defaultPlugin = basicSsl.default; | ||||
|   const defaultCssPlugin = tailwindcss.default; | ||||
|   plugins.push(defaultCssPlugin(),defaultPlugin()); | ||||
| } | ||||
|  | ||||
| let target = 'https://kevisual.xiongxiao.me'; | ||||
| if (isDev) { | ||||
|   target = 'https://kevisual.xiongxiao.me'; | ||||
| } else { | ||||
|   target = 'https://kevisual.cn'; | ||||
| } | ||||
| // let target = 'https://kevisual.xiongxiao.me'; | ||||
| // if (isDev) { | ||||
| //   target = 'https://kevisual.xiongxiao.me'; | ||||
| // } else { | ||||
| //   target = 'https://kevisual.cn'; | ||||
| // } | ||||
| let target  = 'http://localhost:3004'; | ||||
|  | ||||
| let proxy = { | ||||
|   '/root/system-lib/': { | ||||
|     target: `https://${target}/root/system-lib/`, | ||||
|     target: `${target}/root/system-lib/`, | ||||
|   }, | ||||
|   '/user/login/': { | ||||
|     target: `https://${target}/user/login/`, | ||||
|     target: `${target}/user/login/`, | ||||
|   }, | ||||
|   '/api': { | ||||
|     target: `https://${target}`, | ||||
|     target: `${target}`, | ||||
|     changeOrigin: true, | ||||
|     ws: true, | ||||
|     rewriteWsOrigin: true, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user