fix: 前端表格行类型断言修复并补充 psycopg-pool 依赖

- 审批/项目/租户/用户设置等视图新增 asXxx 类型断言辅助函数
- search-fields 由字符串改为数组传参
- backend 依赖新增 psycopg-pool,Dockerfile 依赖校验同步更新
This commit is contained in:
wuyongtao
2026-08-03 11:02:41 +08:00
parent 24c77a990a
commit cc08b164d0
8 changed files with 63 additions and 26 deletions

View File

@@ -10,6 +10,7 @@ dependencies = [
"pydantic>=2.7.0", "pydantic>=2.7.0",
"sqlalchemy>=2.0.30", "sqlalchemy>=2.0.30",
"psycopg[binary]>=3.2.1", "psycopg[binary]>=3.2.1",
"psycopg-pool>=3.2.1",
"alembic>=1.13.1", "alembic>=1.13.1",
"redis>=5.0.4", "redis>=5.0.4",
"httpx>=0.27.0", "httpx>=0.27.0",

View File

@@ -4,6 +4,7 @@ python-multipart>=0.0.9
pydantic>=2.7.0 pydantic>=2.7.0
sqlalchemy>=2.0.30 sqlalchemy>=2.0.30
psycopg[binary]>=3.2.1 psycopg[binary]>=3.2.1
psycopg-pool>=3.2.1
alembic>=1.13.1 alembic>=1.13.1
redis>=5.0.4 redis>=5.0.4
httpx>=0.27.0 httpx>=0.27.0

View File

@@ -11,7 +11,7 @@ RUN pip install --upgrade pip -i https://pypi.tuna.tsinghua.edu.cn/simple \
&& pip install -r /tmp/requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple \ && pip install -r /tmp/requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple \
&& rm -f /tmp/requirements.txt && rm -f /tmp/requirements.txt
RUN python -c "import fastapi, uvicorn, psycopg, sqlalchemy, redis, jwt, passlib, httpx, alembic; print('backend dependency check ok')" RUN python -c "import fastapi, uvicorn, psycopg, psycopg_pool, sqlalchemy, redis, jwt, passlib, httpx, alembic; print('backend dependency check ok')"
RUN mkdir -p /opt/yg-ft/logs/backend /data/yg-ft \ RUN mkdir -p /opt/yg-ft/logs/backend /data/yg-ft \
&& chmod -R 0775 /opt/yg-ft /data/yg-ft && chmod -R 0775 /opt/yg-ft /data/yg-ft

View File

@@ -3,7 +3,8 @@ import { onMounted, reactive, ref } from 'vue'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import DataTablePage from '@/components/DataTablePage.vue' import DataTablePage from '@/components/DataTablePage.vue'
import { getApprovalInstances, decideApproval, type ApprovalInstance } from '@/api/modules/approval' import { getApprovalInstances, decideApproval, type ApprovalInstance } from '@/api/modules/approval'
import { getUsers, type SystemUser } from '@/api/modules/system' import { getUsers } from '@/api/modules/system'
import type { SystemUser } from '@/types'
const loading = ref(false) const loading = ref(false)
const instances = ref<ApprovalInstance[]>([]) const instances = ref<ApprovalInstance[]>([])
@@ -48,6 +49,10 @@ function openDecide(inst: ApprovalInstance) {
showDecide.value = true showDecide.value = true
} }
function asApprovalInstance(row: unknown): ApprovalInstance {
return row as ApprovalInstance
}
async function submitDecision() { async function submitDecision() {
if (!current.value) return if (!current.value) return
if (!decision.value.approver_id) { if (!decision.value.approver_id) {
@@ -72,7 +77,7 @@ onMounted(() => {
<template> <template>
<div class="page"> <div class="page">
<DataTablePage title="审批实例" :data="instances" :loading="loading" searchable search-fields="resource_type,resource_id"> <DataTablePage title="审批实例" :data="instances" :loading="loading" searchable :search-fields="['resource_type', 'resource_id']">
<template #toolbar-extra> <template #toolbar-extra>
<el-select v-model="statusFilter" placeholder="状态" clearable style="width: 140px" @change="load"> <el-select v-model="statusFilter" placeholder="状态" clearable style="width: 140px" @change="load">
<el-option v-for="s in statusOptions" :key="s.value" :label="s.label" :value="s.value" /> <el-option v-for="s in statusOptions" :key="s.value" :label="s.label" :value="s.value" />
@@ -82,14 +87,14 @@ onMounted(() => {
<el-table-column prop="resource_type" label="资源类型" min-width="120" /> <el-table-column prop="resource_type" label="资源类型" min-width="120" />
<el-table-column prop="resource_id" label="资源 ID" min-width="160" show-overflow-tooltip /> <el-table-column prop="resource_id" label="资源 ID" min-width="160" show-overflow-tooltip />
<el-table-column prop="applicant_id" label="申请人" min-width="120"> <el-table-column prop="applicant_id" label="申请人" min-width="120">
<template #default="{ row }">{{ userName(row.applicant_id) }}</template> <template #default="{ row }">{{ userName(asApprovalInstance(row).applicant_id) }}</template>
</el-table-column> </el-table-column>
<el-table-column prop="status" label="状态" min-width="100" /> <el-table-column prop="status" label="状态" min-width="100" />
<el-table-column prop="current_step" label="当前步骤" min-width="100" /> <el-table-column prop="current_step" label="当前步骤" min-width="100" />
<el-table-column prop="create_time" label="创建时间" min-width="180" /> <el-table-column prop="create_time" label="创建时间" min-width="180" />
</template> </template>
<template #actions="{ row }"> <template #actions="{ row }">
<el-button v-if="row.status === 'pending'" link type="primary" @click="openDecide(row)">审批</el-button> <el-button v-if="asApprovalInstance(row).status === 'pending'" link type="primary" @click="openDecide(asApprovalInstance(row))">审批</el-button>
</template> </template>
</DataTablePage> </DataTablePage>
<el-dialog v-model="showDecide" title="审批决策" width="480px"> <el-dialog v-model="showDecide" title="审批决策" width="480px">

View File

@@ -5,7 +5,8 @@ import { ElMessage } from 'element-plus'
import DataTablePage from '@/components/DataTablePage.vue' import DataTablePage from '@/components/DataTablePage.vue'
import AclDialog from '@/components/AclDialog.vue' import AclDialog from '@/components/AclDialog.vue'
import { getProject, getProjectMembers, addProjectMember, removeProjectMember, type Project, type ProjectMember } from '@/api/modules/project' import { getProject, getProjectMembers, addProjectMember, removeProjectMember, type Project, type ProjectMember } from '@/api/modules/project'
import { getUsers, type SystemUser } from '@/api/modules/system' import { getUsers } from '@/api/modules/system'
import type { SystemUser } from '@/types'
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
@@ -56,6 +57,10 @@ async function removeMember(userId: string) {
load() load()
} }
function asProjectMember(row: unknown): ProjectMember {
return row as ProjectMember
}
onMounted(() => { onMounted(() => {
loadUsers() loadUsers()
load() load()
@@ -94,7 +99,7 @@ onMounted(() => {
<el-table-column prop="create_time" label="加入时间" min-width="180" /> <el-table-column prop="create_time" label="加入时间" min-width="180" />
</template> </template>
<template #actions="{ row }"> <template #actions="{ row }">
<el-button link type="danger" @click="removeMember(row.user_id)">移除</el-button> <el-button link type="danger" @click="removeMember(asProjectMember(row).user_id)">移除</el-button>
</template> </template>
</DataTablePage> </DataTablePage>
</el-card> </el-card>

View File

@@ -41,6 +41,10 @@ function openDetail(id: string) {
router.push(`/projects/${id}`) router.push(`/projects/${id}`)
} }
function asProject(row: unknown): Project {
return row as Project
}
async function submitCreate() { async function submitCreate() {
if (!form.value.name || !form.value.code) { if (!form.value.name || !form.value.code) {
ElMessage.warning('请填写项目名与编码') ElMessage.warning('请填写项目名与编码')
@@ -61,7 +65,7 @@ onMounted(() => {
<template> <template>
<div class="page"> <div class="page">
<DataTablePage title="项目空间" :data="projects" :loading="loading" searchable search-fields="name,code"> <DataTablePage title="项目空间" :data="projects" :loading="loading" searchable :search-fields="['name', 'code']">
<template #toolbar-extra> <template #toolbar-extra>
<el-select v-model="tenantId" placeholder="租户" style="width: 160px" @change="load"> <el-select v-model="tenantId" placeholder="租户" style="width: 160px" @change="load">
<el-option v-for="t in tenantOptions" :key="t.value" :label="t.label" :value="t.value" /> <el-option v-for="t in tenantOptions" :key="t.value" :label="t.label" :value="t.value" />
@@ -76,7 +80,7 @@ onMounted(() => {
<el-table-column prop="create_time" label="创建时间" min-width="180" /> <el-table-column prop="create_time" label="创建时间" min-width="180" />
</template> </template>
<template #actions="{ row }"> <template #actions="{ row }">
<el-button link type="primary" @click="openDetail(row.id)">详情</el-button> <el-button link type="primary" @click="openDetail(asProject(row).id)">详情</el-button>
</template> </template>
</DataTablePage> </DataTablePage>
<el-dialog v-model="showCreate" title="新建项目" width="520px"> <el-dialog v-model="showCreate" title="新建项目" width="520px">

View File

@@ -54,6 +54,18 @@ function isSelf(row: SystemUser) {
return row.username === currentUsername.value return row.username === currentUsername.value
} }
function asSystemUser(row: unknown): SystemUser {
return row as SystemUser
}
function userPermissions(row: unknown): PermissionCode[] {
return (asSystemUser(row).permissions || []) as PermissionCode[]
}
function permissionLabel(code: PermissionCode): string {
return PERMISSION_LABELS[code] || code
}
// ---------- 启停 ---------- // ---------- 启停 ----------
async function toggleStatus(row: SystemUser, next: boolean) { async function toggleStatus(row: SystemUser, next: boolean) {
const nextStatus: UserStatus = next ? 'active' : 'disabled' const nextStatus: UserStatus = next ? 'active' : 'disabled'
@@ -162,31 +174,31 @@ async function removeUser(row: SystemUser) {
<el-table-column prop="role" label="角色" width="120" /> <el-table-column prop="role" label="角色" width="120" />
<el-table-column label="状态" width="130"> <el-table-column label="状态" width="130">
<template #default="{ row }"> <template #default="{ row }">
<el-tag :type="statusTagType(row.status)" size="small">{{ statusLabel(row.status) }}</el-tag> <el-tag :type="statusTagType(asSystemUser(row).status)" size="small">{{ statusLabel(asSystemUser(row).status) }}</el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="页面权限" min-width="160"> <el-table-column label="页面权限" min-width="160">
<template #default="{ row }"> <template #default="{ row }">
<el-tag <el-tag
v-for="p in (row.permissions || []).slice(0, 3)" v-for="p in userPermissions(row).slice(0, 3)"
:key="p" :key="p"
size="small" size="small"
type="info" type="info"
class="perm-tag" class="perm-tag"
>{{ PERMISSION_LABELS[p] || p }}</el-tag> >{{ permissionLabel(p) }}</el-tag>
<span v-if="(row.permissions || []).length > 3" class="perm-more"> <span v-if="userPermissions(row).length > 3" class="perm-more">
+{{ (row.permissions || []).length - 3 }} +{{ userPermissions(row).length - 3 }}
</span> </span>
<span v-if="!(row.permissions || []).length" class="perm-more"></span> <span v-if="!userPermissions(row).length" class="perm-more"></span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="create_time" label="创建时间" min-width="180" /> <el-table-column prop="create_time" label="创建时间" min-width="180" />
<el-table-column label="操作" width="260" fixed="right"> <el-table-column label="操作" width="260" fixed="right">
<template #default="{ row }"> <template #default="{ row }">
<el-switch <el-switch
:model-value="row.status === 'active'" :model-value="asSystemUser(row).status === 'active'"
:disabled="row.protected || isSelf(row)" :disabled="asSystemUser(row).protected || isSelf(asSystemUser(row))"
@change="(v: any) => toggleStatus(row, v)" @change="(v: any) => toggleStatus(asSystemUser(row), v)"
inline-prompt inline-prompt
active-text="启用" active-text="启用"
inactive-text="停用" inactive-text="停用"
@@ -194,19 +206,19 @@ async function removeUser(row: SystemUser) {
<el-button <el-button
link link
type="primary" type="primary"
:disabled="row.protected" :disabled="asSystemUser(row).protected"
@click="openResetPwd(row)" @click="openResetPwd(asSystemUser(row))"
>重置密码</el-button> >重置密码</el-button>
<el-button <el-button
link link
type="primary" type="primary"
@click="openPerms(row)" @click="openPerms(asSystemUser(row))"
>页面权限</el-button> >页面权限</el-button>
<el-button <el-button
link link
type="danger" type="danger"
:disabled="row.protected || isSelf(row)" :disabled="asSystemUser(row).protected || isSelf(asSystemUser(row))"
@click="removeUser(row)" @click="removeUser(asSystemUser(row))"
>删除</el-button> >删除</el-button>
</template> </template>
</el-table-column> </el-table-column>

View File

@@ -25,6 +25,15 @@ function openDetail(id: string) {
router.push(`/tenants/${id}`) router.push(`/tenants/${id}`)
} }
function asTenant(row: unknown): Tenant {
return row as Tenant
}
function quotaText(row: unknown): string {
const quota = asTenant(row).quota || {}
return Object.keys(quota).length ? JSON.stringify(quota) : '—'
}
async function submitCreate() { async function submitCreate() {
if (!form.value.name) { if (!form.value.name) {
ElMessage.warning('请填写租户名称') ElMessage.warning('请填写租户名称')
@@ -75,15 +84,15 @@ onMounted(load)
<el-table-column prop="code" label="编码" min-width="100" /> <el-table-column prop="code" label="编码" min-width="100" />
<el-table-column label="配额" min-width="160"> <el-table-column label="配额" min-width="160">
<template #default="{ row }"> <template #default="{ row }">
{{ Object.keys(row.quota || {}).length ? JSON.stringify(row.quota) : '—' }} {{ quotaText(row) }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="status" label="状态" min-width="100" /> <el-table-column prop="status" label="状态" min-width="100" />
<el-table-column prop="create_time" label="创建时间" min-width="180" /> <el-table-column prop="create_time" label="创建时间" min-width="180" />
</template> </template>
<template #actions="{ row }"> <template #actions="{ row }">
<el-button link type="primary" @click="openDetail(row.id)">详情</el-button> <el-button link type="primary" @click="openDetail(asTenant(row).id)">详情</el-button>
<el-button link type="primary" @click="setQuota(row)">配额</el-button> <el-button link type="primary" @click="setQuota(asTenant(row))">配额</el-button>
</template> </template>
</DataTablePage> </DataTablePage>
<el-dialog v-model="showCreate" title="新建租户" width="520px"> <el-dialog v-model="showCreate" title="新建租户" width="520px">