generated from template/vite-react-template
初始化应用
This commit is contained in:
parent
e56eaab69a
commit
f756082399
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,
|
||||
|
Loading…
x
Reference in New Issue
Block a user