修复 admin 构建门禁与类型错误,清理死代码

- tsconfig.node.json 启用 composite,让 vue-tsc --noEmit 真正生效
- 修复 24 处 antd-vue 4.x 类型错误(Table columns、Tree data、
  RangePicker null、Progress format 回调等)
- 删除未被引用的 axios 封装与 api 聚合(admin/utils/request.ts、
  admin/api/index.ts、miniprogram/utils/request.js)
- 新增 .githooks/pre-commit:admin 源码变更时自动 pnpm run build

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
SkyJourney
2026-04-08 17:24:29 +08:00
co-authored by Claude Opus 4.6
parent f9772b99cb
commit 077495a401
19 changed files with 64 additions and 164 deletions
+22
View File
@@ -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
-39
View File
@@ -1,39 +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 authApi = {
login: (data: { username: string; password: string }) => request.post('/auth/login', data),
logout: () => request.post('/auth/logout'),
profile: () => request.get('/auth/profile'),
}
+1 -5
View File
@@ -1,5 +1,3 @@
import { ref } from 'vue'
// ==================== 通用工具 ====================
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
@@ -40,7 +38,7 @@ const supplierNames = ['绿源农业', '丰禾食品', '天然牧场', '鲜美
const materialNames = ['有机大米', '土鸡蛋', '新鲜猪肉', '活鱼', '时令蔬菜', '有机牛奶', '散养鸡', '绿色面粉', '菜籽油', '食用盐']
export function genLandEnvData(n = 12) {
return Array.from({ length: n }, (_, i) => ({
return Array.from({ length: n }, () => ({
id: randomId(),
name: pick(landNames),
area: randomFloat(5, 200, 0) + '亩',
@@ -372,8 +370,6 @@ export function genProductionStatsData() {
}
// ==================== 营养用餐 ====================
const nutrientNames = ['蛋白质', '脂肪', '碳水化合物', '膳食纤维', '维生素A', '维生素C', '钙', '铁', '锌']
export function genNutritionTestData(n = 12) {
return Array.from({ length: n }, () => ({
id: randomId(),
-40
View File
@@ -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">
import { ref, computed, reactive } from 'vue'
import { useRouter } from 'vue-router'
import ChartPanel from '@/components/common/ChartPanel.vue'
import { genDashboardData } from '@/mock/data'
const router = useRouter()
const d = reactive(genDashboardData())
const filterCanteen = ref('')
const trendMetric = ref<'orders' | 'revenue'>('orders')
@@ -111,7 +111,7 @@ const relatedLinks = [
const data = ref(genHarvestData(15))
const searchText = ref('')
const dateRange = ref<[Dayjs, Dayjs] | null>(null)
const dateRange = ref<[Dayjs, Dayjs]>()
const statusFilter = ref<string | undefined>(undefined)
const modalVisible = ref(false)
const currentRecord = ref<any>(null)
@@ -114,7 +114,7 @@ const relatedLinks = [
]
const data = ref(genProcurementData(12))
const dateRange = ref<[Dayjs, Dayjs] | null>(null)
const dateRange = ref<[Dayjs, Dayjs]>()
const statusFilter = ref<string | undefined>(undefined)
const modalVisible = ref(false)
@@ -127,7 +127,7 @@ const form = ref({
quantity: 100,
unit: 'kg',
unitPrice: 0,
deliveryDate: null as Dayjs | null,
deliveryDate: undefined as Dayjs | undefined,
remark: '',
})
@@ -173,7 +173,7 @@ function showModal() {
}
function resetForm() {
form.value = { material: undefined, supplier: undefined, quantity: 100, unit: 'kg', unitPrice: 0, deliveryDate: null, remark: '' }
form.value = { material: undefined, supplier: undefined, quantity: 100, unit: 'kg', unitPrice: 0, deliveryDate: undefined, remark: '' }
}
function handleApprove(record: any) {
@@ -32,7 +32,7 @@
v-if="record.status !== 'done'"
type="primary"
size="small"
@click="confirmAssemble(record)"
@click="confirmAssemble(record as AssembleItem)"
>确认组配</a-button>
<span v-else style="color: #999">已完成</span>
</template>
@@ -58,7 +58,7 @@
<script setup lang="ts">
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 = ['白菜', '萝卜', '番茄', '黄瓜', '茄子', '青椒', '芹菜', '菠菜']
@@ -79,7 +79,7 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { genCookByOrderData, statusMap, canteenNames } from '@/mock/data'
import { genCookByOrderData, canteenNames } from '@/mock/data'
import StatCard from '@/components/common/StatCard.vue'
import RelatedModules from '@/components/common/RelatedModules.vue'
@@ -74,12 +74,12 @@
import { ref, computed } from 'vue'
import { message } from 'ant-design-vue'
import type { Dayjs } from 'dayjs'
import { genSurplusData, statusMap } from '@/mock/data'
import { genSurplusData } from '@/mock/data'
import StatCard from '@/components/common/StatCard.vue'
import ChartPanel from '@/components/common/ChartPanel.vue'
const data = ref(genSurplusData(8))
const dateRange = ref<[Dayjs, Dayjs] | null>(null)
const dateRange = ref<[Dayjs, Dayjs]>()
const disposalFilter = ref<string | undefined>(undefined)
const modalVisible = ref(false)
const currentRecord = ref<any>(null)
@@ -93,7 +93,7 @@ import { genHygieneTestData, statusMap } from '@/mock/data'
const tableData = ref(genHygieneTestData(30))
const searchText = ref('')
const resultFilter = ref('')
const dateRange = ref<[Dayjs, Dayjs] | null>(null)
const dateRange = ref<[Dayjs, Dayjs]>()
const drawerVisible = ref(false)
const currentRecord = ref<any>(null)
@@ -123,7 +123,7 @@
<a-descriptions :column="2" bordered size="small">
<a-descriptions-item label="姓名">{{ personProfile.name }}</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-progress :percent="personProfile.score" :size="20"
:stroke-color="personProfile.score >= 80 ? '#52c41a' : personProfile.score >= 60 ? '#faad14' : '#f5222d'" />
@@ -179,7 +179,7 @@
:percent="record.score"
:size="20"
:stroke-color="record.score >= 80 ? '#52c41a' : record.score >= 60 ? '#faad14' : '#f5222d'"
:format="(p: number) => p + ''"
:format="(p?: number) => (p ?? 0) + ''"
/>
</template>
<template v-else-if="column.key === 'verdict'">
@@ -251,7 +251,7 @@
</template>
<script setup lang="ts">
import { ref, computed, reactive } from 'vue'
import { ref, computed } from 'vue'
import ChartPanel from '@/components/common/ChartPanel.vue'
import { randomNum, randomFloat, pick } from '@/mock/data'
@@ -267,19 +267,6 @@ const drawerPerson = ref<any>(null)
const personList = ['张伟', '李芳', '王娜', '刘敏', '陈强', '杨磊', '黄艳', '赵勇', '周洋', '吴军']
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 就餐数据 ============
function genPersonNutrition() {
return personList.map(name => {
@@ -457,7 +444,7 @@ const mealBalance = computed(() => {
// ============ 个人营养画像 ============
const personProfile = computed(() => {
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(() => {
@@ -150,10 +150,11 @@
<script setup lang="ts">
import { ref, computed, reactive } from 'vue'
import { message } from 'ant-design-vue'
import type { ColumnsType } from 'ant-design-vue/es/table'
import StatCard from '@/components/common/StatCard.vue'
import { canteenList } from '@/mock/data'
const columns = [
const columns: ColumnsType<any> = [
{ title: '食堂名称', dataIndex: 'name', key: 'name', width: 110 },
{ title: '别名', dataIndex: 'alias', key: 'alias', width: 120 },
{ title: '地址', dataIndex: 'address', key: 'address', width: 100 },
@@ -145,10 +145,11 @@
<script setup lang="ts">
import { ref, computed, reactive } from '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'
const columns = [
const columns: ColumnsType<any> = [
{ title: '姓名', dataIndex: 'name', key: 'name', width: 100 },
{ title: '性别', dataIndex: 'gender', key: 'gender', width: 60 },
{ title: '手机号', dataIndex: 'phone', key: 'phone', width: 120 },
@@ -183,13 +184,20 @@ const drawerVisible = ref(false)
const editingId = ref('')
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: '',
gender: '男',
phone: '',
department: '',
position: '',
entryDate: null,
entryDate: undefined,
})
const activeEmployees = computed(() => allEmployees.value.filter(e => e.status === '在职'))
@@ -227,7 +235,7 @@ function showAddModal() {
form.phone = ''
form.department = ''
form.position = ''
form.entryDate = null
form.entryDate = undefined
modalVisible.value = true
}
@@ -270,7 +278,7 @@ function saveEmployee() {
phone: form.phone,
department: form.department,
position: form.position,
entryDate: form.entryDate?.format('YYYY-MM-DD'),
entryDate: form.entryDate?.format('YYYY-MM-DD') ?? '',
}
}
} else {
@@ -281,7 +289,7 @@ function saveEmployee() {
phone: form.phone,
department: form.department,
position: form.position,
entryDate: form.entryDate?.format('YYYY-MM-DD'),
entryDate: form.entryDate?.format('YYYY-MM-DD') ?? '',
status: '试用期',
})
}
@@ -15,7 +15,7 @@
/>
<a-tree
v-model:selectedKeys="selectedOrgKeys"
:tree-data="orgTreeData"
:tree-data="(orgTreeData as any)"
:field-names="{ children: 'children', title: 'name', key: 'id' }"
@select="onOrgSelect"
>
@@ -212,9 +212,9 @@ const selectedOrgEmployees = computed(() => {
return mockEmployees[selectedOrg.value.id] || []
})
function onOrgSelect(keys: string[]) {
function onOrgSelect(keys: Array<string | number>) {
if (keys.length === 0) return
const key = keys[0]
const key = String(keys[0])
const findOrg = (data: any[]): any => {
for (const item of data) {
if (item.id === key) return item
@@ -236,11 +236,11 @@ function showAddOrgModal() {
orgModalVisible.value = true
}
function editOrg(id: string) {
function editOrg(_id: string) {
message.info('编辑功能开发中')
}
function deleteOrg(id: string) {
function deleteOrg(_id: string) {
message.success('部门已删除')
}
@@ -46,7 +46,7 @@
</p>
<a-tree
v-model:checkedKeys="selectedPermissions"
:tree-data="permissionTree"
:tree-data="(permissionTree as any)"
:field-names="{ children: 'children', title: 'name', key: 'id' }"
checkable
@check="onPermissionChange"
@@ -69,7 +69,7 @@
<a-form-item label="权限">
<a-tree
v-model:checkedKeys="roleForm.permissions"
:tree-data="permissionTree"
:tree-data="(permissionTree as any)"
:field-names="{ children: 'children', title: 'name', key: 'id' }"
checkable
/>
@@ -82,8 +82,9 @@
<script setup lang="ts">
import { ref, reactive } from '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: 'description', key: 'description', width: 150, ellipsis: true },
{ title: '用户数', dataIndex: 'userCount', key: 'userCount', width: 60 },
+1 -2
View File
@@ -1,14 +1,13 @@
{
"compilerOptions": {
"composite": true,
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
-33
View File
@@ -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 }