合并本地优化内容至远端新版本(CI/CD + pnpm + 构建修复)
- 以远端新版本为基底,保留采购管理双 Tab、供应商管理、字典配置等新功能 - 叠加本地优化:删除死代码 api/index.ts 和 utils/request.js - 修复编译错误:antd-vue 4.x null→undefined(dateRange/licenseExpiry)、 bodyCell slot 回调改 any、mock/data.ts Array.from 缺失 index 参数、 移除未使用 import(statusMap/randomFloat/pick)、status as const 字面量类型 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,22 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# 仅当本次提交涉及 admin 源代码或配置时,运行 admin 构建(含 vue-tsc 类型检查)
|
||||||
|
# 通过检查暂存区是否有 zhican/admin/ 下的相关文件来决定
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
CHANGED=$(git diff --cached --name-only --diff-filter=ACMR \
|
||||||
|
| grep -E '^zhican/admin/(src/|.*\.(json|ts|html))' || true)
|
||||||
|
|
||||||
|
if [ -z "$CHANGED" ]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "→ pre-commit: 检测到 admin 变更,运行 pnpm run build..."
|
||||||
|
cd "$(git rev-parse --show-toplevel)/zhican/admin"
|
||||||
|
|
||||||
|
if ! command -v pnpm >/dev/null 2>&1; then
|
||||||
|
echo "pre-commit: 未找到 pnpm,跳过构建检查(请手动运行 pnpm run build 验证)" >&2
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
pnpm run build
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
# ============ OS / 系统垃圾 ============
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
desktop.ini
|
||||||
|
|
||||||
|
# ============ 编辑器 / IDE ============
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# ============ 工具本地数据 ============
|
||||||
|
# Claude Code 个人设置(共享设置可放 .claude/settings.json)
|
||||||
|
.claude/settings.local.json
|
||||||
|
# Serena MCP 工具数据
|
||||||
|
.serena/
|
||||||
|
|
||||||
|
# ============ 临时/备份 ============
|
||||||
|
*.bak
|
||||||
|
*.orig
|
||||||
|
*.tmp
|
||||||
|
*.log
|
||||||
Vendored
+18
@@ -0,0 +1,18 @@
|
|||||||
|
# VCS 触发器(只触发 main 分支)在 TeamCity UI / 项目 Kotlin DSL 中配置,
|
||||||
|
# YAML 格式仅描述 jobs 与 steps 结构。
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
BuildAndDeploy:
|
||||||
|
name: 构建并部署 Admin
|
||||||
|
steps:
|
||||||
|
- type: script
|
||||||
|
name: Docker Build
|
||||||
|
script-content: |
|
||||||
|
docker build \
|
||||||
|
-f deploy/Dockerfile \
|
||||||
|
-t zhican-admin:latest \
|
||||||
|
.
|
||||||
|
- type: script
|
||||||
|
name: Compose Up
|
||||||
|
script-content: |
|
||||||
|
docker compose -f deploy/compose.yaml up -d
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## 仓库结构
|
||||||
|
|
||||||
|
这是一个**多端单仓**项目,git 根目录下包含:
|
||||||
|
|
||||||
|
- `tcm-diagnosis.html` — 独立的中医诊断单页 HTML(与下面项目无依赖关系)
|
||||||
|
- `zhican/` — 智能餐养系统主项目,使用 npm workspaces 管理
|
||||||
|
- `admin/` — Vue 3 + Vite + TypeScript 后台管理端(workspace 成员)
|
||||||
|
- `miniprogram/` — 原生微信小程序(无构建步骤,由微信开发者工具直接打开 `project.config.json`)
|
||||||
|
|
||||||
|
注意:执行 npm 脚本前必须先 `cd zhican/`,根目录没有 `package.json`。
|
||||||
|
|
||||||
|
## 常用命令
|
||||||
|
|
||||||
|
**包管理器统一使用 pnpm**。`package-lock.json` 已被 `.gitignore`,禁止提交 npm/yarn 锁文件。仓库追踪的锁文件是 `zhican/admin/pnpm-lock.yaml`。
|
||||||
|
|
||||||
|
进入 `zhican/admin/` 目录执行(推荐):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm install # 安装依赖
|
||||||
|
pnpm run dev # 启动开发服务器(vite,端口 3000)
|
||||||
|
pnpm run build # 类型检查 + 构建(vue-tsc --noEmit && vite build),产物在 dist/
|
||||||
|
pnpm run preview # 本地预览构建产物
|
||||||
|
```
|
||||||
|
|
||||||
|
也可在 `zhican/` 根目录用 workspace 命令 `pnpm run admin:dev` / `admin:build`。
|
||||||
|
|
||||||
|
> 由于 `zhican/package.json` 用的是 npm workspaces 字段而非 `pnpm-workspace.yaml`,跨 workspace 命令的稳定性不如直接进 admin 子目录。一般操作建议进 `zhican/admin/`。
|
||||||
|
|
||||||
|
小程序端无构建步骤,使用微信开发者工具导入 `zhican/miniprogram/` 目录调试。
|
||||||
|
|
||||||
|
## 部署形态(H5 构建能力)
|
||||||
|
|
||||||
|
- **admin**:标准 Vite 项目,`npm run admin:build` 输出 `admin/dist/`(`index.html` + 静态 chunk),可直接部署到任意静态服务器/Nginx/对象存储
|
||||||
|
- **miniprogram**:**原生微信小程序,无法直接构建为 H5**。它使用 `.wxml` / `.wxss` / `wx.*` API 和 `app.json` tabBar,运行时绑定在微信宿主上。要得到浏览器可访问的 H5 版本必须迁移到 uni-app / Taro 等跨端框架重写,不是"加个构建脚本"就能完成
|
||||||
|
- **`tcm-diagnosis.html`**:单文件 HTML,无构建,直接打开或托管即可
|
||||||
|
|
||||||
|
## admin 后台架构
|
||||||
|
|
||||||
|
技术栈:Vue 3 (`<script setup>`) + Vite 6 + TypeScript + Pinia + vue-router 4 + Ant Design Vue 4。
|
||||||
|
|
||||||
|
### 自动导入与按需组件
|
||||||
|
|
||||||
|
`vite.config.ts` 配置了两个 unplugin:
|
||||||
|
|
||||||
|
- `unplugin-auto-import`:自动注入 `vue` / `vue-router` / `pinia` 的 API(`ref`、`useRouter`、`defineStore` 等无需手动 import),类型声明写入 `src/auto-imports.d.ts`
|
||||||
|
- `unplugin-vue-components` + `AntDesignVueResolver`:按需注册 ant-design-vue 组件,类型声明写入 `src/components.d.ts`(`importStyle: false`,依赖 antd 自带样式)
|
||||||
|
|
||||||
|
新增第三方库的全局 API 时需要在 `vite.config.ts` 的 `imports` 数组里登记,否则 TS 会报未定义。
|
||||||
|
|
||||||
|
### 路径与代理
|
||||||
|
|
||||||
|
- 别名 `@` → `src`(在 `vite.config.ts` 和 `tsconfig.json` 同步配置)
|
||||||
|
- dev server 代理 `/api` → `http://localhost:8080`,所以前端请求都写相对路径
|
||||||
|
- **vendor 拆分**:`build.rollupOptions.output.manualChunks` 仅把 vue 全家桶(vue/vue-router/pinia)单独打包成 `vue-vendor`,其余依赖(尤其是 ant-design-vue)维持按路由代码分割。**不要把 ant-design-vue 写进 manualChunks**——它体积约 1MB,强行单 chunk 会拖累首屏;按路由懒加载更优
|
||||||
|
|
||||||
|
### 路由组织(关键)
|
||||||
|
|
||||||
|
`src/router/index.ts` 采用**扁平注册**:所有业务页平铺在 `modules` 数组里,按注释分为 5 大业务域:
|
||||||
|
|
||||||
|
- 数据概览(dashboard / business-flow)
|
||||||
|
- 绿色种采(land-env、planting-monitor、harvest-storage、supplier-mgmt、procurement-mgmt)
|
||||||
|
- 卫生供应(hygiene-test、raw-storage、clean-process、clean-storage、clean-internal、clean-external)
|
||||||
|
- 健康生产(food-safety、recipe-plan、clean-order、clean-cabinet、clean-assemble、cook-by-order、serving-monitor、surplus-disposal、production-stats)
|
||||||
|
- 营养用餐(nutrition-test、food-composition、dish-inventory、algorithm-standard、recipe-mgmt、nutrition-order、user-mgmt、meal-line、pickup-mgmt、consumption-mgmt、nutrition-mgmt、ingredient-benefit、terminal-mgmt、model-mgmt、flavor-innovation)
|
||||||
|
- 系统管理(org-structure、employee-mgmt、role-mgmt、canteen-mgmt)
|
||||||
|
|
||||||
|
所有业务页都是 `MainLayout.vue` 的子路由,路径无前缀(`/dashboard` 而非 `/green-planting/land-env`)。每条路由通过 `meta.menuKey` 与侧边栏菜单联动,`meta.title` 用于 `document.title`。
|
||||||
|
|
||||||
|
**新增页面流程**:
|
||||||
|
1. 在 `src/views/<业务域>/<功能>/index.vue` 创建组件
|
||||||
|
2. 在 `src/router/index.ts` 的 `modules` 数组中追加一行(保持按业务域注释分组)
|
||||||
|
3. 同步在 `MainLayout.vue` 的菜单结构里加入对应 `menuKey`
|
||||||
|
|
||||||
|
### 路由守卫与登录
|
||||||
|
|
||||||
|
`router.beforeEach` 检查 `localStorage.token`,未登录访问非 login 页则重定向到 `/login`。token 由 `src/stores/user.ts` 管理。
|
||||||
|
|
||||||
|
### 数据来源现状(重要)
|
||||||
|
|
||||||
|
**当前项目是纯交互原型,没有任何真实后端调用**。各 view 全部使用 `src/mock/data.ts` 的静态数据驱动界面。原本预留的 `src/utils/request.ts`(axios 封装)和 `src/api/index.ts`(业务 API 聚合)已经清除——它们当时未被任何 view 引用,属于无主死代码。
|
||||||
|
|
||||||
|
登录流程也是原型:`router.beforeEach` 只检查 `localStorage.token` 是否存在,没有真正的鉴权请求。
|
||||||
|
|
||||||
|
**未来对接后端时**:建议沿用以下契约(这是被删除的脚手架原本约定的,可作为参考):
|
||||||
|
- 统一 axios 封装放 `src/utils/request.ts`,baseURL `/api`(配合 vite `server.proxy` 走 `localhost:8080`),自动注入 `Bearer token`
|
||||||
|
- 响应拦截器统一解包后端格式 `{ code, msg, data }`:`code === 0` 时直接返回 `data`,否则 `message.error(msg)` 并 reject;HTTP 401 清 token + 跳 `/login`
|
||||||
|
- API 按业务域聚合到 `src/api/<域>.ts`,view 里 `import { xxxApi } from '@/api/...'`,**不要**在 view 里直接 import `request`,也**不要**再 `.data.data` 解包(拦截器已经解一次)
|
||||||
|
|
||||||
|
### antd-vue 4.x 类型陷阱(踩坑警示)
|
||||||
|
|
||||||
|
`vue-tsc` 在严格模式下与 antd-vue 4.x 有几个高频冲突,新增页面时请提前避开:
|
||||||
|
|
||||||
|
1. **`a-table` 的 `columns` 必须显式注解**。`fixed: 'left' | 'right'` 字面量字段不加类型会被推为宽 `string` 类型,匹配不上 antd 的 `FixedType`。规范写法:
|
||||||
|
```ts
|
||||||
|
import type { ColumnsType } from 'ant-design-vue/es/table'
|
||||||
|
const columns: ColumnsType<any> = [ /* ... */ ]
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **`a-tree` 的 `tree-data` 即使配了 `field-names` 映射,静态类型也仍然要求 `DataNode` 形状(必须有 `key`)**。这是 antd-vue 类型限制,运行时 fieldNames 是有效的。已采用的解法:在绑定处加 `as any` 断言(`:tree-data="(myData as any)"`),不要去污染数据结构
|
||||||
|
|
||||||
|
3. **`a-tree` 的 `@select` 回调签名**是 `(keys: Key[], info) => void`,其中 `Key = string | number`。若声明 `(keys: string[]) => void` 会编译失败,需要 `Array<string | number>` 并按需 `String(keys[0])`
|
||||||
|
|
||||||
|
4. **`a-range-picker` / `a-date-picker` 的 `v-model` 不要用 `null` 初值**。antd 类型是 `[Dayjs, Dayjs] | undefined` 而非 `| null`,写 `ref<...>(null)` 会编译失败。规范写法:`const dateRange = ref<[Dayjs, Dayjs]>()`,表单字段用 `entryDate: undefined as Dayjs | undefined`
|
||||||
|
|
||||||
|
5. **`a-progress` 的 `format` 回调参数是 `percent?: number`**,必须接受 `undefined`。写 `(p: number) => ...` 会失败,应写 `(p?: number) => (p ?? 0) + '...'`
|
||||||
|
|
||||||
|
### 构建质量门禁
|
||||||
|
|
||||||
|
- **`vue-tsc --noEmit` 是构建管线的硬性步骤**(`pnpm run build` 会在 `vite build` 之前先跑),这意味着任何类型错误都会阻止构建。新增/修改代码后请本地跑一遍构建确认
|
||||||
|
- **git 钩子**:仓库根目录配置了 `.githooks/pre-commit`,当本次暂存的文件涉及 `zhican/admin/` 时自动运行 `pnpm run build`。**首次 clone 仓库后需要手动启用一次**:
|
||||||
|
```bash
|
||||||
|
cd <repo-root> && git config core.hooksPath .githooks
|
||||||
|
```
|
||||||
|
钩子若检测不到 `pnpm` 会跳过(不阻塞提交),但会在 stderr 提醒。Windows 用户需用 Git Bash 或确保 `bash` 可用
|
||||||
|
|
||||||
|
### 自动生成的类型声明(不要提交)
|
||||||
|
|
||||||
|
`src/auto-imports.d.ts` 和 `src/components.d.ts` 由 unplugin 在 dev/build 时**重新生成**,已加入 `.gitignore`。它们会随业务代码变化频繁更新,提交进 git 会反复制造无意义 diff 和合并冲突。如果发现这两个文件被意外提交,使用 `git rm --cached` 移除追踪。
|
||||||
|
|
||||||
|
## miniprogram 小程序
|
||||||
|
|
||||||
|
原生微信小程序结构(无构建链)。`app.json` 定义 4 个 tabBar 页面:吃 (`eat`) / 问 (`ask`) / 做 (`cook`) / 我 (`profile`),对应 `pages/<name>/index.{js,wxml,wxss,json}`。
|
||||||
|
|
||||||
|
小程序端目前也是纯交互原型,无任何真实接口调用。原本预留的 `utils/request.js` 同样属于无主死代码已清除。
|
||||||
|
|
||||||
|
## 业务上下文
|
||||||
|
|
||||||
|
「智能餐养系统」覆盖从种植采购到用餐消费的全链路:绿色种采 → 卫生供应(净菜加工) → 健康生产(按单烹制) → 营养用餐(点餐、取餐、营养分析)。理解某个页面的职责时优先按这条业务流向上下游追溯。
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# 构建上下文为仓库根目录
|
||||||
|
# docker build -f deploy/Dockerfile -t zhican-admin:latest .
|
||||||
|
|
||||||
|
# ────────────────────────────────────────────
|
||||||
|
# Stage 1: 构建阶段
|
||||||
|
# ────────────────────────────────────────────
|
||||||
|
FROM node:24-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 配置阿里 npm 镜像,加速依赖安装
|
||||||
|
RUN npm config set registry https://registry.npmmirror.com
|
||||||
|
|
||||||
|
# 安装 pnpm 并配置镜像
|
||||||
|
RUN npm install -g pnpm && pnpm config set registry https://registry.npmmirror.com
|
||||||
|
|
||||||
|
# 优先复制依赖描述文件,利用 Docker 层缓存
|
||||||
|
COPY zhican/admin/package.json zhican/admin/pnpm-lock.yaml ./
|
||||||
|
|
||||||
|
RUN pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
# 复制源码并构建
|
||||||
|
COPY zhican/admin/ .
|
||||||
|
RUN pnpm run build
|
||||||
|
|
||||||
|
# ────────────────────────────────────────────
|
||||||
|
# Stage 2: 成品阶段
|
||||||
|
# ────────────────────────────────────────────
|
||||||
|
FROM nginx:alpine AS runner
|
||||||
|
|
||||||
|
# 移除默认站点配置
|
||||||
|
RUN rm /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
# 注入自定义 nginx 配置
|
||||||
|
COPY deploy/nginx.conf /etc/nginx/conf.d/zhican-admin.conf
|
||||||
|
|
||||||
|
# 拷贝构建产物
|
||||||
|
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
services:
|
||||||
|
admin:
|
||||||
|
image: zhican-admin:latest
|
||||||
|
ports:
|
||||||
|
- "12801:80"
|
||||||
|
restart: unless-stopped
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
# Vue Router history 模式:所有未匹配的路径回落到 index.html
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Vite 构建产物带 hash,可安全设置长期缓存
|
||||||
|
location /assets/ {
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
}
|
||||||
|
|
||||||
|
# gzip 压缩
|
||||||
|
gzip on;
|
||||||
|
gzip_min_length 1024;
|
||||||
|
gzip_comp_level 6;
|
||||||
|
gzip_types
|
||||||
|
text/plain
|
||||||
|
text/css
|
||||||
|
text/javascript
|
||||||
|
application/javascript
|
||||||
|
application/json
|
||||||
|
application/xml
|
||||||
|
image/svg+xml;
|
||||||
|
|
||||||
|
# 基础安全头
|
||||||
|
add_header X-Content-Type-Options "nosniff";
|
||||||
|
add_header X-Frame-Options "SAMEORIGIN";
|
||||||
|
}
|
||||||
+32
-2
@@ -1,7 +1,37 @@
|
|||||||
|
# ============ 依赖 ============
|
||||||
node_modules
|
node_modules
|
||||||
|
# 统一使用 pnpm,禁止意外提交 npm/yarn 锁文件
|
||||||
|
package-lock.json
|
||||||
|
yarn.lock
|
||||||
|
|
||||||
|
# ============ 构建产物 ============
|
||||||
dist
|
dist
|
||||||
.DS_Store
|
dist-ssr
|
||||||
*.local
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# ============ 测试/覆盖率 ============
|
||||||
|
coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# ============ 日志 ============
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
# ============ 环境变量 ============
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
!.env.example
|
!.env.example
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# ============ 系统垃圾 ============
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# ============ unplugin 自动生成的类型声明 ============
|
||||||
|
# 这些文件由 unplugin-auto-import / unplugin-vue-components 在 dev/build 时重新生成
|
||||||
|
# 提交进 git 会反复制造无意义 diff,且容易合并冲突
|
||||||
|
admin/src/auto-imports.d.ts
|
||||||
|
admin/src/components.d.ts
|
||||||
|
|||||||
Generated
+2051
File diff suppressed because it is too large
Load Diff
@@ -1,53 +0,0 @@
|
|||||||
import request from '@/utils/request'
|
|
||||||
|
|
||||||
// 绿色种采 - 土地环境
|
|
||||||
export const landEnvApi = {
|
|
||||||
list: (params?: any) => request.get('/green-planting/land-env', { params }),
|
|
||||||
detail: (id: string) => request.get(`/green-planting/land-env/${id}`),
|
|
||||||
create: (data: any) => request.post('/green-planting/land-env', data),
|
|
||||||
update: (id: string, data: any) => request.put(`/green-planting/land-env/${id}`, data),
|
|
||||||
remove: (id: string) => request.delete(`/green-planting/land-env/${id}`),
|
|
||||||
}
|
|
||||||
|
|
||||||
// 卫生供应 - 卫生检测
|
|
||||||
export const hygieneTestApi = {
|
|
||||||
list: (params?: any) => request.get('/hygiene-supply/hygiene-test', { params }),
|
|
||||||
detail: (id: string) => request.get(`/hygiene-supply/hygiene-test/${id}`),
|
|
||||||
create: (data: any) => request.post('/hygiene-supply/hygiene-test', data),
|
|
||||||
}
|
|
||||||
|
|
||||||
// 健康生产 - 食安监控
|
|
||||||
export const foodSafetyApi = {
|
|
||||||
list: (params?: any) => request.get('/health-production/food-safety', { params }),
|
|
||||||
detail: (id: string) => request.get(`/health-production/food-safety/${id}`),
|
|
||||||
}
|
|
||||||
|
|
||||||
// 营养用餐 - 用户管理
|
|
||||||
export const userMgmtApi = {
|
|
||||||
list: (params?: any) => request.get('/nutrition-dining/users', { params }),
|
|
||||||
detail: (id: string) => request.get(`/nutrition-dining/users/${id}`),
|
|
||||||
create: (data: any) => request.post('/nutrition-dining/users', data),
|
|
||||||
update: (id: string, data: any) => request.put(`/nutrition-dining/users/${id}`, data),
|
|
||||||
remove: (id: string) => request.delete(`/nutrition-dining/users/${id}`),
|
|
||||||
}
|
|
||||||
|
|
||||||
// 系统管理 - 字典配置
|
|
||||||
export const dictApi = {
|
|
||||||
// 字典类型
|
|
||||||
typeList: (params?: any) => request.get('/system/dict-type', { params }),
|
|
||||||
typeCreate: (data: any) => request.post('/system/dict-type', data),
|
|
||||||
typeUpdate: (id: string, data: any) => request.put(`/system/dict-type/${id}`, data),
|
|
||||||
typeRemove: (id: string) => request.delete(`/system/dict-type/${id}`),
|
|
||||||
// 字典项
|
|
||||||
itemList: (typeCode: string, params?: any) => request.get(`/system/dict-item/${typeCode}`, { params }),
|
|
||||||
itemCreate: (data: any) => request.post('/system/dict-item', data),
|
|
||||||
itemUpdate: (id: string, data: any) => request.put(`/system/dict-item/${id}`, data),
|
|
||||||
itemRemove: (id: string) => request.delete(`/system/dict-item/${id}`),
|
|
||||||
}
|
|
||||||
|
|
||||||
// 登录
|
|
||||||
export const authApi = {
|
|
||||||
login: (data: { username: string; password: string }) => request.post('/auth/login', data),
|
|
||||||
logout: () => request.post('/auth/logout'),
|
|
||||||
profile: () => request.get('/auth/profile'),
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
import { ref } from 'vue'
|
|
||||||
|
|
||||||
// ==================== 通用工具 ====================
|
// ==================== 通用工具 ====================
|
||||||
export const randomId = () => Math.random().toString(36).slice(2, 10)
|
export const randomId = () => Math.random().toString(36).slice(2, 10)
|
||||||
export const randomNum = (min: number, max: number) => Math.floor(Math.random() * (max - min + 1)) + min
|
export const randomNum = (min: number, max: number) => Math.floor(Math.random() * (max - min + 1)) + min
|
||||||
@@ -376,8 +374,6 @@ export function genProductionStatsData() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 营养用餐 ====================
|
// ==================== 营养用餐 ====================
|
||||||
const nutrientNames = ['蛋白质', '脂肪', '碳水化合物', '膳食纤维', '维生素A', '维生素C', '钙', '铁', '锌']
|
|
||||||
|
|
||||||
export function genNutritionTestData(n = 12) {
|
export function genNutritionTestData(n = 12) {
|
||||||
return Array.from({ length: n }, () => ({
|
return Array.from({ length: n }, () => ({
|
||||||
id: randomId(),
|
id: randomId(),
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
import axios from 'axios'
|
|
||||||
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
|
|
||||||
import { message } from 'ant-design-vue'
|
|
||||||
|
|
||||||
const request: AxiosInstance = axios.create({
|
|
||||||
baseURL: '/api',
|
|
||||||
timeout: 15000,
|
|
||||||
})
|
|
||||||
|
|
||||||
request.interceptors.request.use(
|
|
||||||
(config) => {
|
|
||||||
const token = localStorage.getItem('token')
|
|
||||||
if (token) {
|
|
||||||
config.headers.Authorization = `Bearer ${token}`
|
|
||||||
}
|
|
||||||
return config
|
|
||||||
},
|
|
||||||
(error) => Promise.reject(error)
|
|
||||||
)
|
|
||||||
|
|
||||||
request.interceptors.response.use(
|
|
||||||
(response: AxiosResponse) => {
|
|
||||||
const { code, msg, data } = response.data
|
|
||||||
if (code === 0) {
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
message.error(msg || '请求失败')
|
|
||||||
return Promise.reject(new Error(msg))
|
|
||||||
},
|
|
||||||
(error) => {
|
|
||||||
if (error.response?.status === 401) {
|
|
||||||
localStorage.removeItem('token')
|
|
||||||
window.location.href = '/login'
|
|
||||||
}
|
|
||||||
message.error(error.message || '网络错误')
|
|
||||||
return Promise.reject(error)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
export default request
|
|
||||||
@@ -245,11 +245,9 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, reactive } from 'vue'
|
import { ref, computed, reactive } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import ChartPanel from '@/components/common/ChartPanel.vue'
|
import ChartPanel from '@/components/common/ChartPanel.vue'
|
||||||
import { genDashboardData } from '@/mock/data'
|
import { genDashboardData } from '@/mock/data'
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
const d = reactive(genDashboardData())
|
const d = reactive(genDashboardData())
|
||||||
const filterCanteen = ref('')
|
const filterCanteen = ref('')
|
||||||
const trendMetric = ref<'orders' | 'revenue'>('orders')
|
const trendMetric = ref<'orders' | 'revenue'>('orders')
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ const relatedLinks = [
|
|||||||
|
|
||||||
const data = ref(genHarvestData(15))
|
const data = ref(genHarvestData(15))
|
||||||
const searchText = ref('')
|
const searchText = ref('')
|
||||||
const dateRange = ref<[Dayjs, Dayjs] | null>(null)
|
const dateRange = ref<[Dayjs, Dayjs]>()
|
||||||
const statusFilter = ref<string | undefined>(undefined)
|
const statusFilter = ref<string | undefined>(undefined)
|
||||||
const modalVisible = ref(false)
|
const modalVisible = ref(false)
|
||||||
const currentRecord = ref<any>(null)
|
const currentRecord = ref<any>(null)
|
||||||
|
|||||||
@@ -224,9 +224,9 @@ const verifyColumns = [
|
|||||||
function orderStatusColor(s: string) { return s === 'pending' ? 'blue' : s === 'confirmed' ? 'cyan' : s === 'shipped' ? 'orange' : 'green' }
|
function orderStatusColor(s: string) { return s === 'pending' ? 'blue' : s === 'confirmed' ? 'cyan' : s === 'shipped' ? 'orange' : 'green' }
|
||||||
function orderStatusText(s: string) { return s === 'pending' ? '待确认' : s === 'confirmed' ? '已确认' : s === 'shipped' ? '已发货' : '已到货' }
|
function orderStatusText(s: string) { return s === 'pending' ? '待确认' : s === 'confirmed' ? '已确认' : s === 'shipped' ? '已发货' : '已到货' }
|
||||||
|
|
||||||
function confirmOrder(r: Order) { r.status = 'confirmed'; message.success('已确认') }
|
function confirmOrder(r: any) { r.status = 'confirmed'; message.success('已确认') }
|
||||||
function shipOrder(r: Order) { r.status = 'shipped'; message.success('已标记发货') }
|
function shipOrder(r: any) { r.status = 'shipped'; message.success('已标记发货') }
|
||||||
function arriveOrder(r: Order) {
|
function arriveOrder(r: any) {
|
||||||
r.status = 'arrived'
|
r.status = 'arrived'
|
||||||
const actual = r.quantity - randomNum(-5, 10)
|
const actual = r.quantity - randomNum(-5, 10)
|
||||||
verifyData.value.unshift({
|
verifyData.value.unshift({
|
||||||
@@ -237,8 +237,8 @@ function arriveOrder(r: Order) {
|
|||||||
message.success('已到货,请前往到货验收')
|
message.success('已到货,请前往到货验收')
|
||||||
}
|
}
|
||||||
|
|
||||||
function verifyPass(r: VerifyRecord) { r.verifyStatus = 'passed'; message.success('验收合格') }
|
function verifyPass(r: any) { r.verifyStatus = 'passed'; message.success('验收合格') }
|
||||||
function verifyFail(r: VerifyRecord) { r.verifyStatus = 'failed'; message.error('验收不合格') }
|
function verifyFail(r: any) { r.verifyStatus = 'failed'; message.error('验收不合格') }
|
||||||
|
|
||||||
function showOrderModal() {
|
function showOrderModal() {
|
||||||
orderForm.value = { supplier: undefined, material: undefined, quantity: 100, unitPrice: 10 }
|
orderForm.value = { supplier: undefined, material: undefined, quantity: 100, unitPrice: 10 }
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ import { ref, computed } from 'vue'
|
|||||||
import { message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
import type { Dayjs } from 'dayjs'
|
import type { Dayjs } from 'dayjs'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { randomId, randomNum, randomFloat, randomDate, pick } from '@/mock/data'
|
import { randomId, randomNum, randomDate, pick } from '@/mock/data'
|
||||||
import StatCard from '@/components/common/StatCard.vue'
|
import StatCard from '@/components/common/StatCard.vue'
|
||||||
|
|
||||||
interface Supplier {
|
interface Supplier {
|
||||||
@@ -205,7 +205,7 @@ const currentRecord = ref<Supplier | null>(null)
|
|||||||
const form = ref({
|
const form = ref({
|
||||||
name: '', ownership: 'own' as 'own' | 'external', supplierType: undefined as string | undefined,
|
name: '', ownership: 'own' as 'own' | 'external', supplierType: undefined as string | undefined,
|
||||||
contact: '', phone: '', address: '',
|
contact: '', phone: '', address: '',
|
||||||
licenseName: '', licenseExpiry: null as Dayjs | null, creditScore: 80,
|
licenseName: '', licenseExpiry: undefined as Dayjs | undefined, creditScore: 80,
|
||||||
})
|
})
|
||||||
|
|
||||||
const ownData = computed(() => allData.value.filter(s => s.ownership === 'own'))
|
const ownData = computed(() => allData.value.filter(s => s.ownership === 'own'))
|
||||||
@@ -256,23 +256,23 @@ function qualText(q: string) {
|
|||||||
function handleAdd() {
|
function handleAdd() {
|
||||||
isEdit.value = false
|
isEdit.value = false
|
||||||
currentRecord.value = null
|
currentRecord.value = null
|
||||||
form.value = { name: '', ownership: activeTab.value, supplierType: undefined, contact: '', phone: '', address: '', licenseName: '', licenseExpiry: null, creditScore: 80 }
|
form.value = { name: '', ownership: activeTab.value, supplierType: undefined, contact: '', phone: '', address: '', licenseName: '', licenseExpiry: undefined, creditScore: 80 }
|
||||||
modalVisible.value = true
|
modalVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleEdit(record: Supplier) {
|
function handleEdit(record: any) {
|
||||||
isEdit.value = true
|
isEdit.value = true
|
||||||
currentRecord.value = record
|
currentRecord.value = record
|
||||||
form.value = {
|
form.value = {
|
||||||
name: record.name, ownership: record.ownership, supplierType: record.supplierType,
|
name: record.name, ownership: record.ownership, supplierType: record.supplierType,
|
||||||
contact: record.contactPerson, phone: record.phone, address: record.address,
|
contact: record.contactPerson, phone: record.phone, address: record.address,
|
||||||
licenseName: record.licenseName, licenseExpiry: record.licenseExpiry ? dayjs(record.licenseExpiry) : null,
|
licenseName: record.licenseName, licenseExpiry: record.licenseExpiry ? dayjs(record.licenseExpiry) : undefined,
|
||||||
creditScore: record.creditScore,
|
creditScore: record.creditScore,
|
||||||
}
|
}
|
||||||
modalVisible.value = true
|
modalVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDetail(record: Supplier) {
|
function handleDetail(record: any) {
|
||||||
currentRecord.value = record
|
currentRecord.value = record
|
||||||
drawerVisible.value = true
|
drawerVisible.value = true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
v-if="record.status !== 'done'"
|
v-if="record.status !== 'done'"
|
||||||
type="primary"
|
type="primary"
|
||||||
size="small"
|
size="small"
|
||||||
@click="confirmAssemble(record)"
|
@click="confirmAssemble(record as AssembleItem)"
|
||||||
>确认组配</a-button>
|
>确认组配</a-button>
|
||||||
<span v-else style="color: #999">已完成</span>
|
<span v-else style="color: #999">已完成</span>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -58,7 +58,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { statusMap, randomId, randomNum, randomFloat, pick, pickMulti, randomTime } from '@/mock/data'
|
import { statusMap, randomId, randomNum, randomFloat, pickMulti, randomTime } from '@/mock/data'
|
||||||
|
|
||||||
const vegs = ['白菜', '萝卜', '番茄', '黄瓜', '茄子', '青椒', '芹菜', '菠菜']
|
const vegs = ['白菜', '萝卜', '番茄', '黄瓜', '茄子', '青椒', '芹菜', '菠菜']
|
||||||
|
|
||||||
|
|||||||
@@ -88,7 +88,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
import { genCookByOrderData, statusMap, canteenNames, randomNum } from '@/mock/data'
|
import { genCookByOrderData, canteenNames, randomNum } from '@/mock/data'
|
||||||
import StatCard from '@/components/common/StatCard.vue'
|
import StatCard from '@/components/common/StatCard.vue'
|
||||||
import RelatedModules from '@/components/common/RelatedModules.vue'
|
import RelatedModules from '@/components/common/RelatedModules.vue'
|
||||||
|
|
||||||
|
|||||||
@@ -89,7 +89,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, computed } from 'vue'
|
import { ref, reactive, computed } from 'vue'
|
||||||
import { genProductionStatsData, randomNum, randomFloat } from '@/mock/data'
|
import { genProductionStatsData, randomNum } from '@/mock/data'
|
||||||
import StatCard from '@/components/common/StatCard.vue'
|
import StatCard from '@/components/common/StatCard.vue'
|
||||||
import ChartPanel from '@/components/common/ChartPanel.vue'
|
import ChartPanel from '@/components/common/ChartPanel.vue'
|
||||||
|
|
||||||
|
|||||||
@@ -76,12 +76,12 @@
|
|||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
import type { Dayjs } from 'dayjs'
|
import type { Dayjs } from 'dayjs'
|
||||||
import { genSurplusData, statusMap } from '@/mock/data'
|
import { genSurplusData } from '@/mock/data'
|
||||||
import StatCard from '@/components/common/StatCard.vue'
|
import StatCard from '@/components/common/StatCard.vue'
|
||||||
import ChartPanel from '@/components/common/ChartPanel.vue'
|
import ChartPanel from '@/components/common/ChartPanel.vue'
|
||||||
|
|
||||||
const data = ref(genSurplusData(8))
|
const data = ref(genSurplusData(8))
|
||||||
const dateRange = ref<[Dayjs, Dayjs] | null>(null)
|
const dateRange = ref<[Dayjs, Dayjs]>()
|
||||||
const disposalFilter = ref<string | undefined>(undefined)
|
const disposalFilter = ref<string | undefined>(undefined)
|
||||||
const modalVisible = ref(false)
|
const modalVisible = ref(false)
|
||||||
const currentRecord = ref<any>(null)
|
const currentRecord = ref<any>(null)
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ const tableData = ref(genHygieneTestData(30))
|
|||||||
const searchText = ref('')
|
const searchText = ref('')
|
||||||
const testItemFilter = ref<string>()
|
const testItemFilter = ref<string>()
|
||||||
const resultFilter = ref<string>()
|
const resultFilter = ref<string>()
|
||||||
const dateRange = ref<[Dayjs, Dayjs] | null>(null)
|
const dateRange = ref<[Dayjs, Dayjs]>()
|
||||||
const detailVisible = ref(false)
|
const detailVisible = ref(false)
|
||||||
const currentRecord = ref<any>(null)
|
const currentRecord = ref<any>(null)
|
||||||
|
|
||||||
@@ -214,5 +214,5 @@ const reportColumns = [
|
|||||||
{ title: '操作', key: 'action', width: 80 },
|
{ title: '操作', key: 'action', width: 80 },
|
||||||
]
|
]
|
||||||
|
|
||||||
function showReportDetail(record: Report) { currentReport.value = record; reportDetailVisible.value = true }
|
function showReportDetail(record: any) { currentReport.value = record; reportDetailVisible.value = true }
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -57,7 +57,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import ChartPanel from '@/components/common/ChartPanel.vue'
|
import ChartPanel from '@/components/common/ChartPanel.vue'
|
||||||
import { randomNum, randomFloat, pick } from '@/mock/data'
|
import { randomNum, randomFloat } from '@/mock/data'
|
||||||
|
|
||||||
const benefitTab = ref('ingredient')
|
const benefitTab = ref('ingredient')
|
||||||
const materials = ['猪肉', '鸡肉', '大米', '青菜', '番茄', '土豆', '鸡蛋', '面粉']
|
const materials = ['猪肉', '鸡肉', '大米', '青菜', '番茄', '土豆', '鸡蛋', '面粉']
|
||||||
|
|||||||
@@ -132,7 +132,7 @@
|
|||||||
<a-descriptions :column="2" bordered size="small">
|
<a-descriptions :column="2" bordered size="small">
|
||||||
<a-descriptions-item label="姓名">{{ personProfile.name }}</a-descriptions-item>
|
<a-descriptions-item label="姓名">{{ personProfile.name }}</a-descriptions-item>
|
||||||
<a-descriptions-item label="部门">{{ personProfile.dept }}</a-descriptions-item>
|
<a-descriptions-item label="部门">{{ personProfile.dept }}</a-descriptions-item>
|
||||||
<a-descriptions-item label="日均热量">{{ personProfile.avgCalories }} kcal</a-descriptions-item>
|
<a-descriptions-item label="日均热量">{{ personProfile.calories }} kcal</a-descriptions-item>
|
||||||
<a-descriptions-item label="营养评分">
|
<a-descriptions-item label="营养评分">
|
||||||
<a-progress :percent="personProfile.score" :size="20"
|
<a-progress :percent="personProfile.score" :size="20"
|
||||||
:stroke-color="personProfile.score >= 80 ? '#52c41a' : personProfile.score >= 60 ? '#faad14' : '#f5222d'" />
|
:stroke-color="personProfile.score >= 80 ? '#52c41a' : personProfile.score >= 60 ? '#faad14' : '#f5222d'" />
|
||||||
@@ -188,7 +188,7 @@
|
|||||||
:percent="record.score"
|
:percent="record.score"
|
||||||
:size="20"
|
:size="20"
|
||||||
:stroke-color="record.score >= 80 ? '#52c41a' : record.score >= 60 ? '#faad14' : '#f5222d'"
|
:stroke-color="record.score >= 80 ? '#52c41a' : record.score >= 60 ? '#faad14' : '#f5222d'"
|
||||||
:format="(p: number) => p + ''"
|
:format="(p?: number) => (p ?? 0) + ''"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="column.key === 'verdict'">
|
<template v-else-if="column.key === 'verdict'">
|
||||||
@@ -295,7 +295,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, reactive } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import ChartPanel from '@/components/common/ChartPanel.vue'
|
import ChartPanel from '@/components/common/ChartPanel.vue'
|
||||||
import { randomNum, randomFloat, pick } from '@/mock/data'
|
import { randomNum, randomFloat, pick } from '@/mock/data'
|
||||||
|
|
||||||
@@ -312,19 +312,6 @@ const drawerPerson = ref<any>(null)
|
|||||||
const personList = ['张伟', '李芳', '王娜', '刘敏', '陈强', '杨磊', '黄艳', '赵勇', '周洋', '吴军']
|
const personList = ['张伟', '李芳', '王娜', '刘敏', '陈强', '杨磊', '黄艳', '赵勇', '周洋', '吴军']
|
||||||
const deptList = ['研发部', '市场部', '财务部', '行政部']
|
const deptList = ['研发部', '市场部', '财务部', '行政部']
|
||||||
|
|
||||||
// ============ 营养标准 ============
|
|
||||||
const standards = {
|
|
||||||
calories: { min: 1800, max: 2200, recommend: 2000 },
|
|
||||||
protein: { min: 55, max: 85, recommend: 70, unit: 'g' },
|
|
||||||
fat: { min: 45, max: 75, recommend: 60, unit: 'g' },
|
|
||||||
carbs: { min: 250, max: 340, recommend: 300, unit: 'g' },
|
|
||||||
fiber: { min: 20, max: 35, recommend: 25, unit: 'g' },
|
|
||||||
calcium: { min: 700, max: 1000, recommend: 800, unit: 'mg' },
|
|
||||||
iron: { min: 10, max: 20, recommend: 15, unit: 'mg' },
|
|
||||||
vitC: { min: 80, max: 150, recommend: 100, unit: 'mg' },
|
|
||||||
sodium: { min: 1000, max: 2300, recommend: 1500, unit: 'mg' },
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============ Mock 就餐数据 ============
|
// ============ Mock 就餐数据 ============
|
||||||
function genPersonNutrition() {
|
function genPersonNutrition() {
|
||||||
return personList.map(name => {
|
return personList.map(name => {
|
||||||
@@ -502,7 +489,7 @@ const mealBalance = computed(() => {
|
|||||||
// ============ 个人营养画像 ============
|
// ============ 个人营养画像 ============
|
||||||
const personProfile = computed(() => {
|
const personProfile = computed(() => {
|
||||||
const p = allPersonData.value.find(d => d.name === filterPerson.value)
|
const p = allPersonData.value.find(d => d.name === filterPerson.value)
|
||||||
return p || { name: '', dept: '', avgCalories: 0, score: 0, consecutiveDays: 0, bmi: 0 }
|
return p || { name: '', dept: '', calories: 0, score: 0, consecutiveDays: 0, bmi: 0 }
|
||||||
})
|
})
|
||||||
|
|
||||||
const personNutrientData = computed(() => {
|
const personNutrientData = computed(() => {
|
||||||
|
|||||||
@@ -150,10 +150,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, reactive } from 'vue'
|
import { ref, computed, reactive } from 'vue'
|
||||||
import { message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
|
import type { ColumnsType } from 'ant-design-vue/es/table'
|
||||||
import StatCard from '@/components/common/StatCard.vue'
|
import StatCard from '@/components/common/StatCard.vue'
|
||||||
import { canteenList } from '@/mock/data'
|
import { canteenList } from '@/mock/data'
|
||||||
|
|
||||||
const columns = [
|
const columns: ColumnsType<any> = [
|
||||||
{ title: '食堂名称', dataIndex: 'name', key: 'name', width: 110 },
|
{ title: '食堂名称', dataIndex: 'name', key: 'name', width: 110 },
|
||||||
{ title: '别名', dataIndex: 'alias', key: 'alias', width: 120 },
|
{ title: '别名', dataIndex: 'alias', key: 'alias', width: 120 },
|
||||||
{ title: '地址', dataIndex: 'address', key: 'address', width: 100 },
|
{ title: '地址', dataIndex: 'address', key: 'address', width: 100 },
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ const typeSearch = ref('')
|
|||||||
const selectedType = ref<DictType | null>(null)
|
const selectedType = ref<DictType | null>(null)
|
||||||
const typeModalVisible = ref(false)
|
const typeModalVisible = ref(false)
|
||||||
const editingType = ref<DictType | null>(null)
|
const editingType = ref<DictType | null>(null)
|
||||||
const typeForm = ref({ name: '', code: '', status: 'enabled' as const, remark: '' })
|
const typeForm = ref({ name: '', code: '', status: 'enabled' as 'enabled' | 'disabled', remark: '' })
|
||||||
|
|
||||||
const filteredTypes = computed(() => {
|
const filteredTypes = computed(() => {
|
||||||
const kw = typeSearch.value.toLowerCase()
|
const kw = typeSearch.value.toLowerCase()
|
||||||
@@ -168,7 +168,7 @@ function selectType(record: DictType) {
|
|||||||
selectedType.value = record
|
selectedType.value = record
|
||||||
}
|
}
|
||||||
|
|
||||||
function openTypeModal(record?: DictType) {
|
function openTypeModal(record?: any) {
|
||||||
editingType.value = record || null
|
editingType.value = record || null
|
||||||
typeForm.value = record
|
typeForm.value = record
|
||||||
? { name: record.name, code: record.code, status: record.status, remark: record.remark }
|
? { name: record.name, code: record.code, status: record.status, remark: record.remark }
|
||||||
@@ -202,7 +202,7 @@ function removeType(id: string) {
|
|||||||
const items = ref<DictItem[]>([...initItems])
|
const items = ref<DictItem[]>([...initItems])
|
||||||
const itemModalVisible = ref(false)
|
const itemModalVisible = ref(false)
|
||||||
const editingItem = ref<DictItem | null>(null)
|
const editingItem = ref<DictItem | null>(null)
|
||||||
const itemForm = ref({ label: '', value: '', sort: 0, status: 'enabled' as const, remark: '' })
|
const itemForm = ref({ label: '', value: '', sort: 0, status: 'enabled' as 'enabled' | 'disabled', remark: '' })
|
||||||
|
|
||||||
const currentItems = computed(() => {
|
const currentItems = computed(() => {
|
||||||
if (!selectedType.value) return []
|
if (!selectedType.value) return []
|
||||||
@@ -219,7 +219,7 @@ const itemColumns = [
|
|||||||
{ title: '操作', key: 'action', width: 120 },
|
{ title: '操作', key: 'action', width: 120 },
|
||||||
]
|
]
|
||||||
|
|
||||||
function openItemModal(record?: DictItem) {
|
function openItemModal(record?: any) {
|
||||||
editingItem.value = record || null
|
editingItem.value = record || null
|
||||||
itemForm.value = record
|
itemForm.value = record
|
||||||
? { label: record.label, value: record.value, sort: record.sort, status: record.status, remark: record.remark }
|
? { label: record.label, value: record.value, sort: record.sort, status: record.status, remark: record.remark }
|
||||||
@@ -247,7 +247,7 @@ function removeItem(id: string) {
|
|||||||
message.success('删除成功')
|
message.success('删除成功')
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleItemStatus(record: DictItem) {
|
function toggleItemStatus(record: any) {
|
||||||
record.status = record.status === 'enabled' ? 'disabled' : 'enabled'
|
record.status = record.status === 'enabled' ? 'disabled' : 'enabled'
|
||||||
message.success(`已${record.status === 'enabled' ? '启用' : '停用'}`)
|
message.success(`已${record.status === 'enabled' ? '启用' : '停用'}`)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -145,10 +145,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, reactive } from 'vue'
|
import { ref, computed, reactive } from 'vue'
|
||||||
import { message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
import dayjs from 'dayjs'
|
import dayjs, { type Dayjs } from 'dayjs'
|
||||||
|
import type { ColumnsType } from 'ant-design-vue/es/table'
|
||||||
import StatCard from '@/components/common/StatCard.vue'
|
import StatCard from '@/components/common/StatCard.vue'
|
||||||
|
|
||||||
const columns = [
|
const columns: ColumnsType<any> = [
|
||||||
{ title: '姓名', dataIndex: 'name', key: 'name', width: 100 },
|
{ title: '姓名', dataIndex: 'name', key: 'name', width: 100 },
|
||||||
{ title: '性别', dataIndex: 'gender', key: 'gender', width: 60 },
|
{ title: '性别', dataIndex: 'gender', key: 'gender', width: 60 },
|
||||||
{ title: '手机号', dataIndex: 'phone', key: 'phone', width: 120 },
|
{ title: '手机号', dataIndex: 'phone', key: 'phone', width: 120 },
|
||||||
@@ -183,13 +184,20 @@ const drawerVisible = ref(false)
|
|||||||
const editingId = ref('')
|
const editingId = ref('')
|
||||||
const viewingEmployee = ref<any>(null)
|
const viewingEmployee = ref<any>(null)
|
||||||
|
|
||||||
const form = reactive({
|
const form = reactive<{
|
||||||
|
name: string
|
||||||
|
gender: string
|
||||||
|
phone: string
|
||||||
|
department: string
|
||||||
|
position: string
|
||||||
|
entryDate: Dayjs | undefined
|
||||||
|
}>({
|
||||||
name: '',
|
name: '',
|
||||||
gender: '男',
|
gender: '男',
|
||||||
phone: '',
|
phone: '',
|
||||||
department: '',
|
department: '',
|
||||||
position: '',
|
position: '',
|
||||||
entryDate: null,
|
entryDate: undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
const activeEmployees = computed(() => allEmployees.value.filter(e => e.status === '在职'))
|
const activeEmployees = computed(() => allEmployees.value.filter(e => e.status === '在职'))
|
||||||
@@ -227,7 +235,7 @@ function showAddModal() {
|
|||||||
form.phone = ''
|
form.phone = ''
|
||||||
form.department = ''
|
form.department = ''
|
||||||
form.position = ''
|
form.position = ''
|
||||||
form.entryDate = null
|
form.entryDate = undefined
|
||||||
modalVisible.value = true
|
modalVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,7 +278,7 @@ function saveEmployee() {
|
|||||||
phone: form.phone,
|
phone: form.phone,
|
||||||
department: form.department,
|
department: form.department,
|
||||||
position: form.position,
|
position: form.position,
|
||||||
entryDate: form.entryDate?.format('YYYY-MM-DD'),
|
entryDate: form.entryDate?.format('YYYY-MM-DD') ?? '',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -281,7 +289,7 @@ function saveEmployee() {
|
|||||||
phone: form.phone,
|
phone: form.phone,
|
||||||
department: form.department,
|
department: form.department,
|
||||||
position: form.position,
|
position: form.position,
|
||||||
entryDate: form.entryDate?.format('YYYY-MM-DD'),
|
entryDate: form.entryDate?.format('YYYY-MM-DD') ?? '',
|
||||||
status: '试用期',
|
status: '试用期',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
/>
|
/>
|
||||||
<a-tree
|
<a-tree
|
||||||
v-model:selectedKeys="selectedOrgKeys"
|
v-model:selectedKeys="selectedOrgKeys"
|
||||||
:tree-data="orgTreeData"
|
:tree-data="(orgTreeData as any)"
|
||||||
:field-names="{ children: 'children', title: 'name', key: 'id' }"
|
:field-names="{ children: 'children', title: 'name', key: 'id' }"
|
||||||
@select="onOrgSelect"
|
@select="onOrgSelect"
|
||||||
>
|
>
|
||||||
@@ -212,9 +212,9 @@ const selectedOrgEmployees = computed(() => {
|
|||||||
return mockEmployees[selectedOrg.value.id] || []
|
return mockEmployees[selectedOrg.value.id] || []
|
||||||
})
|
})
|
||||||
|
|
||||||
function onOrgSelect(keys: string[]) {
|
function onOrgSelect(keys: Array<string | number>) {
|
||||||
if (keys.length === 0) return
|
if (keys.length === 0) return
|
||||||
const key = keys[0]
|
const key = String(keys[0])
|
||||||
const findOrg = (data: any[]): any => {
|
const findOrg = (data: any[]): any => {
|
||||||
for (const item of data) {
|
for (const item of data) {
|
||||||
if (item.id === key) return item
|
if (item.id === key) return item
|
||||||
@@ -236,11 +236,11 @@ function showAddOrgModal() {
|
|||||||
orgModalVisible.value = true
|
orgModalVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function editOrg(id: string) {
|
function editOrg(_id: string) {
|
||||||
message.info('编辑功能开发中')
|
message.info('编辑功能开发中')
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteOrg(id: string) {
|
function deleteOrg(_id: string) {
|
||||||
message.success('部门已删除')
|
message.success('部门已删除')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@
|
|||||||
</p>
|
</p>
|
||||||
<a-tree
|
<a-tree
|
||||||
v-model:checkedKeys="selectedPermissions"
|
v-model:checkedKeys="selectedPermissions"
|
||||||
:tree-data="permissionTree"
|
:tree-data="(permissionTree as any)"
|
||||||
:field-names="{ children: 'children', title: 'name', key: 'id' }"
|
:field-names="{ children: 'children', title: 'name', key: 'id' }"
|
||||||
checkable
|
checkable
|
||||||
@check="onPermissionChange"
|
@check="onPermissionChange"
|
||||||
@@ -69,7 +69,7 @@
|
|||||||
<a-form-item label="权限">
|
<a-form-item label="权限">
|
||||||
<a-tree
|
<a-tree
|
||||||
v-model:checkedKeys="roleForm.permissions"
|
v-model:checkedKeys="roleForm.permissions"
|
||||||
:tree-data="permissionTree"
|
:tree-data="(permissionTree as any)"
|
||||||
:field-names="{ children: 'children', title: 'name', key: 'id' }"
|
:field-names="{ children: 'children', title: 'name', key: 'id' }"
|
||||||
checkable
|
checkable
|
||||||
/>
|
/>
|
||||||
@@ -82,8 +82,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive } from 'vue'
|
import { ref, reactive } from 'vue'
|
||||||
import { message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
|
import type { ColumnsType } from 'ant-design-vue/es/table'
|
||||||
|
|
||||||
const roleColumns = [
|
const roleColumns: ColumnsType<any> = [
|
||||||
{ title: '角色名称', dataIndex: 'name', key: 'name', width: 120 },
|
{ title: '角色名称', dataIndex: 'name', key: 'name', width: 120 },
|
||||||
{ title: '描述', dataIndex: 'description', key: 'description', width: 150, ellipsis: true },
|
{ title: '描述', dataIndex: 'description', key: 'description', width: 150, ellipsis: true },
|
||||||
{ title: '用户数', dataIndex: 'userCount', key: 'userCount', width: 60 },
|
{ title: '用户数', dataIndex: 'userCount', key: 'userCount', width: 60 },
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
"target": "ES2022",
|
"target": "ES2022",
|
||||||
"lib": ["ES2023"],
|
"lib": ["ES2023"],
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"allowImportingTsExtensions": true,
|
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"moduleDetection": "force",
|
"moduleDetection": "force",
|
||||||
"noEmit": true,
|
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
|
|||||||
@@ -35,4 +35,16 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
build: {
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
// 拆分 vendor chunk
|
||||||
|
// 只提取所有路由都用到的"foundation"依赖,让 ant-design-vue 维持按路由代码分割
|
||||||
|
// —— antd 体积巨大(~1MB),强行打成单 chunk 反而拖累首屏;按路由 lazy 加载更优
|
||||||
|
manualChunks: {
|
||||||
|
'vue-vendor': ['vue', 'vue-router', 'pinia'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# 微信开发者工具个人设置(每个开发者机器各异)
|
||||||
|
project.private.config.json
|
||||||
|
|
||||||
|
# 小程序 npm 构建产物
|
||||||
|
miniprogram_npm/
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# 系统垃圾
|
||||||
|
.DS_Store
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
const app = getApp()
|
|
||||||
|
|
||||||
const request = (options) => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
wx.request({
|
|
||||||
url: app.globalData.baseUrl + options.url,
|
|
||||||
method: options.method || 'GET',
|
|
||||||
data: options.data || {},
|
|
||||||
header: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
Authorization: `Bearer ${app.globalData.token}`,
|
|
||||||
...options.header,
|
|
||||||
},
|
|
||||||
success(res) {
|
|
||||||
if (res.data.code === 0) {
|
|
||||||
resolve(res.data.data)
|
|
||||||
} else if (res.data.code === 401) {
|
|
||||||
wx.navigateTo({ url: '/pages/profile/index' })
|
|
||||||
reject(new Error('未登录'))
|
|
||||||
} else {
|
|
||||||
wx.showToast({ title: res.data.msg || '请求失败', icon: 'none' })
|
|
||||||
reject(new Error(res.data.msg))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
fail(err) {
|
|
||||||
wx.showToast({ title: '网络错误', icon: 'none' })
|
|
||||||
reject(err)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { request }
|
|
||||||
Generated
-3237
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user