批量推送

This commit is contained in:
郭庆泰
2026-08-16 13:42:26 +08:00
parent c612d4297a
commit 0c093076a4
66 changed files with 4847 additions and 1627 deletions
+15
View File
@@ -91,3 +91,18 @@ export const bizGet = (entity, id) => request.get(`/business/${entity}/${id}`)
export const bizAdd = (entity, data) => request.post(`/business/${entity}`, data)
export const bizUpdate = (entity, data) => request.put(`/business/${entity}`, data)
export const bizDelete = (entity, ids) => request.delete(`/business/${entity}/${ids}`)
// 人员批量导入 (unitType: 'sponsor' | 'executor')
export const importPerson = (unitType, file) => {
const form = new FormData()
form.append('file', file)
return request.post(`/business/person/${unitType}Import`, form, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
// 人员导入模板下载
export const downloadImportTemplate = (unitType) =>
request.get(`/business/person/${unitType}ImportTemplate`, { responseType: 'blob' })
// 专家导入模板下载
export const downloadExpertTemplate = () =>
request.get('/business/expert/importTemplate', { responseType: 'blob' })
+63
View File
@@ -0,0 +1,63 @@
<template>
<!--
业务审核状态标签
通用封装, 后端 BizAuditStatusEnum 对应
0=未提交 1=待审核 2=通过 3=拒绝
-->
<el-tag
v-if="resolved"
:type="resolved.type"
:effect="effect"
:size="size"
>{{ resolved.text }}</el-tag>
<span v-else style="color:#c0c4cc">{{ emptyText }}</span>
</template>
<script setup>
import { computed } from 'vue'
/**
* 审核状态标签
*
* 用法:
* <audit-status-tag :status="row.auditStatus" />
* <audit-status-tag :status="row.auditStatus" size="small" effect="dark" />
*
* 兼容: 若传入旧值 ('审核通过'/'已退回'/'已驳回'/'待审核' 等中文), 自动归一化到新枚举
*/
const props = defineProps({
status: { type: [String, Number, null], default: null },
size: { type: String, default: 'small' }, // small / default / large
effect: { type: String, default: 'light' }, // light / dark / plain
emptyText: { type: String, default: '-' },
})
// 映射表: 后端 BizAuditStatusEnum 一致
const MAP = {
'0': { text: '未提交', type: 'info' },
'1': { text: '待审核', type: 'warning' },
'2': { text: '通过', type: 'success' },
'3': { text: '拒绝', type: 'danger' },
}
// 旧值兼容 (历史脏数据 / 老代码)
const LEGACY = {
'待审核': '1',
'审核通过': '2',
'通过': '2',
'已退回': '3',
'已驳回': '3',
'退回': '3',
'驳回': '3',
'拒绝': '3',
}
const resolved = computed(() => {
let s = props.status
if (s == null || s === '') return null
s = String(s).trim()
if (MAP[s]) return MAP[s]
if (LEGACY[s]) return MAP[LEGACY[s]]
return null
})
</script>
+26 -7
View File
@@ -7,17 +7,18 @@
placeholder="证书"
/>
block=true 时撑满父容器宽度 (用于表单行内全宽上传), 字体缩小到 14px
readonly=true 时不触发上传, 点击缩略图弹 el-image 预览
上传走 OSS 直传 (utils/oss.js uploadToOss)
-->
<template>
<div class="ht-image-upload-content" :class="{ 'has-file': modelValue }" @click="handleClick">
<div class="ht-image-upload-content" :class="{ 'has-file': modelValue, 'is-block': block }" @click="handleClick">
<div v-if="!modelValue && !readonly" class="ht-image-placeholder-wrap">
<span class="placeholder-text">{{ placeholder }}</span>
</div>
<div v-else-if="!modelValue && readonly" style="width: 90px;height: 60px; background: #f2f4f5;"></div>
<div v-else-if="!modelValue && readonly" :style="readonlyBoxStyle"></div>
<img v-else-if="!readonly" class="ht-image" :src="modelValue" />
<el-image v-else :src="modelValue" style="width: 90px;height: 60px; display: flex; justify-content: center; align-items: center; background: #f2f4f5;"
<el-image v-else :src="modelValue" :style="readonlyBoxStyle"
:preview-src-list="[modelValue]"></el-image>
</div>
<input
@@ -30,7 +31,7 @@
</template>
<script setup>
import { ref } from 'vue'
import { ref, computed } from 'vue'
import { ElMessage } from 'element-plus'
import { uploadToOss } from '@/utils/oss'
@@ -38,7 +39,9 @@ const props = defineProps({
modelValue: { type: String, default: '' },
dir: { type: String, default: 'ry8080/image/' },
placeholder: { type: String, default: '点击上传' },
readonly: { type: Boolean, default: false }
readonly: { type: Boolean, default: false },
/** true 时撑满父容器宽度, 用于表单行内全宽上传 */
block: { type: Boolean, default: false }
})
const emit = defineEmits(['update:modelValue'])
@@ -46,6 +49,13 @@ const emit = defineEmits(['update:modelValue'])
const fileInput = ref(null)
const uploading = ref(false)
const readonlyBoxStyle = computed(() => {
if (props.block) {
return 'width: 100%; height: 100px; background: #f2f4f5; display: flex; justify-content: center; align-items: center;'
}
return 'width: 90px; height: 60px; background: #f2f4f5; display: flex; justify-content: center; align-items: center;'
})
function handleClick() {
if (props.readonly || uploading.value) return
fileInput.value?.click()
@@ -90,6 +100,10 @@ async function onFileChange(e) {
cursor: pointer;
background: #fafafa;
}
.ht-image-upload-content.is-block {
width: 100%;
height: 100px;
}
.ht-image-upload-content:hover {
border-color: #1890ff;
}
@@ -105,11 +119,16 @@ async function onFileChange(e) {
justify-content: center;
}
.placeholder-text {
font-size: 24px;
font-weight: 300;
font-size: 14px;
font-weight: 400;
color: #8c8c8c;
line-height: 1;
}
.ht-image-upload-content.is-block .placeholder-text {
font-size: 14px;
font-weight: 400;
color: #8c8c8c;
}
.ht-image {
width: 100%;
height: 100%;
+4 -2
View File
@@ -61,7 +61,8 @@ const MENU = {
{ path: '/admin/department', title: '科室管理', icon: Folder },
{ path: '/admin/title', title: '职称管理', icon: Medal },
{ path: '/admin/experts', title: '专家管理', icon: UserFilled },
{ path: '/admin/orgs', title: '公司管理', icon: Connection },
{ path: '/admin/sponsor-orgs', title: '支持单位管理', icon: Connection },
{ path: '/admin/executor-orgs', title: '服务机构管理', icon: OfficeBuilding },
{ path: '/admin/account', title: '账号信息', icon: User }
],
leader: [
@@ -76,7 +77,8 @@ const MENU = {
{ path: '/manager/projects', title: '项目管理', icon: Document },
{ path: '/manager/meetings', title: '会议管理', icon: Calendar },
{ path: '/manager/experts', title: '专家审核', icon: User },
{ path: '/manager/orgs', title: '公司管理', icon: OfficeBuilding },
{ path: '/manager/sponsor-orgs', title: '支持单位管理', icon: Connection },
{ path: '/manager/executor-orgs', title: '服务机构管理', icon: OfficeBuilding },
{ path: '/manager/support-intent', title: '支持意向', icon: Tickets },
{ path: '/manager/exec-intent', title: '执行意向', icon: Tickets },
{ path: '/manager/accounts', title: '账号管理', icon: Setting }
+21
View File
@@ -28,6 +28,17 @@ const routes = [
{ path: 'department', name: 'admin-department', component: () => import('@/views/admin/Department.vue'), meta: { title: '科室管理' } },
{ path: 'title', name: 'admin-title', component: () => import('@/views/admin/Title.vue'), meta: { title: '职称管理' } },
{ path: 'experts', name: 'admin-experts', component: () => import('@/views/admin/Experts.vue'), meta: { title: '专家管理' } },
{ path: 'experts/new', name: 'admin-experts-new', component: () => import('@/views/admin/ExpertNew.vue'), meta: { title: '新建专家' } },
{ path: 'experts/edit/:id', name: 'admin-experts-edit', component: () => import('@/views/admin/ExpertNew.vue'), meta: { title: '编辑专家' } },
{ path: 'experts/view/:id', name: 'admin-experts-view', component: () => import('@/views/admin/ExpertDetail.vue'), meta: { title: '专家详情' } },
{ path: 'sponsor-orgs', name: 'admin-sponsor-orgs', component: () => import('@/views/admin/SponsorOrgs.vue'), meta: { title: '支持单位管理' } },
{ path: 'executor-orgs', name: 'admin-executor-orgs', component: () => import('@/views/admin/ExecutorOrgs.vue'), meta: { title: '服务机构管理' } },
{ path: 'sponsor-people', name: 'admin-sponsor-people', component: () => import('@/views/admin/SponsorPeople.vue'), meta: { title: '支持单位下人员' } },
{ path: 'sponsor-people/new', name: 'admin-sponsor-people-new', component: () => import('@/views/admin/SponsorPersonNew.vue'), meta: { title: '新建人员' } },
{ path: 'sponsor-people/edit/:id', name: 'admin-sponsor-people-edit', component: () => import('@/views/admin/SponsorPersonNew.vue'), meta: { title: '编辑人员' } },
{ path: 'executor-people', name: 'admin-executor-people', component: () => import('@/views/admin/ExecutorPeople.vue'), meta: { title: '服务机构下人员' } },
{ path: 'executor-people/new', name: 'admin-executor-people-new', component: () => import('@/views/admin/ExecutorPersonNew.vue'), meta: { title: '新建人员' } },
{ path: 'executor-people/edit/:id', name: 'admin-executor-people-edit', component: () => import('@/views/admin/ExecutorPersonNew.vue'), meta: { title: '编辑人员' } },
{ path: 'orgs', name: 'admin-orgs', component: () => import('@/views/admin/Orgs.vue'), meta: { title: '公司管理' } },
{ path: 'account', name: 'admin-account', component: () => import('@/views/admin/Account.vue'), meta: { title: '账号信息' } }
]
@@ -51,6 +62,14 @@ const routes = [
{ path: 'meetings', name: 'manager-meetings', component: () => import('@/views/manager/Meetings.vue'), meta: { title: '会议管理' } },
{ path: 'meetings/new', name: 'manager-meetings-new', component: () => import('@/views/manager/MeetingNew.vue'), meta: { title: '新建会议' } },
{ path: 'experts', name: 'manager-experts', component: () => import('@/views/manager/Experts.vue'), meta: { title: '专家审核' } },
{ path: 'sponsor-orgs', name: 'manager-sponsor-orgs', component: () => import('@/views/manager/SponsorOrgs.vue'), meta: { title: '支持单位管理' } },
{ path: 'executor-orgs', name: 'manager-executor-orgs', component: () => import('@/views/manager/ExecutorOrgs.vue'), meta: { title: '服务机构管理' } },
{ path: 'sponsor-people', name: 'manager-sponsor-people', component: () => import('@/views/manager/SponsorPeople.vue'), meta: { title: '支持单位下人员' } },
{ path: 'sponsor-people/new', name: 'manager-sponsor-people-new', component: () => import('@/views/manager/SponsorPersonNew.vue'), meta: { title: '新建人员' } },
{ path: 'sponsor-people/edit/:id', name: 'manager-sponsor-people-edit', component: () => import('@/views/manager/SponsorPersonNew.vue'), meta: { title: '编辑人员' } },
{ path: 'executor-people', name: 'manager-executor-people', component: () => import('@/views/manager/ExecutorPeople.vue'), meta: { title: '服务机构下人员' } },
{ path: 'executor-people/new', name: 'manager-executor-people-new', component: () => import('@/views/manager/ExecutorPersonNew.vue'), meta: { title: '新建人员' } },
{ path: 'executor-people/edit/:id', name: 'manager-executor-people-edit', component: () => import('@/views/manager/ExecutorPersonNew.vue'), meta: { title: '编辑人员' } },
{ path: 'orgs', name: 'manager-orgs', component: () => import('@/views/manager/Orgs.vue'), meta: { title: '公司管理' } },
{ path: 'support-intent', name: 'manager-support-intent', component: () => import('@/views/manager/SupportIntent.vue'), meta: { title: '支持意向' } },
{ path: 'exec-intent', name: 'manager-exec-intent', component: () => import('@/views/manager/ExecIntent.vue'), meta: { title: '执行意向' } },
@@ -76,6 +95,8 @@ const routes = [
{ path: 'meetings', name: 'executor-meetings', component: () => import('@/views/executor/Meetings.vue'), meta: { title: '会议执行' } },
{ path: 'projects', name: 'executor-projects', component: () => import('@/views/executor/Projects.vue'), meta: { title: '项目列表' } },
{ path: 'people', name: 'executor-people', component: () => import('@/views/executor/People.vue'), meta: { title: '人员管理' } },
{ path: 'people/new', name: 'executor-people-new', component: () => import('@/views/executor/NewPerson.vue'), meta: { title: '新建人员' } },
{ path: 'people/edit/:id', name: 'executor-people-edit', component: () => import('@/views/executor/NewPerson.vue'), meta: { title: '编辑人员' } },
{ path: 'account', name: 'executor-account', component: () => import('@/views/executor/Account.vue'), meta: { title: '账号信息' } },
{ path: 'labor', name: 'executor-labor', component: () => import('@/views/executor/Labor.vue'), meta: { title: '劳务凭证' } }
]
+47 -48
View File
@@ -1,53 +1,51 @@
<template>
<div class="admin-account">
<div class="page-card">
<div class="desc">查看当前登录账号修改个人信息与密码</div>
<div class="page-card admin-account">
<div class="breadcrumb">首页 / 账号信息</div>
<el-descriptions :column="2" border style="margin-bottom: 24px">
<el-descriptions-item label="账号">{{ profile.userName }}</el-descriptions-item>
<el-descriptions-item label="姓名">{{ profile.nickName || profile.userName }}</el-descriptions-item>
<el-descriptions-item label="角色">{{ roleLabel }}</el-descriptions-item>
<el-descriptions-item label="手机号">{{ profile.phonenumber || '-' }}</el-descriptions-item>
<el-descriptions-item label="邮箱">{{ profile.email || '-' }}</el-descriptions-item>
<el-descriptions-item label="最近登录">{{ profile.loginDate || '-' }}</el-descriptions-item>
</el-descriptions>
<el-descriptions :column="2" border style="margin-bottom: 24px">
<el-descriptions-item label="账号">{{ profile.userName }}</el-descriptions-item>
<el-descriptions-item label="姓名">{{ profile.nickName || profile.userName }}</el-descriptions-item>
<el-descriptions-item label="角色">{{ roleLabel }}</el-descriptions-item>
<el-descriptions-item label="手机号">{{ profile.phonenumber || '-' }}</el-descriptions-item>
<el-descriptions-item label="邮箱">{{ profile.email || '-' }}</el-descriptions-item>
<el-descriptions-item label="最近登录">{{ profile.loginDate || '-' }}</el-descriptions-item>
</el-descriptions>
<el-tabs v-model="tab">
<el-tab-pane label="修改资料" name="profile">
<el-form :model="form" label-width="100px" style="max-width: 480px">
<el-form-item label="姓名">
<el-input v-model="form.nickName" placeholder="请输入姓名" />
</el-form-item>
<el-form-item label="手机号">
<el-input v-model="form.phonenumber" placeholder="请输入手机号" />
</el-form-item>
<el-form-item label="邮箱">
<el-input v-model="form.email" placeholder="请输入邮箱" />
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="loading" @click="saveProfile">保存</el-button>
</el-form-item>
</el-form>
</el-tab-pane>
<el-tabs v-model="tab">
<el-tab-pane label="修改资料" name="profile">
<el-form :model="form" label-width="100px" class="account-form">
<el-form-item label="姓名">
<el-input v-model="form.nickName" placeholder="请输入姓名" />
</el-form-item>
<el-form-item label="手机号">
<el-input v-model="form.phonenumber" placeholder="请输入手机号" />
</el-form-item>
<el-form-item label="邮箱">
<el-input v-model="form.email" placeholder="请输入邮箱" />
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="loading" @click="saveProfile">保存</el-button>
</el-form-item>
</el-form>
</el-tab-pane>
<el-tab-pane label="修改密码" name="password">
<el-form :model="pwd" label-width="100px" style="max-width: 480px">
<el-form-item label="旧密码">
<el-input v-model="pwd.oldPassword" type="password" show-password placeholder="请输入旧密码" />
</el-form-item>
<el-form-item label="新密码">
<el-input v-model="pwd.newPassword" type="password" show-password placeholder="请输入新密码" />
</el-form-item>
<el-form-item label="确认新密码">
<el-input v-model="pwd.confirmPassword" type="password" show-password placeholder="请再次输入新密码" />
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="loading" @click="changePassword">保存</el-button>
</el-form-item>
</el-form>
</el-tab-pane>
</el-tabs>
</div>
<el-tab-pane label="修改密码" name="password">
<el-form :model="pwd" label-width="100px" class="account-form">
<el-form-item label="旧密码">
<el-input v-model="pwd.oldPassword" type="password" show-password placeholder="请输入旧密码" />
</el-form-item>
<el-form-item label="新密码">
<el-input v-model="pwd.newPassword" type="password" show-password placeholder="请输入新密码" />
</el-form-item>
<el-form-item label="确认新密码">
<el-input v-model="pwd.confirmPassword" type="password" show-password placeholder="请再次输入新密码" />
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="loading" @click="changePassword">保存</el-button>
</el-form-item>
</el-form>
</el-tab-pane>
</el-tabs>
</div>
</template>
@@ -107,7 +105,8 @@ onMounted(load)
</script>
<style scoped>
/* 整页面板 (与 People.vue 风格一致) */
.admin-account { padding: 16px; }
.page-card { background: #fff; border-radius: 4px; padding: 20px 24px; border: 1px solid #ebeef5; }
.desc { font-size: 13px; color: #909399; margin-bottom: 16px; padding-bottom: 12px; border-bottom: 1px solid #ebeef5; }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.account-form { max-width: 480px; }
</style>
+50 -52
View File
@@ -1,56 +1,54 @@
<template>
<div class="admin-department">
<div class="page-card">
<div class="desc">维护医生可用的科室字典停用的科室不会出现在医生选择下拉中</div>
<div class="page-card admin-department">
<div class="breadcrumb">首页 / 科室管理</div>
<el-form inline :model="q" class="filter-bar">
<el-form-item label="科室名称:">
<el-input v-model="q.name" placeholder="输入名称模糊搜索" clearable />
</el-form-item>
<el-form-item label="状态:">
<el-select v-model="q.status" placeholder="全部状态" clearable>
<el-option label="用" value="0" />
<el-option label="停用" value="1" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查询</el-button>
<el-button @click="reset">重置</el-button>
<el-button type="success" @click="openAdd">新增科室</el-button>
</el-form-item>
</el-form>
<!-- ========== 筛选区 ( People.vue 风格一致) ========== -->
<el-form inline :model="q" class="filter-form">
<el-form-item label="科室名称"><el-input v-model="q.name" placeholder="输入名称模糊搜索" clearable /></el-form-item>
<el-form-item label="状态">
<el-select v-model="q.status" placeholder="全部状态" clearable>
<el-option label="启用" value="0" />
<el-option label="用" value="1" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查询</el-button>
<el-button @click="reset">重置</el-button>
<el-button type="success" @click="openAdd">新增科室</el-button>
</el-form-item>
</el-form>
<el-table :data="rows" border stripe v-loading="loading">
<el-table-column prop="deptId" label="ID" width="80" />
<el-table-column prop="name" label="科室名称" min-width="200" />
<el-table-column prop="sort" label="排序" width="100" />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="row.status === '0' ? 'success' : 'info'">{{ row.status === '0' ? '启用' : '停用' }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="170" />
<el-table-column label="操作" width="180" fixed="right">
<template #default="{ row }">
<el-button size="small" link type="primary" @click="openEdit(row)">编辑</el-button>
<el-button size="small" link :type="row.status === '0' ? 'warning' : 'success'" @click="toggleStatus(row)">
{{ row.status === '0' ? '停用' : '启用' }}
</el-button>
<el-button size="small" link type="danger" @click="removeOne(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- ========== 表格 ( People.vue 风格一致, page-card 包裹) ========== -->
<el-table :data="rows" border stripe v-loading="loading">
<el-table-column prop="deptId" label="ID" width="80" />
<el-table-column prop="name" label="科室名称" min-width="200" />
<el-table-column prop="sort" label="排序" width="100" />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="row.status === '0' ? 'success' : 'info'">{{ row.status === '0' ? '启用' : '停用' }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="170" />
<el-table-column label="操作" width="220" fixed="right">
<template #default="{ row }">
<el-button size="small" link type="primary" @click="openEdit(row)">编辑</el-button>
<el-button size="small" link :type="row.status === '0' ? 'warning' : 'success'" @click="toggleStatus(row)">
{{ row.status === '0' ? '停用' : '启用' }}
</el-button>
<el-button size="small" link type="danger" @click="removeOne(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="pager">
<el-pagination
class="pager"
background
layout="total, prev, pager, next, sizes"
v-model:current-page="q.pageNum"
v-model:page-size="q.pageSize"
:total="total"
:page-sizes="[10, 20, 50]"
:page-size="q.pageSize"
:current-page="q.pageNum"
@current-change="(p) => { q.pageNum = p; load() }"
@size-change="(s) => { q.pageSize = s; q.pageNum = 1; load() }"
layout="total, sizes, prev, pager, next, jumper"
@current-change="load"
@size-change="load"
/>
</div>
@@ -92,8 +90,8 @@ async function load() {
loading.value = true
try {
const r = await listDepartments({ ...q })
rows.value = r.rows || []
total.value = r.total || 0
rows.value = (r.data && r.data.rows) || r.rows || []
total.value = (r.data && r.data.total) || r.total || 0
} finally { loading.value = false }
}
@@ -164,9 +162,9 @@ onMounted(load)
</script>
<style scoped>
/* 整页面板 (与 People.vue 风格一致) */
.admin-department { padding: 16px; }
.page-card { background: #fff; border-radius: 4px; padding: 20px 24px; border: 1px solid #ebeef5; }
.desc { font-size: 13px; color: #909399; margin-bottom: 16px; padding-bottom: 12px; border-bottom: 1px solid #ebeef5; }
.filter-bar { margin-bottom: 16px; }
.pager { margin-top: 16px; text-align: right; }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.filter-form { margin-bottom: 12px; }
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
</style>
+238
View File
@@ -0,0 +1,238 @@
<template>
<!--
角色: 🟦 后台 服务机构管理 (admin/executor-orgs)
说明: admin/Orgs.vue 拆出 executor 一支, 硬编码 orgType='executor'
接口: bizList('org', q) / bizAdd / bizUpdate
-->
<div class="page-card admin-executor-orgs">
<div class="breadcrumb">首页 / 服务机构管理</div>
<el-form inline :model="q" class="filter-form">
<el-form-item label="公司名称">
<el-input v-model="q.orgName" placeholder="输入公司名称" clearable style="width:200px" />
</el-form-item>
<el-form-item label="税号/统一社会信用代码">
<el-input v-model="q.taxNo" placeholder="输入税号" clearable style="width:220px" />
</el-form-item>
<el-form-item label="合作状态">
<el-select v-model="q.status" placeholder="请选择" clearable style="width:140px">
<el-option label="正常" value="正常" />
<el-option label="合作中" value="合作中" />
<el-option label="禁用" value="禁用" />
<el-option label="0正常" value="0" />
<el-option label="1禁用" value="1" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查询</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
<div class="batch-bar">
<el-button type="primary" @click="openAdd">新增服务机构</el-button>
</div>
<el-table :data="rows" v-loading="loading" stripe border>
<el-table-column type="index" label="#" width="50" />
<el-table-column prop="orgName" label="公司名称" min-width="200" show-overflow-tooltip />
<el-table-column prop="address" label="公司地址" min-width="220" show-overflow-tooltip />
<el-table-column prop="taxNo" label="税号/统一社会信用代码" min-width="180" />
<el-table-column label="合作状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="statusTagType(row.status)" disable-transitions>{{ row.status }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建日期" width="120" align="center">
<template #default="{ row }">{{ formatDate(row.createTime) }}</template>
</el-table-column>
<el-table-column prop="contactName" label="联系人" width="100" align="center" />
<el-table-column prop="contactPhone" label="联系电话" width="130" align="center" />
<el-table-column label="操作" width="240" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="onView(row)">查看</el-button>
<el-button link type="primary" @click="onEdit(row)">编辑</el-button>
<el-button link type="primary" @click="onPeople(row)">人员管理</el-button>
<el-button link :type="isDisabled(row) ? 'success' : 'danger'" @click="onToggleStatus(row)">
{{ isDisabled(row) ? '恢复' : '禁用' }}
</el-button>
</template>
</el-table-column>
</el-table>
<div class="pager">
<el-pagination
v-model:current-page="page.pageNum"
v-model:page-size="page.pageSize"
:total="page.total"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next, jumper"
@current-change="load"
@size-change="load"
/>
</div>
<el-dialog v-model="dialogVisible" :title="isEdit ? '编辑服务机构' : '新增服务机构'" width="640px">
<el-form :model="form" label-width="120px" :rules="rules" ref="formRef">
<el-form-item label="公司名称" prop="orgName">
<el-input v-model="form.orgName" />
</el-form-item>
<el-form-item label="公司地址">
<el-input v-model="form.address" />
</el-form-item>
<el-form-item label="税号/统一社会信用代码">
<el-input v-model="form.taxNo" />
</el-form-item>
<el-form-item label="联系人">
<el-input v-model="form.contactName" />
</el-form-item>
<el-form-item label="联系电话">
<el-input v-model="form.contactPhone" maxlength="11" />
</el-form-item>
<el-form-item v-if="isEdit" label="状态">
<el-select v-model="form.status" style="width:160px">
<el-option label="0正常" value="0" />
<el-option label="合作中" value="合作中" />
<el-option label="1禁用" value="1" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="saving" @click="onSave">保存</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { bizList, bizAdd, bizUpdate } from '@/api/public'
import { ElMessage, ElMessageBox } from 'element-plus'
const router = useRouter()
const q = reactive({ orgType: 'executor', orgName: '', taxNo: '', status: '' })
const rows = ref([])
const loading = ref(false)
const page = reactive({ pageNum: 1, pageSize: 10, total: 0 })
function statusTagType(s) {
if (s === '正常' || s === '0' || s === '合作中') return 'success'
if (s === '禁用' || s === '1') return 'danger'
return 'info'
}
function isDisabled(row) {
return row.status === '禁用' || row.status === '1'
}
function formatDate(d) {
if (!d) return ''
const str = String(d)
return str.length >= 10 ? str.slice(0, 10) : str
}
async function load() {
loading.value = true
try {
const { data } = await bizList('org', { ...q, pageNum: page.pageNum, pageSize: page.pageSize })
rows.value = (data && data.rows) || []
page.total = (data && data.total) || 0
} catch { rows.value = []; page.total = 0 }
finally { loading.value = false }
}
function reset() {
Object.assign(q, { orgType: 'executor', orgName: '', taxNo: '', status: '' })
page.pageNum = 1
load()
}
async function onToggleStatus(row) {
const disabled = isDisabled(row)
// executor 状态语义: 恢复 = '0', 禁用 = '1' (数字)
const newStatus = disabled ? '0' : '1'
const label = disabled ? '恢复' : '禁用'
if (!disabled) {
try {
await ElMessageBox.confirm(
`确定要${label}${row.orgName}」吗? ${label}后该企业下所有账号均不可登录`,
'确认',
{ type: 'warning' }
)
} catch { return }
}
try {
await bizUpdate('org', { orgId: row.orgId, status: newStatus })
ElMessage.success('已' + label)
load()
} catch (e) { ElMessage.error(e?.msg || '操作失败') }
}
const dialogVisible = ref(false)
const isEdit = ref(false)
const saving = ref(false)
const formRef = ref(null)
const form = reactive({
orgId: null, orgType: 'executor', orgName: '', address: '', taxNo: '',
contactName: '', contactPhone: '', status: '0'
})
const rules = {
orgName: [{ required: true, message: '请输入公司名称', trigger: 'blur' }]
}
function openAdd() {
isEdit.value = false
Object.assign(form, {
orgId: null, orgType: 'executor', orgName: '', address: '', taxNo: '',
contactName: '', contactPhone: '', status: '0'
})
dialogVisible.value = true
}
function onEdit(row) {
isEdit.value = true
Object.assign(form, { ...row, orgType: 'executor' })
dialogVisible.value = true
}
function onPeople(row) {
router.push({ path: '/admin/executor-people', query: { orgId: row.orgId, orgName: row.orgName } })
}
function onView(row) {
ElMessageBox.alert(
`类型: 执行方\n` +
`名称: ${row.orgName}\n` +
`地址: ${row.address || '-'}\n` +
`税号: ${row.taxNo || '-'}\n` +
`联系人: ${row.contactName || '-'} (${row.contactPhone || '-'})\n` +
`状态: ${row.status}\n` +
`创建日期: ${formatDate(row.createTime)}`,
'服务机构详情',
{ confirmButtonText: '关闭' }
).catch(() => {})
}
async function onSave() {
await formRef.value.validate()
saving.value = true
try {
if (isEdit.value) {
await bizUpdate('org', { ...form })
ElMessage.success('修改成功')
} else {
await bizAdd('org', { ...form })
ElMessage.success('新增成功')
}
dialogVisible.value = false
load()
} catch (e) { ElMessage.error(e?.msg || '保存失败') }
finally { saving.value = false }
}
onMounted(load)
</script>
<style scoped>
.admin-executor-orgs { padding: 16px; }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.filter-form { margin-bottom: 12px; }
.batch-bar { display: flex; gap: 8px; margin-bottom: 12px; align-items: center; }
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
</style>
+225
View File
@@ -0,0 +1,225 @@
<template>
<!--
角色: 🟦 后台 服务机构下人员 (admin/executor-people)
入口: org 表格行内人员管理按钮 (router.push orgId/orgName)
接口: bizList('person', q) / bizUpdate / bizAdd / executorImport / executorImportTemplate
-->
<div class="page-card admin-executor-people">
<div class="breadcrumb">
首页 / 服务机构管理 / 人员管理{{ orgName ? ' (' + orgName + ')' : '' }}
</div>
<el-form inline :model="q" class="filter-form">
<el-form-item label="姓名">
<el-input v-model="q.name" placeholder="姓名" clearable style="width:140px" />
</el-form-item>
<el-form-item label="手机号">
<el-input v-model="q.phone" placeholder="手机号" clearable style="width:140px" />
</el-form-item>
<el-form-item label="企业名称">
<el-input v-model="q.orgName" placeholder="企业名称" clearable style="width:180px" />
</el-form-item>
<el-form-item label="部门">
<el-input v-model="q.department" placeholder="部门" clearable style="width:140px" />
</el-form-item>
<el-form-item label="职务">
<el-input v-model="q.position" placeholder="职务" clearable style="width:140px" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查询</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
<div class="batch-bar">
<el-button type="primary" @click="goNew">新建人员</el-button>
<el-button @click="openImport">批量导入</el-button>
<span class="filter-tip">*批量导入模板与列表表项一致</span>
</div>
<el-table :data="rows" v-loading="loading" stripe border>
<el-table-column prop="name" label="姓名" min-width="100" />
<el-table-column prop="phone" label="手机号" min-width="130" />
<el-table-column prop="orgName" label="企业名称" min-width="180" show-overflow-tooltip />
<el-table-column prop="department" label="部门" min-width="100" />
<el-table-column prop="position" label="职务" min-width="100" />
<el-table-column label="状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="statusTagType(row.status)" disable-transitions>{{ row.status || '正常' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="120" fixed="right">
<template #default="{ row }">
<el-button link :type="isDisabled(row) ? 'success' : 'danger'" @click="onToggleStatus(row)">
{{ isDisabled(row) ? '启用' : '禁用' }}
</el-button>
</template>
</el-table-column>
</el-table>
<div class="pager">
<el-pagination
v-model:current-page="page.pageNum"
v-model:page-size="page.pageSize"
:total="page.total"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next, jumper"
@current-change="load"
@size-change="load"
/>
</div>
<el-dialog v-model="importOpen" title="批量导入" width="520px" :close-on-click-modal="false">
<el-form label-width="100px">
<el-form-item label="选择文件">
<el-upload ref="uploadRef" :auto-upload="false" :limit="1" accept=".xls,.xlsx" :on-change="onFileChange" :on-remove="onFileRemove">
<el-button>选择文件</el-button>
</el-upload>
</el-form-item>
<el-form-item label="">
<el-button link type="primary" @click="downloadTemplate">批量导入模板下载</el-button>
</el-form-item>
<el-form-item><span class="filter-tip">*导入列: 姓名/手机号/所属公司/部门/职务/角色</span></el-form-item>
</el-form>
<template #footer>
<el-button @click="importOpen = false">取消</el-button>
<el-button type="primary" :loading="importing" @click="submitImport">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { bizList, bizUpdate, importPerson, downloadImportTemplate } from '@/api/public'
import { ElMessage, ElMessageBox } from 'element-plus'
const route = useRoute()
const router = useRouter()
const orgId = computed(() => route.query.orgId ? Number(route.query.orgId) : null)
const orgName = computed(() => route.query.orgName || '')
const q = reactive({ name: '', phone: '', orgName: '', department: '', position: '' })
const rows = ref([])
const loading = ref(false)
const page = reactive({ pageNum: 1, pageSize: 10, total: 0 })
function statusTagType(s) {
if (s === '禁用' || s === '1') return 'danger'
return 'success'
}
function isDisabled(row) {
return row.status === '禁用' || row.status === '1'
}
async function load() {
loading.value = true
try {
const params = {
...q,
orgType: 'executor',
...(orgId.value ? { orgId: orgId.value } : {}),
pageNum: page.pageNum,
pageSize: page.pageSize,
}
const { data } = await bizList('person', params)
rows.value = (data && data.rows) || []
page.total = (data && data.total) || 0
} catch { rows.value = []; page.total = 0 }
finally { loading.value = false }
}
function reset() {
Object.assign(q, { name: '', phone: '', orgName: '', department: '', position: '' })
page.pageNum = 1
load()
}
async function onToggleStatus(row) {
const disabled = isDisabled(row)
const newStatus = disabled ? '正常' : '禁用'
const label = disabled ? '启用' : '禁用'
try {
await ElMessageBox.confirm(
`确定要${label}${row.name}」吗?`,
'确认',
{ type: 'warning' }
)
} catch { return }
try {
await bizUpdate('person', { personId: row.personId, status: newStatus })
ElMessage.success('已' + label)
load()
} catch (e) { ElMessage.error(e?.msg || '操作失败') }
}
// ========== 新建人员 ==========
function goNew() {
router.push({
path: '/admin/executor-people/new',
query: {
...(orgId.value ? { orgId: orgId.value } : {}),
...(orgName.value ? { orgName: orgName.value } : {})
}
})
}
// ========== 批量导入 ==========
const importOpen = ref(false)
const importing = ref(false)
const uploadRef = ref()
const importFile = ref(null)
function openImport() { importFile.value = null; importOpen.value = true }
function onFileChange(file) { importFile.value = file.raw }
function onFileRemove() { importFile.value = null }
async function downloadTemplate() {
try {
const res = await downloadImportTemplate('executor')
const blob = new Blob([res.data], { type: res.data.type || 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = '服务机构人员导入模板.xlsx'
a.click()
URL.revokeObjectURL(url)
} catch { ElMessage.error('模板下载失败') }
}
async function submitImport() {
if (!importFile.value) { ElMessage.warning('请先选择文件'); return }
importing.value = true
try {
const res = await importPerson('executor', importFile.value)
const payload = res.data || res
const ok = payload.ok ?? 0
const total = payload.total ?? 0
const failed = (payload.results || []).filter(r => !r.ok)
if (failed.length === 0) {
ElMessage.success(`导入成功 ${ok}/${total}`)
} else {
ElMessageBox.alert(
`成功 ${ok} 条, 失败 ${failed.length} 条:\n` +
failed.map(f => `${f.rowNo}${f.name || ''}: ${f.message}`).join('\n'),
'导入结果', { type: 'warning' }
)
}
importOpen.value = false
load()
} catch (e) {
ElMessage.error(e?.msg || '导入失败')
} finally {
importing.value = false
}
}
onMounted(load)
</script>
<style scoped>
.admin-executor-people { padding: 16px; }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.filter-form { margin-bottom: 12px; }
.batch-bar { display: flex; gap: 8px; margin-bottom: 12px; align-items: center; }
.filter-tip { color: #909399; font-size: 12px; }
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
</style>
+258
View File
@@ -0,0 +1,258 @@
<template>
<!--
专家详情独立页 (只读)
1:1 ExpertNew.vue 风格 (page-card + 面包屑 + form-card nested + 2 )
区别: 所有控件禁用, 仅展示
-->
<div class="page-card expert-detail">
<!-- 面包屑 -->
<div class="breadcrumb">
<a @click="goBack">专家管理</a> &gt; 专家详情
</div>
<!-- 表单卡片 -->
<el-card v-loading="loading" shadow="never" class="form-card nested">
<el-form :model="form" label-width="120px" disabled>
<!-- Row 1: 姓名 | 手机号 -->
<el-row :gutter="12">
<el-col :span="12">
<el-form-item label="姓名">
<el-input v-model="form.name" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="手机号">
<el-input v-model="form.phone" />
</el-form-item>
</el-col>
</el-row>
<!-- Row 2: 工作单位 | 科室 -->
<el-row :gutter="12">
<el-col :span="12">
<el-form-item label="工作单位">
<el-input v-model="form.workUnit" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="科室">
<el-input v-model="form.department" />
</el-form-item>
</el-col>
</el-row>
<!-- Row 3: 职称 | 地区 -->
<el-row :gutter="12">
<el-col :span="12">
<el-form-item label="职称">
<el-input v-model="form.title" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="地区">
<el-input v-model="form.region" />
</el-form-item>
</el-col>
</el-row>
<!-- Row 4: 执业医师证书 [span24] -->
<el-row :gutter="12">
<el-col :span="24">
<el-form-item label="执业医师证书">
<oss-image-uploader
v-model="form.practiceCertUrl"
dir="ry8080/expert/practice/"
placeholder="+"
readonly
/>
</el-form-item>
</el-col>
</el-row>
<!-- Row 5: 职称证明 [span24] -->
<el-row :gutter="12">
<el-col :span="24">
<el-form-item label="职称证明">
<oss-image-uploader
v-model="form.titleCertUrl"
dir="ry8080/expert/title/"
placeholder="+"
readonly
/>
</el-form-item>
</el-col>
</el-row>
<!-- Row 6: 身份证件号码 [span24] -->
<el-row :gutter="12">
<el-col :span="24">
<el-form-item label="身份证件号码">
<el-input v-model="form.idCard" />
</el-form-item>
</el-col>
</el-row>
<!-- Row 7: 身份证件照片 [span24] -->
<el-row :gutter="12">
<el-col :span="24">
<el-form-item label="身份证件照片">
<id-card-uploader
:front-url="form.idCardFrontUrl"
:back-url="form.idCardBackUrl"
dir="ry8080/expert/idcard/"
readonly
/>
</el-form-item>
</el-col>
</el-row>
<!-- Row 8: 银行卡号 | 银行名称 -->
<el-row :gutter="12">
<el-col :span="12">
<el-form-item label="银行卡号">
<el-input v-model="form.bankCard" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="银行名称">
<el-input v-model="form.bankName" />
</el-form-item>
</el-col>
</el-row>
<!-- Row 9: 开户行省份 | 开户行城市 -->
<el-row :gutter="12">
<el-col :span="12">
<el-form-item label="开户行省份">
<el-input v-model="form.bankProvince" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="开户行城市">
<el-input v-model="form.bankCity" />
</el-form-item>
</el-col>
</el-row>
<!-- Row 10: 开户行地址 [span24] -->
<el-row :gutter="12">
<el-col :span="24">
<el-form-item label="开户行地址">
<el-input v-model="form.bankAddress" />
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
<!-- 操作按钮 -->
<div class="form-actions">
<el-button @click="goBack">返回</el-button>
</div>
</div>
</template>
<script setup>
import { reactive, ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { bizGet } from '@/api/public'
import OssImageUploader from '@/components/OssImageUploader.vue'
import IdCardUploader from '@/components/IdCardUploader.vue'
const router = useRouter()
const route = useRoute()
const expertId = route.params.id
const loading = ref(false)
const form = reactive({
expertId: null,
name: '',
phone: '',
workUnit: '',
department: '',
title: '',
region: '',
practiceCertUrl: '',
titleCertUrl: '',
idCard: '',
idCardFrontUrl: '',
idCardBackUrl: '',
bankCard: '',
bankName: '',
bankProvince: '',
bankCity: '',
bankAddress: '',
})
function goBack() {
router.push('/admin/experts')
}
async function loadDetail() {
if (!expertId) {
ElMessage.error('缺少专家ID')
goBack()
return
}
loading.value = true
try {
const res = await bizGet('expert', expertId)
if (res?.code === 200 && res.data) {
const d = res.data
Object.assign(form, {
expertId: d.expertId,
name: d.name || '',
phone: d.phone || '',
workUnit: d.workUnit || '',
department: d.department || '',
title: d.title || '',
region: d.region || '',
practiceCertUrl: d.practiceCertUrl || '',
titleCertUrl: d.titleCertUrl || '',
idCard: d.idCard || '',
idCardFrontUrl: d.idCardFrontUrl || '',
idCardBackUrl: d.idCardBackUrl || '',
bankCard: d.bankCard || '',
bankName: d.bankName || '',
bankProvince: d.bankProvince || '',
bankCity: d.bankCity || '',
bankAddress: d.bankAddress || '',
})
} else {
ElMessage.error(res?.msg || '加载失败')
goBack()
}
} catch (e) {
ElMessage.error(e?.msg || e?.message || '加载失败')
goBack()
} finally {
loading.value = false
}
}
onMounted(loadDetail)
</script>
<style scoped>
/* 1:1 抄 ExpertNew.vue */
.expert-detail { max-width: 1200px; }
.page-card { background: #fff; padding: 16px 20px; border-radius: 6px; border: 1px solid #f0f0f0; }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 16px; }
.breadcrumb a { color: var(--brand-primary); cursor: pointer; text-decoration: none; }
.breadcrumb a:hover { color: var(--brand-primary-deep, var(--brand-primary)); }
.form-card.nested { background: transparent; border: none; box-shadow: none; }
:deep(.form-card .el-card__body) { padding: 24px 28px; }
.form-actions { margin-top: 16px; display: flex; gap: 8px; }
:deep(.el-button--primary) { background: var(--brand-primary); border-color: var(--brand-primary); border-radius: 4px; }
:deep(.el-button--primary:hover) { background: var(--brand-primary-deep, var(--brand-primary)); border-color: var(--brand-primary-deep, var(--brand-primary)); }
:deep(.el-button) { border-radius: 4px; }
:deep(.el-input__wrapper), :deep(.el-textarea__inner) { border-radius: 4px; }
/* 只读时禁用表单 input 鼠标样式, 保留视觉 */
:deep(.el-form.is-disabled .el-input__wrapper) { background-color: #f5f7fa; box-shadow: 0 0 0 1px #e4e7ed inset; }
</style>
+355
View File
@@ -0,0 +1,355 @@
<template>
<!--
1:1 sponsor/NewPerson.vue 风格
15 字段专家新建/编辑独立页
编辑/新建复用: 通过 route.params.id 区分
-->
<div class="page-card expert-new">
<!-- 面包屑 -->
<div class="breadcrumb">
<a @click="goBack">专家管理</a> &gt; {{ isEdit ? '编辑专家' : '新建专家' }}
</div>
<!-- 表单卡片 -->
<el-card v-loading="loadingDetail" shadow="never" class="form-card nested">
<el-form :model="form" :rules="rules" ref="formRef" label-width="120px">
<!-- Row 1: 姓名 | 手机号 -->
<el-row :gutter="12">
<el-col :span="12">
<el-form-item label="姓名" prop="name">
<el-input v-model="form.name" placeholder="请输入姓名" maxlength="50" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="手机号" prop="phone">
<el-input v-model="form.phone" placeholder="请输入手机号" maxlength="11" />
</el-form-item>
</el-col>
</el-row>
<!-- Row 2: 工作单位 | 科室 -->
<el-row :gutter="12">
<el-col :span="12">
<el-form-item label="工作单位" prop="workUnit">
<el-input v-model="form.workUnit" placeholder="请输入工作单位" maxlength="200" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="科室" prop="department">
<doctor-dept-select
v-model="form.department"
value-field="label"
placeholder="输入关键词搜索"
filterable
style="width:100%"
/>
</el-form-item>
</el-col>
</el-row>
<!-- Row 3: 职称 | 地区 -->
<el-row :gutter="12">
<el-col :span="12">
<el-form-item label="职称" prop="title">
<doctor-title-select
v-model="form.title"
value-field="label"
placeholder="输入关键词搜索"
filterable
style="width:100%"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="地区">
<area-cascader
v-model="form.region"
format="string"
join-sep="/"
placeholder="请选择省/市/区"
style="width:100%"
/>
</el-form-item>
</el-col>
</el-row>
<!-- Row 4: 执业医师证书 [span24] -->
<el-row :gutter="12">
<el-col :span="24">
<el-form-item label="执业医师证书">
<oss-image-uploader
v-model="form.practiceCertUrl"
dir="ry8080/expert/practice/"
placeholder="+"
/>
</el-form-item>
</el-col>
</el-row>
<!-- Row 5: 职称证明 [span24] -->
<el-row :gutter="12">
<el-col :span="24">
<el-form-item label="职称证明">
<oss-image-uploader
v-model="form.titleCertUrl"
dir="ry8080/expert/title/"
placeholder="+"
/>
</el-form-item>
</el-col>
</el-row>
<!-- Row 6: 身份证件号码 [span24] -->
<el-row :gutter="12">
<el-col :span="24">
<el-form-item label="身份证件号码">
<el-input v-model="form.idCard" placeholder="请输入身份证件号码" maxlength="18" />
</el-form-item>
</el-col>
</el-row>
<!-- Row 7: 身份证件照片 [span24] -->
<el-row :gutter="12">
<el-col :span="24">
<el-form-item label="身份证件照片">
<id-card-uploader
v-model:front-url="form.idCardFrontUrl"
v-model:back-url="form.idCardBackUrl"
dir="ry8080/expert/idcard/"
/>
</el-form-item>
</el-col>
</el-row>
<!-- Row 8: 银行卡号 | 银行名称 -->
<el-row :gutter="12">
<el-col :span="12">
<el-form-item label="银行卡号">
<el-input v-model="form.bankCard" placeholder="请输入银行卡号" maxlength="19" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="银行名称">
<el-input v-model="form.bankName" placeholder="如: 中国工商银行" maxlength="50" />
</el-form-item>
</el-col>
</el-row>
<!-- Row 9: 开户行省份 | 开户行城市 -->
<el-row :gutter="12">
<el-col :span="12">
<el-form-item label="开户行省份">
<el-input v-model="form.bankProvince" placeholder="如: 北京市" maxlength="50" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="开户行城市">
<el-input v-model="form.bankCity" placeholder="如: 北京市" maxlength="50" />
</el-form-item>
</el-col>
</el-row>
<!-- Row 10: 开户行地址 [span24] -->
<el-row :gutter="12">
<el-col :span="24">
<el-form-item label="开户行地址">
<el-input v-model="form.bankAddress" placeholder="请输入开户行详细地址" maxlength="200" />
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
<!-- 操作按钮 -->
<div class="form-actions">
<el-button type="primary" :loading="saving" @click="onSave">保存</el-button>
<el-button @click="confirmCancel">取消</el-button>
</div>
</div>
</template>
<script setup>
import { reactive, ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { bizAdd, bizUpdate, bizGet } from '@/api/public'
import { useUserStore } from '@/store/user'
import DoctorTitleSelect from '@/components/DoctorTitleSelect.vue'
import DoctorDeptSelect from '@/components/DoctorDeptSelect.vue'
import AreaCascader from '@/components/AreaCascader.vue'
import IdCardUploader from '@/components/IdCardUploader.vue'
import OssImageUploader from '@/components/OssImageUploader.vue'
const router = useRouter()
const route = useRoute()
const userStore = useUserStore()
const formRef = ref(null)
const saving = ref(false)
const loadingDetail = ref(false)
const isEdit = !!route.params.id
const expertId = route.params.id || null
const form = reactive({
expertId: null,
name: '',
phone: '',
workUnit: '',
department: '',
title: '',
region: '',
practiceCertUrl: '',
titleCertUrl: '',
idCard: '',
idCardFrontUrl: '',
idCardBackUrl: '',
bankCard: '',
bankName: '',
bankProvince: '',
bankCity: '',
bankAddress: '',
})
// 5 必填字段 + 手机号正则 (项目标准)
const rules = {
name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
phone: [
{ required: true, message: '请输入手机号', trigger: 'blur' },
{ pattern: /^1[0-9]\d{9}$/, message: '手机号格式不正确', trigger: 'blur' },
],
workUnit: [{ required: true, message: '请输入工作单位', trigger: 'blur' }],
department:[{ required: true, message: '请选择科室', trigger: 'change' }],
title: [{ required: true, message: '请选择职称', trigger: 'change' }],
}
function goBack() {
router.push('/admin/experts')
}
async function loadDetail() {
if (!expertId) return
loadingDetail.value = true
try {
const res = await bizGet('expert', expertId)
if (res?.code === 200 && res.data) {
const d = res.data
Object.assign(form, {
expertId: d.expertId,
name: d.name || '',
phone: d.phone || '',
workUnit: d.workUnit || '',
department: d.department || '',
title: d.title || '',
region: d.region || '',
practiceCertUrl: d.practiceCertUrl || '',
titleCertUrl: d.titleCertUrl || '',
idCard: d.idCard || '',
idCardFrontUrl: d.idCardFrontUrl || '',
idCardBackUrl: d.idCardBackUrl || '',
bankCard: d.bankCard || '',
bankName: d.bankName || '',
bankProvince: d.bankProvince || '',
bankCity: d.bankCity || '',
bankAddress: d.bankAddress || '',
})
} else {
ElMessage.error(res?.msg || '加载失败')
goBack()
}
} catch (e) {
ElMessage.error(e?.msg || e?.message || '加载失败')
goBack()
} finally {
loadingDetail.value = false
}
}
function confirmCancel() {
// 检查表单是否有内容 (新建时只要 form.name 即可)
const hasContent = form.name || form.phone || form.workUnit || form.department || form.title
if (!isEdit && !hasContent) { goBack(); return }
ElMessageBox.confirm('确定取消?未保存的内容将丢失', '提示', { type: 'warning' })
.then(() => goBack())
.catch(() => {})
}
// 业务"重复/已存在"提示用 warning 黄 (memory 规则)
function isDuplicateError(msg) {
return /已存在|重复|已注册|duplicate|uk_expert_phone/i.test(msg || '')
}
async function onSave() {
try {
await formRef.value.validate()
} catch {
ElMessage.warning('请填写必填项')
return
}
saving.value = true
try {
const payload = { ...form }
if (isEdit) {
// 编辑: 不改 expertId 主键, 也不重置 auditStatus
delete payload.expertId
} else {
// 新建: admin 创建, 自动通过审核 (BizAuditStatusEnum.APPROVED='2'), 状态正常
payload.auditStatus = '2'
payload.auditBy = userStore.user?.userName || userStore.user?.nickName || 'admin'
payload.auditTime = new Date().toISOString().slice(0, 19).replace('T', ' ')
payload.status = 'Y'
}
const res = isEdit
? await bizUpdate('expert', payload)
: await bizAdd('expert', payload)
if (res?.code === 200) {
if (!isEdit && res.data) {
// 新建成功: 后端同时创建 sys_user, 提示账号信息
const u = res.data
ElMessageBox.alert(
`专家账号已创建\n账号: ${u.userName || form.phone}\n初始密码: ${u.password || form.phone}\n请记录后告知使用人 (密码仅本次显示)`,
'创建成功',
{ type: 'success', confirmButtonText: '我已记录, 返回' }
).then(() => goBack())
.catch(() => goBack())
} else {
ElMessage.success(isEdit ? '修改成功' : '新建成功')
goBack()
}
} else {
const msg = res?.msg || '保存失败'
if (isDuplicateError(msg)) ElMessage.warning('该手机号已注册,请更换')
else ElMessage.error(msg)
}
} catch (e) {
const msg = e?.msg || e?.message || '保存失败'
if (isDuplicateError(msg)) ElMessage.warning('该手机号已注册,请更换')
else ElMessage.error(msg)
} finally {
saving.value = false
}
}
onMounted(() => {
if (isEdit) loadDetail()
})
</script>
<style scoped>
/* 1:1 抄 sponsor/NewPerson.vue */
.expert-new { max-width: 1200px; }
.page-card { background: #fff; padding: 16px 20px; border-radius: 6px; border: 1px solid #f0f0f0; }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 16px; }
.breadcrumb a { color: var(--brand-primary); cursor: pointer; text-decoration: none; }
.breadcrumb a:hover { color: var(--brand-primary-deep, var(--brand-primary)); }
.form-card.nested { background: transparent; border: none; box-shadow: none; }
:deep(.form-card .el-card__body) { padding: 24px 28px; }
.form-actions { margin-top: 16px; display: flex; gap: 8px; }
:deep(.el-button--primary) { background: var(--brand-primary); border-color: var(--brand-primary); border-radius: 4px; }
:deep(.el-button--primary:hover) { background: var(--brand-primary-deep, var(--brand-primary)); border-color: var(--brand-primary-deep, var(--brand-primary)); }
:deep(.el-button) { border-radius: 4px; }
:deep(.el-input__wrapper), :deep(.el-textarea__inner) { border-radius: 4px; }
</style>
+253 -245
View File
@@ -4,96 +4,95 @@
字段映射: manager/Experts.vue 11 + 操作列, 容器/风格改用 admin 通用 page-card + desc
接口: bizList('expert', q) / bizAdd / bizUpdate
-->
<div class="admin-experts">
<div class="page-card">
<div class="desc">维护全平台评审专家档案, 管理其执业证书职称证明与账号状态</div>
<div class="page-card admin-experts">
<div class="breadcrumb">首页 / 专家管理</div>
<el-form inline :model="q" class="filter-bar">
<el-form-item label="姓名">
<el-input v-model="q.name" placeholder="输入姓名" clearable style="width:160px" />
</el-form-item>
<el-form-item label="手机号">
<el-input v-model="q.phone" placeholder="输入手机号" clearable maxlength="11" style="width:160px" />
</el-form-item>
<el-form-item label="工作单位">
<el-input v-model="q.workUnit" placeholder="输入工作单位" clearable style="width:200px" />
</el-form-item>
<el-form-item label="审核状态">
<el-select v-model="q.auditStatus" placeholder="请选择" clearable style="width:140px">
<el-option label="待审核" value="0" />
<el-option label="通过" value="1" />
<el-option label="退回" value="2" />
</el-select>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="q.status" placeholder="全部状态" clearable style="width:120px">
<el-option label="启用" value="1" />
<el-option label="禁用" value="0" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查询</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
<!-- ========== 筛选区 ( People.vue 风格一致) ========== -->
<el-form inline :model="q" class="filter-form">
<el-form-item label="姓名"><el-input v-model="q.name" placeholder="输入姓名" clearable style="width:160px" /></el-form-item>
<el-form-item label="手机号"><el-input v-model="q.phone" placeholder="输入手机号" clearable maxlength="11" style="width:160px" /></el-form-item>
<el-form-item label="工作单位"><el-input v-model="q.workUnit" placeholder="输入工作单位" clearable style="width:200px" /></el-form-item>
<el-form-item label="审核状态">
<el-select v-model="q.auditStatus" placeholder="请选择" clearable style="width:140px">
<el-option label="未提交" value="0" />
<el-option label="待审核" value="1" />
<el-option label="通过" value="2" />
<el-option label="拒绝" value="3" />
</el-select>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="q.status" placeholder="全部状态" clearable style="width:120px">
<el-option label="正常" value="Y" />
<el-option label="禁用" value="N" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查询</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
<!-- ========== 批量按钮区 ( People.vue batch-bar 风格一致) ========== -->
<div class="batch-bar">
<el-button type="primary" @click="goNew">新建专家</el-button>
<el-button @click="openImport">批量导入</el-button>
<el-button :disabled="!selection.length" @click="onBatchAudit">批量审核</el-button>
<el-button @click="openImportPracticeCert">批量导入执业证书</el-button>
<el-button @click="openImportTitleCert">批量导入职称证明</el-button>
</div>
<div class="page-card">
<div class="batch-bar">
<el-button type="primary" @click="openAdd">新建专家</el-button>
<el-button @click="openImport">批量导入</el-button>
<el-button :disabled="!selection.length" @click="onBatchAudit">批量审核</el-button>
<el-button @click="openImportPracticeCert">批量导入执业证书</el-button>
<el-button @click="openImportTitleCert">批量导入职称证明</el-button>
</div>
<el-table :data="rows" v-loading="loading" stripe border @selection-change="onSel">
<el-table-column type="selection" width="44" />
<el-table-column prop="name" label="姓名" width="100" fixed />
<el-table-column prop="phone" label="手机号" width="130" />
<el-table-column prop="workUnit" label="工作单位" min-width="180" show-overflow-tooltip />
<el-table-column prop="department" label="科室" width="100" />
<el-table-column prop="title" label="职称" width="100" />
<el-table-column label="执业证书" width="170">
<template #default="{ row }">
<template v-if="row.practiceCertUrl">
<el-button link type="primary" @click="onViewCert(row.practiceCertUrl)">查看</el-button>
<el-button link type="primary" @click="onDownloadCert(row.practiceCertUrl, row.phone)">下载</el-button>
</template>
<span v-else style="color:#c0c4cc">-</span>
<!-- ========== 表格 ( People.vue 风格一致, page-card 包裹) ========== -->
<el-table :data="rows" v-loading="loading" stripe border @selection-change="onSel">
<el-table-column type="selection" width="44" />
<el-table-column prop="name" label="姓名" width="100" fixed />
<el-table-column prop="phone" label="手机号" width="130" />
<el-table-column prop="workUnit" label="工作单位" min-width="180" show-overflow-tooltip />
<el-table-column prop="department" label="科室" width="100" />
<el-table-column prop="title" label="职称" width="100" />
<el-table-column label="执业证书" width="170">
<template #default="{ row }">
<template v-if="row.practiceCertUrl">
<el-button link type="primary" @click="onPreviewCert(row.practiceCertUrl)">查看</el-button>
<el-button link type="primary" @click="onDownloadCert(row.practiceCertUrl, row.phone)">下载</el-button>
</template>
</el-table-column>
<el-table-column label="职称证明" width="170">
<template #default="{ row }">
<template v-if="row.titleCertUrl">
<el-button link type="primary" @click="onViewCert(row.titleCertUrl)">查看</el-button>
<el-button link type="primary" @click="onDownloadCert(row.titleCertUrl, row.phone)">下载</el-button>
</template>
<span v-else style="color:#c0c4cc">-</span>
<span v-else style="color:#c0c4cc">-</span>
</template>
</el-table-column>
<el-table-column label="职称证明" width="170">
<template #default="{ row }">
<template v-if="row.titleCertUrl">
<el-button link type="primary" @click="onPreviewCert(row.titleCertUrl)">查看</el-button>
<el-button link type="primary" @click="onDownloadCert(row.titleCertUrl, row.phone)">下载</el-button>
</template>
</el-table-column>
<el-table-column label="审核状态" width="100" align="center">
<template #default="{ row }">
<span :class="['stage-tag', auditStatusClass(row.auditStatus)]">{{ auditStatusText(row.auditStatus) }}</span>
</template>
</el-table-column>
<el-table-column prop="auditTime" label="审批时间" width="160" align="center">
<template #default="{ row }">{{ fmtTime(row.auditTime) }}</template>
</el-table-column>
<el-table-column prop="auditBy" label="审批人" width="120" align="center" />
<el-table-column label="操作" width="260" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="onAudit(row)">审核</el-button>
<el-button link type="primary" @click="onEdit(row)">修改</el-button>
<el-button link type="primary" @click="onView(row)">查看</el-button>
<el-button link :type="row.status === '1' ? 'danger' : 'success'" @click="row.status === '1' ? onDisable(row) : onEnable(row)">
{{ row.status === '1' ? '禁用' : '恢复' }}
</el-button>
<el-button link type="info" v-if="row.auditStatus === '2'" @click="onViewAuditOpinion(row)">已退回</el-button>
</template>
</el-table-column>
</el-table>
<span v-else style="color:#c0c4cc">-</span>
</template>
</el-table-column>
<el-table-column label="审核状态" width="100" align="center">
<template #default="{ row }">
<audit-status-tag :status="row.auditStatus" />
</template>
</el-table-column>
<el-table-column label="启用状态" width="100" align="center">
<template #default="{ row }">
<el-tag size="small" :type="row.status === 'N' ? 'danger' : 'success'">
{{ row.status === 'N' ? '禁用' : '正常' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="260" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="onAudit(row)">审核</el-button>
<el-button link type="primary" @click="goEdit(row)">修改</el-button>
<el-button link type="primary" @click="goView(row)">查看</el-button>
<el-button link :type="row.status === 'N' ? 'success' : 'danger'" @click="row.status === 'N' ? onEnable(row) : onDisable(row)">
{{ row.status === 'N' ? '恢复' : '禁用' }}
</el-button>
<el-button link type="info" v-if="row.auditStatus === '3'" @click="onViewAuditOpinion(row)">已拒绝</el-button>
</template>
</el-table-column>
</el-table>
<div class="pager">
<el-pagination
v-model:current-page="page.pageNum"
v-model:page-size="page.pageSize"
@@ -102,35 +101,11 @@
layout="total, sizes, prev, pager, next, jumper"
@current-change="load"
@size-change="load"
style="margin-top: 12px; text-align: right;"
/>
</div>
<!-- 新建/编辑专家 -->
<el-dialog v-model="editOpen" :title="editForm.expertId ? '编辑专家' : '新建专家'" width="640px">
<el-form :model="editForm" label-width="120px" :rules="editRules" ref="editFormRef">
<el-form-item label="姓名" prop="name"><el-input v-model="editForm.name" /></el-form-item>
<el-form-item label="手机号" prop="phone"><el-input v-model="editForm.phone" maxlength="11" /></el-form-item>
<el-form-item label="工作单位" prop="workUnit"><el-input v-model="editForm.workUnit" /></el-form-item>
<el-form-item label="科室" prop="department"><el-input v-model="editForm.department" placeholder="如: 呼吸科" /></el-form-item>
<el-form-item label="职称" prop="title">
<el-select v-model="editForm.title" placeholder="请选择" style="width:100%">
<el-option label="住院医师" value="住院医师" />
<el-option label="主治医师" value="主治医师" />
<el-option label="副主任医师" value="副主任医师" />
<el-option label="主任医师" value="主任医师" />
<el-option label="教授" value="教授" />
<el-option label="副教授" value="副教授" />
</el-select>
</el-form-item>
<el-form-item label="执业证书 URL"><el-input v-model="editForm.practiceCertUrl" /></el-form-item>
<el-form-item label="职称证明 URL"><el-input v-model="editForm.titleCertUrl" /></el-form-item>
</el-form>
<template #footer>
<el-button @click="editOpen=false">取消</el-button>
<el-button type="primary" :loading="submitting" @click="submitEdit">确定</el-button>
</template>
</el-dialog>
<!-- 新建/编辑专家: 跳转独立页 /admin/experts/new /experts/edit/:id -->
<!-- (旧的 edit dialog 已删) -->
<!-- 审核 -->
<el-dialog v-model="auditOpen" title="审核" width="520px">
@@ -138,8 +113,8 @@
<el-form :model="auditForm" label-width="100px">
<el-form-item label="审核结果">
<el-radio-group v-model="auditForm.result">
<el-radio value="1">通过</el-radio>
<el-radio value="2">退回</el-radio>
<el-radio value="2">通过</el-radio>
<el-radio value="3">拒绝</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="审核意见">
@@ -152,27 +127,33 @@
</template>
</el-dialog>
<!-- 批量导入专家 -->
<el-dialog v-model="importOpen" title="批量导入专家" width="520px">
<div style="margin-bottom:12px;font-size:13px">
<el-link type="primary" @click="onDownloadTpl">批量导入模板下载</el-link>
<!-- 批量导入专家 (RuoYi 用户导入风格) -->
<el-dialog v-model="importOpen" title="批量导入专家" width="400px" append-to-body>
<el-upload
ref="importUploadRef"
:limit="1"
accept=".xlsx, .xls"
:headers="importHeaders"
:action="importUrl"
:disabled="importing"
:on-progress="onImportProgress"
:on-success="onImportSuccess"
:auto-upload="false"
drag
>
<i class="el-icon-upload"></i>
<div class="el-upload__text">将文件拖到此处, <em>点击上传</em></div>
<div slot="tip" class="el-upload__tip el-upload__tip--inline">
<el-checkbox v-model="importUpdateSupport" :disabled="importing" /> 是否更新已存在的专家
<el-link style="font-size:12px;vertical-align:baseline;margin-left:8px" type="primary" :underline="false" @click.stop="onDownloadTpl">下载模板</el-link>
</div>
</el-upload>
<!-- 导入结果反馈 (RuoYi 风格: 内联在 dialog ) -->
<div v-if="importResultMsg" class="upload-result" v-html="importResultMsg" />
<div slot="footer" class="dialog-footer">
<el-button @click="importOpen=false"> </el-button>
<el-button type="primary" :loading="importing" @click="submitImport"> </el-button>
</div>
<p style="color:#606266;margin-bottom:8px">*批量导入模板与列表表项一致</p>
<el-form label-width="100px">
<el-form-item label="审核结果">
<el-radio-group v-model="importAuditResult">
<el-radio value="1">通过</el-radio>
<el-radio value="2">退回</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="选择文件">
<el-input v-model="importFileUrl" placeholder="上传文件 URL 或选择文件" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="importOpen=false">取消</el-button>
<el-button type="primary" :loading="importing" @click="submitImport">确定</el-button>
</template>
</el-dialog>
<!-- 批量导入执业证书 -->
@@ -195,34 +176,40 @@
</template>
</el-dialog>
<!-- 详情查看 -->
<el-dialog v-model="viewOpen" title="专家详情" width="640px">
<el-descriptions :column="2" border v-if="currentRow">
<el-descriptions-item label="姓名">{{ currentRow.name }}</el-descriptions-item>
<el-descriptions-item label="手机号">{{ currentRow.phone }}</el-descriptions-item>
<el-descriptions-item label="工作单位">{{ currentRow.workUnit }}</el-descriptions-item>
<el-descriptions-item label="科室">{{ currentRow.department }}</el-descriptions-item>
<el-descriptions-item label="职称">{{ currentRow.title }}</el-descriptions-item>
<el-descriptions-item label="审核状态">{{ auditStatusText(currentRow.auditStatus) }}</el-descriptions-item>
<el-descriptions-item label="执业证书">
<el-link v-if="currentRow.practiceCertUrl" type="primary" @click="onViewCert(currentRow.practiceCertUrl)">查看证书</el-link>
<span v-else>-</span>
</el-descriptions-item>
<el-descriptions-item label="职称证明">
<el-link v-if="currentRow.titleCertUrl" type="primary" @click="onViewCert(currentRow.titleCertUrl)">查看证书</el-link>
<span v-else>-</span>
</el-descriptions-item>
<el-descriptions-item label="审批时间">{{ fmtTime(currentRow.auditTime) }}</el-descriptions-item>
<el-descriptions-item label="审批人">{{ currentRow.auditBy }}</el-descriptions-item>
</el-descriptions>
<!-- 详情查看: 已改跳独立页 /admin/experts/view/:id, 这里删除 dialog -->
<!-- 证书/证明 图片预览 dialog -->
<el-dialog v-model="previewOpen" title="证书预览" width="640px" :show-close="true" destroy-on-close>
<div class="cert-preview">
<el-image
:src="previewUrl"
fit="contain"
:preview-src-list="[previewUrl]"
style="width:100%;max-height:70vh"
loading="lazy"
>
<template #error>
<div class="cert-preview-error">图片加载失败, <a @click="onDownloadCert(previewUrl)">下载</a>查看</div>
</template>
</el-image>
</div>
<template #footer>
<el-button @click="onDownloadCert(previewUrl)">下载</el-button>
<el-button type="primary" @click="previewOpen=false">关闭</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { bizList, bizAdd, bizUpdate } from '@/api/public'
import { ref, reactive, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import request from '@/utils/request'
import { bizList, bizUpdate, downloadExpertTemplate } from '@/api/public'
import { ElMessage, ElMessageBox } from 'element-plus'
import AuditStatusTag from '@/components/AuditStatusTag.vue'
const router = useRouter()
const q = reactive({ name: '', phone: '', workUnit: '', auditStatus: '', status: '' })
const rows = ref([])
@@ -231,12 +218,23 @@ const loading = ref(false)
const submitting = ref(false)
const page = reactive({ pageNum: 1, pageSize: 10, total: 0 })
// 跳独立页 (新建/编辑)
function goNew() {
router.push('/admin/experts/new')
}
function goEdit(row) {
router.push('/admin/experts/edit/' + row.expertId)
}
function goView(row) {
router.push('/admin/experts/view/' + row.expertId)
}
async function load() {
loading.value = true
try {
const { data } = await bizList('expert', { ...q, pageNum: page.pageNum, pageSize: page.pageSize })
rows.value = data?.rows || []
page.total = data?.total || 0
rows.value = (data && data.rows) || []
page.total = (data && data.total) || 0
} catch { rows.value = []; page.total = 0 }
finally { loading.value = false }
}
@@ -247,16 +245,7 @@ function reset() {
}
function onSel(rs) { selection.value = rs }
function auditStatusText(s) {
if (s === '1') return '通过'
if (s === '2') return '退回'
return '待审核'
}
function auditStatusClass(s) {
if (s === '1') return 'stage-done'
if (s === '2') return 'stage-reject'
return 'stage-reviewing'
}
// auditStatusText / auditStatusClass 已删除: 改用 AuditStatusTag 组件
function fmtTime(d) {
if (!d) return ''
const dt = new Date(d)
@@ -264,62 +253,19 @@ function fmtTime(d) {
return `${dt.getFullYear()}.${pad(dt.getMonth() + 1)}.${pad(dt.getDate())} ${pad(dt.getHours())}:${pad(dt.getMinutes())}`
}
// ===== 新建/编辑 =====
const editOpen = ref(false)
const editFormRef = ref(null)
const editForm = reactive({
expertId: '', name: '', phone: '', workUnit: '', department: '', title: '',
practiceCertUrl: '', titleCertUrl: ''
})
const editRules = {
name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
phone: [{ required: true, message: '请输入手机号', trigger: 'blur' }],
workUnit: [{ required: true, message: '请输入工作单位', trigger: 'blur' }],
department: [{ required: true, message: '请输入科室', trigger: 'blur' }],
title: [{ required: true, message: '请选择职称', trigger: 'change' }]
}
function openAdd() {
Object.assign(editForm, { expertId: '', name: '', phone: '', workUnit: '', department: '', title: '', practiceCertUrl: '', titleCertUrl: '' })
editOpen.value = true
}
function onEdit(row) {
Object.assign(editForm, {
expertId: row.expertId, name: row.name, phone: row.phone, workUnit: row.workUnit,
department: row.department, title: row.title,
practiceCertUrl: row.practiceCertUrl || '', titleCertUrl: row.titleCertUrl || ''
})
editOpen.value = true
}
async function submitEdit() {
await editFormRef.value.validate()
submitting.value = true
try {
if (editForm.expertId) {
await bizUpdate('expert', { ...editForm })
ElMessage.success('修改成功')
} else {
await bizAdd('expert', {
...editForm,
auditStatus: '1', auditBy: 'admin',
auditTime: new Date().toISOString().slice(0, 19).replace('T', ' '),
status: '1'
})
ElMessage.success('新建成功')
}
editOpen.value = false
load()
} catch (e) { ElMessage.error(e?.msg || '保存失败') }
finally { submitting.value = false }
}
// auditStatusText / auditStatusClass 已删除: 改用 AuditStatusTag 组件
// ===== 新建/编辑: 跳独立页 (本页面不再保留 dialog) =====
// ===== 审核 =====
const auditOpen = ref(false)
const auditForm = reactive({ expertId: '', name: '', phone: '', result: '1', opinion: '' })
// result: '2'=通过, '3'=拒绝 (BizAuditStatusEnum: 0未提交 1待审核 2通过 3拒绝)
const auditForm = reactive({ expertId: '', name: '', phone: '', result: '2', opinion: '' })
function onAudit(row) {
auditForm.expertId = row.expertId
auditForm.name = row.name
auditForm.phone = row.phone
auditForm.result = '1'
auditForm.result = '2'
auditForm.opinion = ''
auditOpen.value = true
}
@@ -333,7 +279,7 @@ async function submitAudit() {
auditBy: 'admin',
auditTime: new Date().toISOString().slice(0, 19).replace('T', ' ')
})
ElMessage.success(auditForm.result === '1' ? '审核通过' : '已退回')
ElMessage.success(auditForm.result === '2' ? '审核通过' : '已拒绝')
auditOpen.value = false
load()
} catch { ElMessage.error('审核失败') }
@@ -351,7 +297,7 @@ async function onBatchAudit() {
for (const r of selection.value) {
try {
await bizUpdate('expert', {
expertId: r.expertId, auditStatus: '1', auditOpinion: '批量通过',
expertId: r.expertId, auditStatus: '2', auditOpinion: '批量通过',
auditBy: 'admin', auditTime: new Date().toISOString().slice(0, 19).replace('T', ' ')
})
ok++
@@ -363,34 +309,39 @@ async function onBatchAudit() {
load()
}
// ===== 启用/禁用 =====
// ===== 启用/禁用: 专用 endpoint (PUT /business/expert/{expertId}/status) =====
async function onDisable(row) {
try {
await bizUpdate('expert', { expertId: row.expertId, status: '0' })
await request.put(`/business/expert/${row.expertId}/status`, null, { params: { status: 'N' } })
ElMessage.success('已禁用'); load()
} catch { ElMessage.error('禁用失败') }
} catch (e) { ElMessage.error(e?.msg || '禁用失败') }
}
async function onEnable(row) {
try {
await bizUpdate('expert', { expertId: row.expertId, status: '1' })
await request.put(`/business/expert/${row.expertId}/status`, null, { params: { status: 'Y' } })
ElMessage.success('已恢复'); load()
} catch { ElMessage.error('恢复失败') }
} catch (e) { ElMessage.error(e?.msg || '恢复失败') }
}
// ===== 详情 / 退回意见 =====
const viewOpen = ref(false)
const currentRow = ref(null)
function onView(row) { currentRow.value = row; viewOpen.value = true }
// 详情查看已改跳独立页 /admin/experts/view/:id (见 goView)
function onViewAuditOpinion(row) {
ElMessageBox.alert(
`退回时间: ${fmtTime(row.auditTime) || '-'}\n审批人: ${row.auditBy || '-'}\n退回意见: ${row.auditOpinion || '(无)'}`,
`专家 ${row.name} 审核已退回`,
`专家 ${row.name} 审核已拒绝`,
{ type: 'warning', confirmButtonText: '知道了' }
).catch(() => {})
}
// ===== 证书查看/下载 =====
function onViewCert(url) { if (url) window.open(url, '_blank') }
// 图片预览: dialog 弹窗直接显示 (用 el-image 自带 preview-src-list 支持点击放大)
const previewOpen = ref(false)
const previewUrl = ref('')
function onPreviewCert(url) {
if (!url) return
previewUrl.value = url
previewOpen.value = true
}
function onDownloadCert(url, phone) {
if (!url) return
const a = document.createElement('a')
@@ -399,22 +350,65 @@ function onDownloadCert(url, phone) {
document.body.appendChild(a); a.click(); document.body.removeChild(a)
}
// ===== 批量导入 dialog =====
// ===== 批量导入 dialog (RuoYi 用户导入风格) =====
const importOpen = ref(false)
const importAuditResult = ref('1')
const importFileUrl = ref('')
const importing = ref(false)
const importUploadRef = ref(null)
const importUpdateSupport = ref(false)
const importResultMsg = ref('')
// importHeaders 与 importUrl 用于 el-upload auto-upload=false 时的手动提交
const importHeaders = computed(() => ({
Authorization: 'Bearer ' + (localStorage.getItem('ry_token') || '')
}))
const importUrl = computed(() => '/dev-api/business/expert/importData?updateSupport=' + importUpdateSupport.value)
function openImport() {
importAuditResult.value = '1'; importFileUrl.value = ''; importOpen.value = true
importResultMsg.value = ''
importUpdateSupport.value = false
// 清空 el-upload 已选文件 (手动触发, 否则下次打开还显示上次的)
if (importUploadRef.value) {
importUploadRef.value.clearFiles()
}
importOpen.value = true
}
function onDownloadTpl() { ElMessage.info('批量导入模板下载 — 待后端提供静态文件') }
async function submitImport() {
if (!importFileUrl.value) return ElMessage.warning('请选择文件')
function onImportProgress() {
importing.value = true
await new Promise(r => setTimeout(r, 500))
}
function onImportSuccess(res, file) {
importing.value = false
ElMessage.info(`已提交批量导入任务 — 待后端实现`)
importOpen.value = false
// 后端返回 {code:200, msg:"..."}
importResultMsg.value = res?.msg || '导入成功'
if (res?.code === 200) {
ElMessage.success('导入完成')
load()
} else {
ElMessage.error(res?.msg || '导入失败')
}
}
// 与 SponsorPeople.vue downloadTemplate 同款: 走 RuoYi 极简模式 (blob + a.download + 延迟释放)
// 注意: a 必须挂到 body 上 (Firefox 要求), URL.revokeObjectURL 要延后 (避免下载未启动就释放)
function onDownloadTpl() {
downloadExpertTemplate().then((res) => {
const blob = new Blob([res.data], { type: res.data.type || 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = '专家导入模板.xlsx'
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
setTimeout(() => URL.revokeObjectURL(url), 1000)
}).catch((e) => {
ElMessage.error(e?.message || '模板下载失败')
})
}
function submitImport() {
if (!importUploadRef.value) return ElMessage.warning('上传组件未就绪')
importUploadRef.value.submit()
}
// 批量导入执业证书
@@ -447,14 +441,28 @@ onMounted(load)
</script>
<style scoped>
/* 整页面板 (与 People.vue 风格一致) */
.admin-experts { padding: 16px; }
.page-card { background: #fff; border-radius: 4px; padding: 20px 24px; border: 1px solid #ebeef5; margin-bottom: 12px; }
.desc { font-size: 13px; color: #909399; margin-bottom: 16px; padding-bottom: 12px; border-bottom: 1px solid #ebeef5; }
.filter-bar { margin-bottom: 0; }
.batch-bar { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 12px; }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.filter-form { margin-bottom: 12px; }
.batch-bar { display: flex; gap: 8px; margin-bottom: 12px; align-items: center; flex-wrap: wrap; }
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
.stage-tag { display: inline-block; padding: 2px 10px; border-radius: 3px; font-size: 12px; border: 1px solid; }
.stage-done { color: #67c23a; border-color: #c2e7b0; background: #f0f9eb; }
.stage-reviewing { color: #e6a23c; border-color: #f5dab1; background: #fdf6ec; }
.stage-reject { color: #f56c6c; border-color: #fbc4c4; background: #fef0f0; }
/* 证书预览 dialog */
.cert-preview { display: flex; justify-content: center; align-items: center; min-height: 200px; background: #f5f7fa; border-radius: 4px; padding: 8px; }
.cert-preview-error { color: #909399; padding: 40px 0; }
/* 批量导入 dialog (RuoYi 风格) */
.upload-result {
margin-top: 12px; padding: 10px 12px;
background: #f5f7fa; border-radius: 4px;
font-size: 13px; line-height: 1.8;
max-height: 200px; overflow-y: auto;
}
:deep(.el-upload__tip--inline) {
display: flex; align-items: center; justify-content: center;
gap: 8px; margin-top: 8px;
}
/* stage-* CSS 已删除: 改用 AuditStatusTag 组件 (el-tag 自带颜色) */
</style>
+39 -41
View File
@@ -1,45 +1,43 @@
<template>
<div class="admin-meetings">
<div class="page-card">
<div class="desc">查看系统中所有会议 (跨角色全量)</div>
<div class="page-card admin-meetings">
<div class="breadcrumb">首页 / 会议全览</div>
<el-form inline :model="q" class="filter-bar">
<el-form-item label="会议主题:">
<el-input v-model="q.title" placeholder="输入会议主题" clearable />
</el-form-item>
<el-form-item label="状态:">
<el-select v-model="q.status" placeholder="全部状态" clearable>
<el-option v-for="s in statusOptions" :key="s.value" :label="s.label" :value="s.value" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查询</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
<!-- ========== 筛选区 ( People.vue 风格一致) ========== -->
<el-form inline :model="q" class="filter-form">
<el-form-item label="会议主题"><el-input v-model="q.title" placeholder="输入会议主题" clearable /></el-form-item>
<el-form-item label="状态">
<el-select v-model="q.status" placeholder="全部状态" clearable>
<el-option v-for="s in statusOptions" :key="s.value" :label="s.label" :value="s.value" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查询</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
<el-table :data="rows" border stripe v-loading="loading">
<el-table-column prop="title" label="会议主题" min-width="200" show-overflow-tooltip />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="STATUS_TAG[row.status] || 'info'">{{ STATUS_LABEL[row.status] || row.status }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="startTime" label="开始时间" width="170" />
<el-table-column prop="endTime" label="结束时间" width="170" />
<el-table-column prop="createTime" label="创建时间" width="170" />
</el-table>
<!-- ========== 表格 ( People.vue 风格一致, page-card 包裹) ========== -->
<el-table :data="rows" border stripe v-loading="loading">
<el-table-column prop="title" label="会议主题" min-width="200" show-overflow-tooltip />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="STATUS_TAG[row.status] || 'info'">{{ STATUS_LABEL[row.status] || row.status }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="startTime" label="开始时间" width="170" />
<el-table-column prop="endTime" label="结束时间" width="170" />
<el-table-column prop="createTime" label="创建时间" width="170" />
</el-table>
<div class="pager">
<el-pagination
class="pager"
background
layout="total, prev, pager, next, sizes"
v-model:current-page="q.pageNum"
v-model:page-size="q.pageSize"
:total="total"
:page-sizes="[10, 20, 50]"
:page-size="q.pageSize"
:current-page="q.pageNum"
@current-change="(p) => { q.pageNum = p; load() }"
@size-change="(s) => { q.pageSize = s; q.pageNum = 1; load() }"
layout="total, sizes, prev, pager, next, jumper"
@current-change="load"
@size-change="load"
/>
</div>
</div>
@@ -66,8 +64,8 @@ async function load() {
loading.value = true
try {
const r = await bizList('meeting', { ...q })
rows.value = r.rows || r.data?.rows || []
total.value = r.total || r.data?.total || 0
rows.value = r.rows || (r.data && r.data.rows) || []
total.value = r.total || (r.data && r.data.total) || 0
} finally {
loading.value = false
}
@@ -82,9 +80,9 @@ onMounted(load)
</script>
<style scoped>
/* 整页面板 (与 People.vue 风格一致) */
.admin-meetings { padding: 16px; }
.page-card { background: #fff; border-radius: 4px; padding: 20px 24px; border: 1px solid #ebeef5; }
.desc { font-size: 13px; color: #909399; margin-bottom: 16px; padding-bottom: 12px; border-bottom: 1px solid #ebeef5; }
.filter-bar { margin-bottom: 16px; }
.pager { margin-top: 16px; text-align: right; }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.filter-form { margin-bottom: 12px; }
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
</style>
+75 -77
View File
@@ -7,77 +7,78 @@
biz_service_org 整体删除
接口: bizList('org', q) / bizUpdate
-->
<div class="admin-orgs">
<div class="page-card">
<div class="desc">管理公司档案 (赞助方 + 执行方共用一张表, 通过 org_type 区分)</div>
<div class="page-card admin-orgs">
<div class="breadcrumb">首页 / 公司管理</div>
<el-form inline :model="q" class="filter-bar">
<el-form-item label="公司类型">
<el-select v-model="q.orgType" placeholder="全部" clearable style="width:140px">
<el-option label="赞助方" value="sponsor" />
<el-option label="执行方" value="executor" />
</el-select>
</el-form-item>
<el-form-item label="公司名称">
<el-input v-model="q.orgName" placeholder="输入公司名称" clearable style="width:200px" />
</el-form-item>
<el-form-item label="税号/统一社会信用代码">
<el-input v-model="q.taxNo" placeholder="输入税号" clearable style="width:220px" />
</el-form-item>
<el-form-item label="合作状态">
<el-select v-model="q.status" placeholder="请选择" clearable style="width:140px">
<el-option label="正常" value="正常" />
<el-option label="合作中" value="合作中" />
<el-option label="禁用" value="禁用" />
<el-option label="0正常" value="0" />
<el-option label="1禁用" value="1" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查询</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
<!-- ========== 筛选区 ( People.vue 风格一致) ========== -->
<el-form inline :model="q" class="filter-form">
<el-form-item label="公司类型">
<el-select v-model="q.orgType" placeholder="全部" clearable style="width:140px">
<el-option label="赞助方" value="sponsor" />
<el-option label="执行方" value="executor" />
</el-select>
</el-form-item>
<el-form-item label="公司名称">
<el-input v-model="q.orgName" placeholder="输入公司名称" clearable style="width:200px" />
</el-form-item>
<el-form-item label="税号/统一社会信用代码">
<el-input v-model="q.taxNo" placeholder="输入税号" clearable style="width:220px" />
</el-form-item>
<el-form-item label="合作状态">
<el-select v-model="q.status" placeholder="请选择" clearable style="width:140px">
<el-option label="正常" value="正常" />
<el-option label="合作中" value="合作中" />
<el-option label="禁用" value="禁用" />
<el-option label="0正常" value="0" />
<el-option label="1禁用" value="1" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查询</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
<!-- ========== 工具栏 ( People.vue batch-bar 风格一致) ========== -->
<div class="batch-bar">
<el-button type="primary" @click="openAdd">新增公司</el-button>
</div>
<div class="page-card">
<div class="toolbar">
<el-button type="primary" @click="openAdd">新增公司</el-button>
</div>
<el-table :data="rows" v-loading="loading" stripe border>
<el-table-column type="index" label="#" width="50" />
<el-table-column label="公司类型" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.orgType === 'sponsor' ? 'success' : 'primary'" disable-transitions>
{{ row.orgType === 'sponsor' ? '赞助方' : '执行方' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="orgName" label="公司名称" min-width="200" show-overflow-tooltip />
<el-table-column prop="address" label="公司地址" min-width="220" show-overflow-tooltip />
<el-table-column prop="taxNo" label="税号/统一社会信用代码" min-width="180" />
<el-table-column label="合作状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="statusTagType(row.status)" disable-transitions>{{ row.status }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建日期" width="120" align="center">
<template #default="{ row }">{{ formatDate(row.createTime) }}</template>
</el-table-column>
<el-table-column prop="contactName" label="联系人" width="100" align="center" />
<el-table-column prop="contactPhone" label="联系电话" width="130" align="center" />
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="onView(row)">查看</el-button>
<el-button link type="primary" @click="onEdit(row)">编辑</el-button>
<el-button link :type="isDisabled(row) ? 'success' : 'danger'" @click="onToggleStatus(row)">
{{ isDisabled(row) ? '恢复' : '禁用' }}
</el-button>
</template>
</el-table-column>
</el-table>
<!-- ========== 表格 ( People.vue 风格一致, page-card 包裹) ========== -->
<el-table :data="rows" v-loading="loading" stripe border>
<el-table-column type="index" label="#" width="50" />
<el-table-column label="公司类型" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.orgType === 'sponsor' ? 'success' : 'primary'" disable-transitions>
{{ row.orgType === 'sponsor' ? '赞助方' : '执行方' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="orgName" label="公司名称" min-width="200" show-overflow-tooltip />
<el-table-column prop="address" label="公司地址" min-width="220" show-overflow-tooltip />
<el-table-column prop="taxNo" label="税号/统一社会信用代码" min-width="180" />
<el-table-column label="合作状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="statusTagType(row.status)" disable-transitions>{{ row.status }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建日期" width="120" align="center">
<template #default="{ row }">{{ formatDate(row.createTime) }}</template>
</el-table-column>
<el-table-column prop="contactName" label="联系人" width="100" align="center" />
<el-table-column prop="contactPhone" label="联系电话" width="130" align="center" />
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="onView(row)">查看</el-button>
<el-button link type="primary" @click="onEdit(row)">编辑</el-button>
<el-button link :type="isDisabled(row) ? 'success' : 'danger'" @click="onToggleStatus(row)">
{{ isDisabled(row) ? '恢复' : '禁用' }}
</el-button>
</template>
</el-table-column>
</el-table>
<div class="pager">
<el-pagination
v-model:current-page="page.pageNum"
v-model:page-size="page.pageSize"
@@ -86,10 +87,7 @@
layout="total, sizes, prev, pager, next, jumper"
@current-change="load"
@size-change="load"
style="margin-top: 12px; text-align: right;"
/>
<p class="hint-bottom">*点击禁用后, 该公司下所有账号均不可登录</p>
</div>
<!-- 查看 / 编辑 dialog -->
@@ -160,8 +158,8 @@ async function load() {
loading.value = true
try {
const { data } = await bizList('org', { ...q, pageNum: page.pageNum, pageSize: page.pageSize })
rows.value = data?.rows || []
page.total = data?.total || 0
rows.value = (data && data.rows) || []
page.total = (data && data.total) || 0
} catch { rows.value = []; page.total = 0 }
finally { loading.value = false }
}
@@ -253,10 +251,10 @@ onMounted(load)
</script>
<style scoped>
/* 整页面板 (与 People.vue 风格一致) */
.admin-orgs { padding: 16px; }
.page-card { background: #fff; border-radius: 4px; padding: 20px 24px; border: 1px solid #ebeef5; margin-bottom: 12px; }
.desc { font-size: 13px; color: #909399; margin-bottom: 16px; padding-bottom: 12px; border-bottom: 1px solid #ebeef5; }
.filter-bar { margin-bottom: 0; }
.toolbar { display: flex; gap: 8px; margin-bottom: 12px; }
.hint-bottom { font-size: 12px; color: #ff4d4f; margin-top: 12px; text-align: right; }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.filter-form { margin-bottom: 12px; }
.batch-bar { display: flex; gap: 8px; margin-bottom: 12px; align-items: center; }
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
</style>
+50 -55
View File
@@ -1,59 +1,54 @@
<template>
<div class="admin-projects">
<div class="page-card">
<div class="desc">查看系统中所有项目 (跨角色全量)</div>
<div class="page-card admin-projects">
<div class="breadcrumb">首页 / 项目全览</div>
<!-- 筛选 -->
<el-form inline :model="q" class="filter-bar">
<el-form-item label="项目编号:">
<el-input v-model="q.projectNo" placeholder="输入项目编号" clearable />
</el-form-item>
<el-form-item label="项目名称:">
<el-input v-model="q.projectName" placeholder="输入项目名称" clearable />
</el-form-item>
<el-form-item label="状态:">
<el-select v-model="q.status" placeholder="全部状态" clearable>
<el-option v-for="s in statusOptions" :key="s.value" :label="s.label" :value="s.value" />
</el-select>
</el-form-item>
<el-form-item label="是否结题:">
<el-select v-model="q.isFinished" placeholder="全部" clearable>
<el-option label="未结题" value="0" />
<el-option label="已结题" value="1" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查询</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
<!-- ========== 筛选区 ( People.vue 风格一致) ========== -->
<el-form inline :model="q" class="filter-form">
<el-form-item label="项目编号"><el-input v-model="q.projectNo" placeholder="输入项目编号" clearable /></el-form-item>
<el-form-item label="项目名称"><el-input v-model="q.projectName" placeholder="输入项目名称" clearable /></el-form-item>
<el-form-item label="状态">
<el-select v-model="q.status" placeholder="全部状态" clearable>
<el-option v-for="s in statusOptions" :key="s.value" :label="s.label" :value="s.value" />
</el-select>
</el-form-item>
<el-form-item label="是否结题">
<el-select v-model="q.isFinished" placeholder="全部" clearable>
<el-option label="未结题" value="0" />
<el-option label="已结题" value="1" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查询</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
<el-table :data="rows" border stripe v-loading="loading">
<el-table-column prop="projectNo" label="项目编号" width="140" />
<el-table-column prop="projectName" label="项目名称" min-width="200" show-overflow-tooltip />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="STATUS_TAG[row.status] || 'info'">{{ STATUS_LABEL[row.status] || row.status }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="isFinished" label="结题" width="80">
<template #default="{ row }">
<el-tag :type="row.isFinished === '1' ? 'success' : 'info'">{{ row.isFinished === '1' ? '已结题' : '未结题' }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="170" />
</el-table>
<!-- ========== 表格 ( People.vue 风格一致, page-card 包裹) ========== -->
<el-table :data="rows" border stripe v-loading="loading">
<el-table-column prop="projectNo" label="项目编号" width="140" />
<el-table-column prop="projectName" label="项目名称" min-width="200" show-overflow-tooltip />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="STATUS_TAG[row.status] || 'info'">{{ STATUS_LABEL[row.status] || row.status }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="isFinished" label="结题" width="80">
<template #default="{ row }">
<el-tag :type="row.isFinished === '1' ? 'success' : 'info'">{{ row.isFinished === '1' ? '已结题' : '未结题' }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="170" />
</el-table>
<div class="pager">
<el-pagination
class="pager"
background
layout="total, prev, pager, next, sizes"
v-model:current-page="q.pageNum"
v-model:page-size="q.pageSize"
:total="total"
:page-sizes="[10, 20, 50]"
:page-size="q.pageSize"
:current-page="q.pageNum"
@current-change="(p) => { q.pageNum = p; load() }"
@size-change="(s) => { q.pageSize = s; q.pageNum = 1; load() }"
layout="total, sizes, prev, pager, next, jumper"
@current-change="load"
@size-change="load"
/>
</div>
</div>
@@ -80,8 +75,8 @@ async function load() {
loading.value = true
try {
const r = await bizList('project', { ...q })
rows.value = r.rows || r.data?.rows || []
total.value = r.total || r.data?.total || 0
rows.value = r.rows || (r.data && r.data.rows) || []
total.value = r.total || (r.data && r.data.total) || 0
} finally {
loading.value = false
}
@@ -96,9 +91,9 @@ onMounted(load)
</script>
<style scoped>
/* 整页面板 (与 People.vue 风格一致) */
.admin-projects { padding: 16px; }
.page-card { background: #fff; border-radius: 4px; padding: 20px 24px; border: 1px solid #ebeef5; }
.desc { font-size: 13px; color: #909399; margin-bottom: 16px; padding-bottom: 12px; border-bottom: 1px solid #ebeef5; }
.filter-bar { margin-bottom: 16px; }
.pager { margin-top: 16px; text-align: right; }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.filter-form { margin-bottom: 12px; }
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
</style>
+88 -28
View File
@@ -1,23 +1,21 @@
<template>
<div class="admin-roles">
<div class="page-card">
<div class="desc">查看 6 种角色的用户分布与人员明细</div>
<div class="page-card admin-roles">
<div class="breadcrumb">首页 / 角色管理</div>
<el-row :gutter="16" class="role-cards">
<el-col v-for="r in roleStats" :key="r.code" :xs="12" :sm="8" :md="4">
<div class="role-card" :class="{ active: r.code === selected }" @click="selectRole(r.code)">
<div class="role-label">{{ r.label }}</div>
<div class="role-count">{{ r.count }}</div>
<div class="role-hint">点击查看详情</div>
</div>
</el-col>
</el-row>
</div>
<el-row :gutter="16" class="role-cards">
<el-col v-for="r in roleStats" :key="r.code" :xs="12" :sm="8" :md="4">
<div class="role-card" :class="{ active: r.code === selected }" @click="selectRole(r.code)">
<div class="role-label">{{ r.label }}</div>
<div class="role-count">{{ r.count }}</div>
<div class="role-hint">点击查看详情</div>
</div>
</el-col>
</el-row>
<!-- 选中角色详情 -->
<div v-if="selected" class="page-card" style="margin-top: 16px">
<!-- 选中角色详情 ( page-card , sub-title 分割) -->
<template v-if="selected">
<h3 class="sub-title">{{ ROLE_LABEL[selected] }} - 成员列表</h3>
<el-table :data="rows" border stripe v-loading="loading" style="margin-top: 12px">
<el-table :data="rows" border stripe v-loading="loading" class="role-table">
<el-table-column prop="userId" label="用户ID" width="80" />
<el-table-column prop="userName" label="账号" width="140" />
<el-table-column prop="nickName" label="姓名" width="140" />
@@ -29,19 +27,30 @@
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="170" />
<el-table-column label="操作" width="100" fixed="right">
<template #default="{ row }">
<el-button size="small" link :type="row.status === '0' ? 'danger' : 'success'" @click="toggleStatus(row)">
{{ row.status === '0' ? '停用' : '启用' }}
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { listUser } from '@/api/system'
import request from '@/utils/request'
import { ElMessage } from 'element-plus'
const ROLE_LABEL = {
admin: '后台管理员', leader: '项目负责人', manager: '合规人员',
doctor: '评审专家', executor: '执行人', sponsor: '赞助方'
}
// 卡片展示的角色 (隐藏 admin / leader)
const VISIBLE_ROLES = ['manager', 'doctor', 'executor', 'sponsor']
const roleStats = ref([])
const selected = ref('')
@@ -49,17 +58,20 @@ const rows = ref([])
const loading = ref(false)
async function loadStats() {
const codes = Object.keys(ROLE_LABEL)
const arr = []
for (const code of codes) {
for (const code of VISIBLE_ROLES) {
try {
const r = await listUser({ pageNum: 1, pageSize: 1, roleType: code })
arr.push({ code, label: ROLE_LABEL[code], count: r.total || 0 })
arr.push({ code, label: ROLE_LABEL[code], count: (r.data && r.data.total) || r.total || 0 })
} catch (e) {
arr.push({ code, label: ROLE_LABEL[code], count: 0 })
}
}
roleStats.value = arr
// 默认选中第一个角色并加载列表
if (arr.length && !selected.value) {
await selectRole(arr[0].code)
}
}
async function selectRole(code) {
@@ -67,24 +79,72 @@ async function selectRole(code) {
loading.value = true
try {
const r = await listUser({ pageNum: 1, pageSize: 200, roleType: code })
rows.value = r.rows || []
rows.value = (r.data && r.data.rows) || r.rows || []
} finally {
loading.value = false
}
}
async function toggleStatus(row) {
const newStatus = row.status === '0' ? '1' : '0'
try {
await request({
url: '/system/user/changeStatus',
method: 'put',
data: { userId: row.userId, status: newStatus }
})
ElMessage.success('已' + (newStatus === '0' ? '启用' : '停用'))
// 刷新当前角色列表 + 顶部卡片计数
await selectRole(selected.value)
await loadStats()
} catch (e) {
ElMessage.error(e?.msg || '操作失败')
}
}
onMounted(loadStats)
</script>
<style scoped>
/* 整页面板 (与 People.vue 风格一致) */
.admin-roles { padding: 16px; }
.page-card { background: #fff; border-radius: 4px; padding: 20px 24px; border: 1px solid #ebeef5; }
.desc { font-size: 13px; color: #909399; margin-bottom: 16px; padding-bottom: 12px; border-bottom: 1px solid #ebeef5; }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.role-cards { margin-top: 8px; }
.role-card { background: #fafbfc; border: 1px solid #ebeef5; border-radius: 4px; padding: 16px; text-align: center; cursor: pointer; transition: all .2s; }
.role-card:hover, .role-card.active { border-color: var(--brand-primary); background: #fff; box-shadow: 0 2px 8px rgba(24,144,255,.12); }
.role-label { font-size: 14px; color: #606266; margin-bottom: 8px; }
.role-count { font-size: 28px; font-weight: 600; color: var(--brand-primary); margin-bottom: 4px; }
.role-card {
background: #fafbfc;
border: 1px solid #f0f0f0;
border-radius: 8px;
padding: 16px;
text-align: center;
cursor: pointer;
transition: all 0.2s;
}
.role-card:hover, .role-card.active {
border-color: var(--brand-primary);
background: #fff;
box-shadow: 0 2px 8px rgba(30,58,138,0.12);
}
.role-label { font-size: 14px; color: #8c8c8c; margin-bottom: 8px; }
.role-count {
font-size: 28px;
font-weight: 600;
margin-bottom: 4px;
background: linear-gradient(135deg, var(--brand-primary) 0%, var(--brand-primary-deep) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.role-hint { font-size: 12px; color: #c0c4cc; }
.sub-title { font-size: 15px; font-weight: 600; color: #1a1a1a; margin: 0; padding-left: 10px; border-left: 3px solid var(--brand-primary); }
.sub-title {
font-size: 15px;
font-weight: 600;
color: #1a1a1a;
margin: 24px 0 12px;
padding-left: 10px;
border-left: 3px solid var(--brand-primary);
line-height: 1;
}
.role-table { margin-top: 12px; }
</style>
+238
View File
@@ -0,0 +1,238 @@
<template>
<!--
角色: 🟦 后台 支持单位管理 (admin/sponsor-orgs)
说明: admin/Orgs.vue 拆出 sponsor 一支, 硬编码 orgType='sponsor'
接口: bizList('org', q) / bizAdd / bizUpdate
-->
<div class="page-card admin-sponsor-orgs">
<div class="breadcrumb">首页 / 支持单位管理</div>
<!-- ========== 筛选区 ========== -->
<el-form inline :model="q" class="filter-form">
<el-form-item label="公司名称">
<el-input v-model="q.orgName" placeholder="输入公司名称" clearable style="width:200px" />
</el-form-item>
<el-form-item label="税号/统一社会信用代码">
<el-input v-model="q.taxNo" placeholder="输入税号" clearable style="width:220px" />
</el-form-item>
<el-form-item label="合作状态">
<el-select v-model="q.status" placeholder="请选择" clearable style="width:140px">
<el-option label="正常" value="正常" />
<el-option label="合作中" value="合作中" />
<el-option label="禁用" value="禁用" />
<el-option label="0正常" value="0" />
<el-option label="1禁用" value="1" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查询</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
<div class="batch-bar">
<el-button type="primary" @click="openAdd">新增支持单位</el-button>
</div>
<el-table :data="rows" v-loading="loading" stripe border>
<el-table-column type="index" label="#" width="50" />
<el-table-column prop="orgName" label="公司名称" min-width="200" show-overflow-tooltip />
<el-table-column prop="address" label="公司地址" min-width="220" show-overflow-tooltip />
<el-table-column prop="taxNo" label="税号/统一社会信用代码" min-width="180" />
<el-table-column label="合作状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="statusTagType(row.status)" disable-transitions>{{ row.status }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建日期" width="120" align="center">
<template #default="{ row }">{{ formatDate(row.createTime) }}</template>
</el-table-column>
<el-table-column prop="contactName" label="联系人" width="100" align="center" />
<el-table-column prop="contactPhone" label="联系电话" width="130" align="center" />
<el-table-column label="操作" width="240" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="onView(row)">查看</el-button>
<el-button link type="primary" @click="onEdit(row)">编辑</el-button>
<el-button link type="primary" @click="onPeople(row)">人员管理</el-button>
<el-button link :type="isDisabled(row) ? 'success' : 'danger'" @click="onToggleStatus(row)">
{{ isDisabled(row) ? '恢复' : '禁用' }}
</el-button>
</template>
</el-table-column>
</el-table>
<div class="pager">
<el-pagination
v-model:current-page="page.pageNum"
v-model:page-size="page.pageSize"
:total="page.total"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next, jumper"
@current-change="load"
@size-change="load"
/>
</div>
<el-dialog v-model="dialogVisible" :title="isEdit ? '编辑支持单位' : '新增支持单位'" width="640px">
<el-form :model="form" label-width="120px" :rules="rules" ref="formRef">
<el-form-item label="公司名称" prop="orgName">
<el-input v-model="form.orgName" />
</el-form-item>
<el-form-item label="公司地址">
<el-input v-model="form.address" />
</el-form-item>
<el-form-item label="税号/统一社会信用代码">
<el-input v-model="form.taxNo" />
</el-form-item>
<el-form-item label="联系人">
<el-input v-model="form.contactName" />
</el-form-item>
<el-form-item label="联系电话">
<el-input v-model="form.contactPhone" maxlength="11" />
</el-form-item>
<el-form-item v-if="isEdit" label="状态">
<el-select v-model="form.status" style="width:160px">
<el-option label="正常" value="正常" />
<el-option label="合作中" value="合作中" />
<el-option label="禁用" value="禁用" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="saving" @click="onSave">保存</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { bizList, bizAdd, bizUpdate } from '@/api/public'
import { ElMessage, ElMessageBox } from 'element-plus'
const router = useRouter()
const q = reactive({ orgType: 'sponsor', orgName: '', taxNo: '', status: '' })
const rows = ref([])
const loading = ref(false)
const page = reactive({ pageNum: 1, pageSize: 10, total: 0 })
function statusTagType(s) {
if (s === '正常' || s === '0' || s === '合作中') return 'success'
if (s === '禁用' || s === '1') return 'danger'
return 'info'
}
function isDisabled(row) {
return row.status === '禁用' || row.status === '1'
}
function formatDate(d) {
if (!d) return ''
const str = String(d)
return str.length >= 10 ? str.slice(0, 10) : str
}
async function load() {
loading.value = true
try {
const { data } = await bizList('org', { ...q, pageNum: page.pageNum, pageSize: page.pageSize })
rows.value = (data && data.rows) || []
page.total = (data && data.total) || 0
} catch { rows.value = []; page.total = 0 }
finally { loading.value = false }
}
function reset() {
Object.assign(q, { orgType: 'sponsor', orgName: '', taxNo: '', status: '' })
page.pageNum = 1
load()
}
async function onToggleStatus(row) {
const disabled = isDisabled(row)
const newStatus = disabled ? '正常' : '禁用'
const label = disabled ? '恢复' : '禁用'
if (!disabled) {
try {
await ElMessageBox.confirm(
`确定要${label}${row.orgName}」吗? ${label}后该企业下所有账号均不可登录`,
'确认',
{ type: 'warning' }
)
} catch { return }
}
try {
await bizUpdate('org', { orgId: row.orgId, status: newStatus })
ElMessage.success('已' + label)
load()
} catch (e) { ElMessage.error(e?.msg || '操作失败') }
}
const dialogVisible = ref(false)
const isEdit = ref(false)
const saving = ref(false)
const formRef = ref(null)
const form = reactive({
orgId: null, orgType: 'sponsor', orgName: '', address: '', taxNo: '',
contactName: '', contactPhone: '', status: '正常'
})
const rules = {
orgName: [{ required: true, message: '请输入公司名称', trigger: 'blur' }]
}
function openAdd() {
isEdit.value = false
Object.assign(form, {
orgId: null, orgType: 'sponsor', orgName: '', address: '', taxNo: '',
contactName: '', contactPhone: '', status: '正常'
})
dialogVisible.value = true
}
function onEdit(row) {
isEdit.value = true
Object.assign(form, { ...row, orgType: 'sponsor' })
dialogVisible.value = true
}
function onPeople(row) {
router.push({ path: '/admin/sponsor-people', query: { orgId: row.orgId, orgName: row.orgName } })
}
function onView(row) {
ElMessageBox.alert(
`类型: 赞助方\n` +
`名称: ${row.orgName}\n` +
`地址: ${row.address || '-'}\n` +
`税号: ${row.taxNo || '-'}\n` +
`联系人: ${row.contactName || '-'} (${row.contactPhone || '-'})\n` +
`状态: ${row.status}\n` +
`创建日期: ${formatDate(row.createTime)}`,
'支持单位详情',
{ confirmButtonText: '关闭' }
).catch(() => {})
}
async function onSave() {
await formRef.value.validate()
saving.value = true
try {
if (isEdit.value) {
await bizUpdate('org', { ...form })
ElMessage.success('修改成功')
} else {
await bizAdd('org', { ...form })
ElMessage.success('新增成功')
}
dialogVisible.value = false
load()
} catch (e) { ElMessage.error(e?.msg || '保存失败') }
finally { saving.value = false }
}
onMounted(load)
</script>
<style scoped>
.admin-sponsor-orgs { padding: 16px; }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.filter-form { margin-bottom: 12px; }
.batch-bar { display: flex; gap: 8px; margin-bottom: 12px; align-items: center; }
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
</style>
+226
View File
@@ -0,0 +1,226 @@
<template>
<!--
角色: 🟦 后台 支持单位下人员 (admin/sponsor-people)
入口: org 表格行内人员管理按钮 (router.push orgId/orgName)
接口: bizList('person', q) / bizUpdate / bizAdd / sponsorImport / sponsorImportTemplate
-->
<div class="page-card admin-sponsor-people">
<div class="breadcrumb">
首页 / 支持单位管理 / 人员管理{{ orgName ? ' (' + orgName + ')' : '' }}
</div>
<el-form inline :model="q" class="filter-form">
<el-form-item label="姓名">
<el-input v-model="q.name" placeholder="姓名" clearable style="width:140px" />
</el-form-item>
<el-form-item label="手机号">
<el-input v-model="q.phone" placeholder="手机号" clearable style="width:140px" />
</el-form-item>
<el-form-item label="企业名称">
<el-input v-model="q.orgName" placeholder="企业名称" clearable style="width:180px" />
</el-form-item>
<el-form-item label="部门">
<el-input v-model="q.department" placeholder="部门" clearable style="width:140px" />
</el-form-item>
<el-form-item label="职务">
<el-input v-model="q.position" placeholder="职务" clearable style="width:140px" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查询</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
<div class="batch-bar">
<el-button type="primary" @click="goNew">新建人员</el-button>
<el-button @click="openImport">批量导入</el-button>
<span class="filter-tip">*批量导入模板与列表表项一致</span>
</div>
<el-table :data="rows" v-loading="loading" stripe border>
<el-table-column prop="name" label="姓名" min-width="100" />
<el-table-column prop="phone" label="手机号" min-width="130" />
<el-table-column prop="orgName" label="企业名称" min-width="180" show-overflow-tooltip />
<el-table-column prop="department" label="部门" min-width="100" />
<el-table-column prop="position" label="职务" min-width="100" />
<el-table-column label="状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="statusTagType(row.status)" disable-transitions>{{ row.status || '正常' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="120" fixed="right">
<template #default="{ row }">
<el-button link :type="isDisabled(row) ? 'success' : 'danger'" @click="onToggleStatus(row)">
{{ isDisabled(row) ? '启用' : '禁用' }}
</el-button>
</template>
</el-table-column>
</el-table>
<div class="pager">
<el-pagination
v-model:current-page="page.pageNum"
v-model:page-size="page.pageSize"
:total="page.total"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next, jumper"
@current-change="load"
@size-change="load"
/>
</div>
<!-- 批量导入 -->
<el-dialog v-model="importOpen" title="批量导入" width="520px" :close-on-click-modal="false">
<el-form label-width="100px">
<el-form-item label="选择文件">
<el-upload ref="uploadRef" :auto-upload="false" :limit="1" accept=".xls,.xlsx" :on-change="onFileChange" :on-remove="onFileRemove">
<el-button>选择文件</el-button>
</el-upload>
</el-form-item>
<el-form-item label="">
<el-button link type="primary" @click="downloadTemplate">批量导入模板下载</el-button>
</el-form-item>
<el-form-item><span class="filter-tip">*导入列: 姓名/手机号/所属公司/部门/职务/角色</span></el-form-item>
</el-form>
<template #footer>
<el-button @click="importOpen = false">取消</el-button>
<el-button type="primary" :loading="importing" @click="submitImport">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { bizList, bizUpdate, importPerson, downloadImportTemplate } from '@/api/public'
import { ElMessage, ElMessageBox } from 'element-plus'
const route = useRoute()
const router = useRouter()
const orgId = computed(() => route.query.orgId ? Number(route.query.orgId) : null)
const orgName = computed(() => route.query.orgName || '')
const q = reactive({ name: '', phone: '', orgName: '', department: '', position: '' })
const rows = ref([])
const loading = ref(false)
const page = reactive({ pageNum: 1, pageSize: 10, total: 0 })
function statusTagType(s) {
if (s === '禁用' || s === '1') return 'danger'
return 'success'
}
function isDisabled(row) {
return row.status === '禁用' || row.status === '1'
}
async function load() {
loading.value = true
try {
const params = {
...q,
orgType: 'sponsor',
...(orgId.value ? { orgId: orgId.value } : {}),
pageNum: page.pageNum,
pageSize: page.pageSize,
}
const { data } = await bizList('person', params)
rows.value = (data && data.rows) || []
page.total = (data && data.total) || 0
} catch { rows.value = []; page.total = 0 }
finally { loading.value = false }
}
function reset() {
Object.assign(q, { name: '', phone: '', orgName: '', department: '', position: '' })
page.pageNum = 1
load()
}
async function onToggleStatus(row) {
const disabled = isDisabled(row)
const newStatus = disabled ? '正常' : '禁用'
const label = disabled ? '启用' : '禁用'
try {
await ElMessageBox.confirm(
`确定要${label}${row.name}」吗?`,
'确认',
{ type: 'warning' }
)
} catch { return }
try {
await bizUpdate('person', { personId: row.personId, status: newStatus })
ElMessage.success('已' + label)
load()
} catch (e) { ElMessage.error(e?.msg || '操作失败') }
}
// ========== 新建人员 ==========
function goNew() {
router.push({
path: '/admin/sponsor-people/new',
query: {
...(orgId.value ? { orgId: orgId.value } : {}),
...(orgName.value ? { orgName: orgName.value } : {})
}
})
}
// ========== 批量导入 ==========
const importOpen = ref(false)
const importing = ref(false)
const uploadRef = ref()
const importFile = ref(null)
function openImport() { importFile.value = null; importOpen.value = true }
function onFileChange(file) { importFile.value = file.raw }
function onFileRemove() { importFile.value = null }
async function downloadTemplate() {
try {
const res = await downloadImportTemplate('sponsor')
const blob = new Blob([res.data], { type: res.data.type || 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = '支持单位人员导入模板.xlsx'
a.click()
URL.revokeObjectURL(url)
} catch { ElMessage.error('模板下载失败') }
}
async function submitImport() {
if (!importFile.value) { ElMessage.warning('请先选择文件'); return }
importing.value = true
try {
const res = await importPerson('sponsor', importFile.value)
const payload = res.data || res
const ok = payload.ok ?? 0
const total = payload.total ?? 0
const failed = (payload.results || []).filter(r => !r.ok)
if (failed.length === 0) {
ElMessage.success(`导入成功 ${ok}/${total}`)
} else {
ElMessageBox.alert(
`成功 ${ok} 条, 失败 ${failed.length} 条:\n` +
failed.map(f => `${f.rowNo}${f.name || ''}: ${f.message}`).join('\n'),
'导入结果', { type: 'warning' }
)
}
importOpen.value = false
load()
} catch (e) {
ElMessage.error(e?.msg || '导入失败')
} finally {
importing.value = false
}
}
onMounted(load)
</script>
<style scoped>
.admin-sponsor-people { padding: 16px; }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.filter-form { margin-bottom: 12px; }
.batch-bar { display: flex; gap: 8px; margin-bottom: 12px; align-items: center; }
.filter-tip { color: #909399; font-size: 12px; }
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
</style>
+50 -52
View File
@@ -1,56 +1,54 @@
<template>
<div class="admin-title">
<div class="page-card">
<div class="desc">维护医生可用的职称字典停用的职称不会出现在医生选择下拉中</div>
<div class="page-card admin-title">
<div class="breadcrumb">首页 / 职称管理</div>
<el-form inline :model="q" class="filter-bar">
<el-form-item label="职称名称:">
<el-input v-model="q.name" placeholder="输入名称模糊搜索" clearable />
</el-form-item>
<el-form-item label="状态:">
<el-select v-model="q.status" placeholder="全部状态" clearable>
<el-option label="用" value="0" />
<el-option label="停用" value="1" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查询</el-button>
<el-button @click="reset">重置</el-button>
<el-button type="success" @click="openAdd">新增职称</el-button>
</el-form-item>
</el-form>
<!-- ========== 筛选区 ( People.vue 风格一致) ========== -->
<el-form inline :model="q" class="filter-form">
<el-form-item label="职称名称"><el-input v-model="q.name" placeholder="输入名称模糊搜索" clearable /></el-form-item>
<el-form-item label="状态">
<el-select v-model="q.status" placeholder="全部状态" clearable>
<el-option label="启用" value="0" />
<el-option label="用" value="1" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查询</el-button>
<el-button @click="reset">重置</el-button>
<el-button type="success" @click="openAdd">新增职称</el-button>
</el-form-item>
</el-form>
<el-table :data="rows" border stripe v-loading="loading">
<el-table-column prop="titleId" label="ID" width="80" />
<el-table-column prop="name" label="职称名称" min-width="200" />
<el-table-column prop="sort" label="排序" width="100" />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="row.status === '0' ? 'success' : 'info'">{{ row.status === '0' ? '启用' : '停用' }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="170" />
<el-table-column label="操作" width="180" fixed="right">
<template #default="{ row }">
<el-button size="small" link type="primary" @click="openEdit(row)">编辑</el-button>
<el-button size="small" link :type="row.status === '0' ? 'warning' : 'success'" @click="toggleStatus(row)">
{{ row.status === '0' ? '停用' : '启用' }}
</el-button>
<el-button size="small" link type="danger" @click="removeOne(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- ========== 表格 ( People.vue 风格一致, page-card 包裹) ========== -->
<el-table :data="rows" border stripe v-loading="loading">
<el-table-column prop="titleId" label="ID" width="80" />
<el-table-column prop="name" label="职称名称" min-width="200" />
<el-table-column prop="sort" label="排序" width="100" />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="row.status === '0' ? 'success' : 'info'">{{ row.status === '0' ? '启用' : '停用' }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="170" />
<el-table-column label="操作" width="220" fixed="right">
<template #default="{ row }">
<el-button size="small" link type="primary" @click="openEdit(row)">编辑</el-button>
<el-button size="small" link :type="row.status === '0' ? 'warning' : 'success'" @click="toggleStatus(row)">
{{ row.status === '0' ? '停用' : '启用' }}
</el-button>
<el-button size="small" link type="danger" @click="removeOne(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="pager">
<el-pagination
class="pager"
background
layout="total, prev, pager, next, sizes"
v-model:current-page="q.pageNum"
v-model:page-size="q.pageSize"
:total="total"
:page-sizes="[10, 20, 50]"
:page-size="q.pageSize"
:current-page="q.pageNum"
@current-change="(p) => { q.pageNum = p; load() }"
@size-change="(s) => { q.pageSize = s; q.pageNum = 1; load() }"
layout="total, sizes, prev, pager, next, jumper"
@current-change="load"
@size-change="load"
/>
</div>
@@ -92,8 +90,8 @@ async function load() {
loading.value = true
try {
const r = await listTitles({ ...q })
rows.value = r.rows || []
total.value = r.total || 0
rows.value = (r.data && r.data.rows) || r.rows || []
total.value = (r.data && r.data.total) || r.total || 0
} finally { loading.value = false }
}
@@ -164,9 +162,9 @@ onMounted(load)
</script>
<style scoped>
/* 整页面板 (与 People.vue 风格一致) */
.admin-title { padding: 16px; }
.page-card { background: #fff; border-radius: 4px; padding: 20px 24px; border: 1px solid #ebeef5; }
.desc { font-size: 13px; color: #909399; margin-bottom: 16px; padding-bottom: 12px; border-bottom: 1px solid #ebeef5; }
.filter-bar { margin-bottom: 16px; }
.pager { margin-top: 16px; text-align: right; }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.filter-form { margin-bottom: 12px; }
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
</style>
+61 -67
View File
@@ -1,70 +1,63 @@
<template>
<div class="admin-users">
<div class="page-card">
<div class="desc">查看系统中所有用户修改角色与状态</div>
<div class="page-card admin-users">
<div class="breadcrumb">首页 / 用户管理</div>
<!-- 筛选 -->
<el-form inline :model="q" class="filter-bar">
<el-form-item label="用户账号:">
<el-input v-model="q.userName" placeholder="输入用户账号" clearable />
</el-form-item>
<el-form-item label="姓名:">
<el-input v-model="q.nickName" placeholder="输入姓名" clearable />
</el-form-item>
<el-form-item label="角色:">
<el-select v-model="q.roleType" placeholder="全部角色" clearable>
<el-option v-for="r in roleOptions" :key="r.value" :label="r.label" :value="r.value" />
</el-select>
</el-form-item>
<el-form-item label="状态:">
<el-select v-model="q.status" placeholder="全部状态" clearable>
<el-option label="启用" value="0" />
<el-option label="停用" value="1" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查询</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
<!-- ========== 筛选区 ( People.vue 风格一致) ========== -->
<el-form inline :model="q" class="filter-form">
<el-form-item label="用户账号"><el-input v-model="q.userName" placeholder="输入用户账号" clearable /></el-form-item>
<el-form-item label="姓名"><el-input v-model="q.nickName" placeholder="输入姓名" clearable /></el-form-item>
<el-form-item label="角色">
<el-select v-model="q.roleType" placeholder="全部角色" clearable>
<el-option v-for="r in roleOptions" :key="r.value" :label="r.label" :value="r.value" />
</el-select>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="q.status" placeholder="全部状态" clearable>
<el-option label="启用" value="0" />
<el-option label="停用" value="1" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查询</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
<!-- 列表 -->
<el-table :data="rows" border stripe v-loading="loading">
<el-table-column prop="userId" label="用户ID" width="80" />
<el-table-column prop="userName" label="账号" width="120" />
<el-table-column prop="nickName" label="姓名" width="140" />
<el-table-column prop="roleType" label="角色" width="100">
<template #default="{ row }">{{ ROLE_LABEL[row.roleType] || row.roleType }}</template>
</el-table-column>
<el-table-column prop="phonenumber" label="手机号" width="130" />
<el-table-column prop="email" label="邮箱" min-width="200" show-overflow-tooltip />
<el-table-column prop="status" label="状态" width="80">
<template #default="{ row }">
<el-tag :type="row.status === '0' ? 'success' : 'danger'">{{ row.status === '0' ? '启用' : '停用' }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="170" />
<el-table-column label="操作" width="180" fixed="right">
<template #default="{ row }">
<el-button size="small" link type="primary" @click="openEdit(row)">编辑</el-button>
<el-button size="small" link :type="row.status === '0' ? 'danger' : 'success'" @click="toggleStatus(row)">
{{ row.status === '0' ? '停用' : '启用' }}
</el-button>
</template>
</el-table-column>
</el-table>
<!-- ========== 表格 ( People.vue 风格一致, page-card 包裹) ========== -->
<el-table :data="rows" border stripe v-loading="loading">
<el-table-column prop="userId" label="用户ID" width="80" />
<el-table-column prop="userName" label="账号" width="120" />
<el-table-column prop="nickName" label="姓名" width="140" />
<el-table-column prop="roleType" label="角色" width="100">
<template #default="{ row }">{{ ROLE_LABEL[row.roleType] || row.roleType }}</template>
</el-table-column>
<el-table-column prop="phonenumber" label="手机号" width="130" />
<el-table-column prop="email" label="邮箱" min-width="200" show-overflow-tooltip />
<el-table-column prop="status" label="状态" width="80">
<template #default="{ row }">
<el-tag :type="row.status === '0' ? 'success' : 'danger'">{{ row.status === '0' ? '启用' : '停用' }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="170" />
<el-table-column label="操作" width="180" fixed="right">
<template #default="{ row }">
<el-button size="small" link type="primary" @click="openEdit(row)">编辑</el-button>
<el-button size="small" link :type="row.status === '0' ? 'danger' : 'success'" @click="toggleStatus(row)">
{{ row.status === '0' ? '停用' : '启用' }}
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pager">
<el-pagination
class="pager"
background
layout="total, prev, pager, next, sizes"
v-model:current-page="q.pageNum"
v-model:page-size="q.pageSize"
:total="total"
:page-sizes="[10, 20, 50]"
:page-size="q.pageSize"
:current-page="q.pageNum"
@current-change="(p) => { q.pageNum = p; load() }"
@size-change="(s) => { q.pageSize = s; q.pageNum = 1; load() }"
layout="total, sizes, prev, pager, next, jumper"
@current-change="load"
@size-change="load"
/>
</div>
@@ -118,8 +111,8 @@ async function load() {
loading.value = true
try {
const r = await listUser({ ...q })
rows.value = r.rows || []
total.value = r.total || 0
rows.value = (r.data && r.data.rows) || r.rows || []
total.value = (r.data && r.data.total) || r.total || 0
} catch (e) {
console.error(e)
} finally {
@@ -146,7 +139,8 @@ function openEdit(row) {
async function saveEdit() {
try {
await request({ url: '/system/user/edit', method: 'put', data: { ...editForm } })
// 2026-08-16: 角色统一存 sys_user.role_type, 单次 PUT 即可 (mapper updateUser 已包含 role_type 字段)
await request({ url: '/system/user', method: 'put', data: { ...editForm } })
ElMessage.success('保存成功')
editVisible.value = false
load()
@@ -174,9 +168,9 @@ onMounted(load)
</script>
<style scoped>
/* 整页面板 (与 People.vue 风格一致) */
.admin-users { padding: 16px; }
.page-card { background: #fff; border-radius: 4px; padding: 20px 24px; border: 1px solid #ebeef5; }
.desc { font-size: 13px; color: #909399; margin-bottom: 16px; padding-bottom: 12px; border-bottom: 1px solid #ebeef5; }
.filter-bar { margin-bottom: 16px; }
.pager { margin-top: 16px; text-align: right; }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.filter-form { margin-bottom: 12px; }
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
</style>
+70 -18
View File
@@ -1,8 +1,6 @@
<template>
<div class="admin-workbench">
<!-- 标题 -->
<h2 class="page-title">系统总览</h2>
<div class="page-card admin-workbench">
<div class="breadcrumb">首页</div>
<!-- KPI : 用户/项目/会议/已结题 -->
<div class="stats-grid">
@@ -70,7 +68,7 @@ async function load() {
for (const code of codes) {
try {
const r = await listUser({ pageNum: 1, pageSize: 1, roleType: code })
all.push({ code, label: ROLE_LABEL[code], count: r.total || 0 })
all.push({ code, label: ROLE_LABEL[code], count: (r.data && r.data.total) || r.total || 0 })
} catch (e) {
all.push({ code, label: ROLE_LABEL[code], count: 0 })
}
@@ -79,13 +77,13 @@ async function load() {
// 项目数
const ps = await bizList('project', { pageNum: 1, pageSize: 1 })
stats.value.totalProjects = ps.total || ps.data?.total || 0
stats.value.totalProjects = ps.total || (ps.data && ps.data.total) || 0
// 已结题
const fp = await bizList('project', { pageNum: 1, pageSize: 1, isFinished: '1' })
stats.value.finishedProjects = fp.total || fp.data?.total || 0
stats.value.finishedProjects = fp.total || (fp.data && fp.data.total) || 0
// 会议数
const ms = await bizList('meeting', { pageNum: 1, pageSize: 1 })
stats.value.totalMeetings = ms.total || ms.data?.total || 0
stats.value.totalMeetings = ms.total || (ms.data && ms.data.total) || 0
} catch (e) {
console.warn('admin workbench load failed', e)
}
@@ -95,18 +93,72 @@ onMounted(load)
</script>
<style scoped>
/* 整页面板 (与 People.vue 风格一致) */
.admin-workbench { padding: 16px; }
.page-title { font-size: 20px; font-weight: 600; color: #1a1a1a; margin-bottom: 16px; padding-left: 10px; border-left: 3px solid var(--brand-primary); }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.stats-grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 16px; margin-bottom: 12px; }
.stat-card { background: #fff; border-radius: 4px; padding: 24px; border: 1px solid #ebeef5; cursor: pointer; text-decoration: none; color: inherit; display: flex; flex-direction: column; align-items: center; justify-content: center; transition: opacity .15s, border-color .15s; min-height: 120px; }
.stat-card:hover { border-color: var(--brand-primary); opacity: .9; }
.stat-label { font-size: 14px; color: #606266; margin-bottom: 16px; }
.stat-value { font-size: 32px; font-weight: 600; color: var(--brand-primary); line-height: 1.2; }
.stat-card {
background: #fff;
border: 1px solid #f0f0f0;
border-radius: 8px;
padding: 24px;
cursor: pointer;
text-decoration: none;
color: inherit;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
transition: border-color 0.2s, box-shadow 0.2s;
min-height: 120px;
}
.stat-card:hover { border-color: var(--brand-primary); box-shadow: 0 2px 8px rgba(30,58,138,0.15); }
.stat-label { font-size: 14px; color: #8c8c8c; margin-bottom: 16px; }
.stat-value {
font-size: 32px;
font-weight: 600;
line-height: 1.2;
background: linear-gradient(135deg, var(--brand-primary) 0%, var(--brand-primary-deep) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.hint-text { font-size: 12px; color: #ff4d4f; margin: 12px 0 20px; text-align: center; }
.section { background: #fff; border-radius: 4px; padding: 20px 24px; border: 1px solid #ebeef5; margin-top: 16px; }
.section-title { font-size: 15px; font-weight: 600; color: #1a1a1a; margin-bottom: 16px; padding-left: 10px; border-left: 3px solid var(--brand-primary); }
.section {
background: #fff;
border: 1px solid #f0f0f0;
border-radius: 8px;
padding: 20px 24px;
margin-top: 16px;
}
.section-title {
font-size: 15px;
font-weight: 600;
color: #1a1a1a;
margin-bottom: 16px;
padding-left: 10px;
border-left: 3px solid var(--brand-primary);
line-height: 1;
}
.role-grid { display: grid; grid-template-columns: repeat(6, 1fr); gap: 12px; }
.role-card { background: #fafbfc; border: 1px solid #ebeef5; border-radius: 4px; padding: 16px; text-align: center; }
.role-name { font-size: 13px; color: #606266; margin-bottom: 8px; }
.role-count { font-size: 24px; font-weight: 600; color: var(--brand-primary); }
.role-card {
background: #fafbfc;
border: 1px solid #f0f0f0;
border-radius: 8px;
padding: 16px;
text-align: center;
}
.role-name { font-size: 13px; color: #8c8c8c; margin-bottom: 8px; }
.role-count {
font-size: 24px;
font-weight: 600;
background: linear-gradient(135deg, var(--brand-primary) 0%, var(--brand-primary-deep) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
</style>
+1 -1
View File
@@ -37,7 +37,7 @@ const detailOpen = ref(false)
async function load() {
try {
const r = await listMyMessages({ limit: 50 }); const list = r.rows
const r = await listMyMessages({ limit: 50 }); const list = (r.data && r.data.rows) || r.rows || []
rows.value = list.map(a => ({
id: a.id,
category: a.category,
+29 -29
View File
@@ -1,29 +1,29 @@
<template>
<div class="executor-account">
<div class="page-card executor-account">
<div class="breadcrumb">首页 / 账号信息</div>
<div class="page-card">
<el-form :model="form" label-width="120px" style="max-width:520px;">
<el-form-item label="姓名">
<el-input v-model="form.name" placeholder="请输入姓名" />
</el-form-item>
<el-form-item label="手机号">
<el-input v-model="form.phone" placeholder="请输入手机号" maxlength="11" />
</el-form-item>
<el-form-item label="原密码">
<el-input v-model="form.oldPwd" type="password" show-password placeholder="请输入原密码" />
</el-form-item>
<el-form-item label="新密码">
<el-input v-model="form.newPwd" type="password" show-password placeholder="请输入新密码" />
</el-form-item>
<el-form-item label="确认密码">
<el-input v-model="form.confirmPwd" type="password" show-password placeholder="请再次输入新密码" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSave">保存</el-button>
<el-button @click="onCancel">取消</el-button>
</el-form-item>
</el-form>
</div>
<p class="page-sub">管理账号信息, 修改密码或手机号</p>
<el-form :model="form" label-width="120px" class="account-form">
<el-form-item label="姓名">
<el-input v-model="form.name" placeholder="请输入姓名" />
</el-form-item>
<el-form-item label="手机号">
<el-input v-model="form.phone" placeholder="请输入手机号" maxlength="11" />
</el-form-item>
<el-form-item label="原密码">
<el-input v-model="form.oldPwd" type="password" show-password placeholder="请输入原密码" />
</el-form-item>
<el-form-item label="新密码">
<el-input v-model="form.newPwd" type="password" show-password placeholder="请输入新密码" />
</el-form-item>
<el-form-item label="确认密码">
<el-input v-model="form.confirmPwd" type="password" show-password placeholder="请再次输入新密码" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSave">保存</el-button>
<el-button @click="onCancel">取消</el-button>
</el-form-item>
</el-form>
</div>
</template>
@@ -68,9 +68,9 @@ function onCancel() {
</script>
<style scoped>
.executor-account { padding: 0; }
.breadcrumb { color: #909399; font-size: 13px; margin-bottom: 12px; }
.page-card { background: #fff; border-radius: 4px; padding: 24px; border: 1px solid #ebeef5; }
:deep(.el-button--primary) { background: var(--brand-primary); border-color: var(--brand-primary); border-radius: 4px; }
:deep(.el-button--primary:hover) { background: var(--brand-primary-deep); border-color: var(--brand-primary-deep); }
/* 整页面板 (与 People.vue 风格一致) */
.executor-account { padding: 16px; }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.page-sub { font-size: 13px; color: #909399; margin-bottom: 16px; }
.account-form { max-width: 520px; }
</style>
+61 -57
View File
@@ -1,54 +1,52 @@
<template>
<div class="executor-labor">
<div class="page-card executor-labor">
<div class="breadcrumb">首页 / 劳务凭证</div>
<p class="page-sub">管理会务执行产生的劳务凭证, 上传付款凭证 PDF</p>
<div class="filter-card">
<el-form :inline="true" :model="q" size="default">
<el-form-item label="会议名称:">
<el-input v-model="q.meetingName" clearable placeholder="输入会议名称" style="width:200px" />
</el-form-item>
<el-form-item label="专家姓名:">
<el-input v-model="q.expertName" clearable placeholder="输入专家姓名" style="width:160px" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查找</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
<!-- 红字提示 ( People.vue 一致) -->
<div class="hint-list">
<p>*结算前, 执行方和支持方无法看到凭证材料菜单, 结算后才能看到</p>
<p>*结算后, 已完结状态, 执行方不能再编辑</p>
</div>
<div class="page-card">
<p class="tip">*结算前执行方和支持方无法看到凭证材料菜单结算后才能看到结算材料</p>
<p class="tip">*结算后已完结状态 执行方不能再进行编辑</p>
<!-- ========== 筛选区 ( People.vue 风格一致) ========== -->
<el-form inline :model="q" class="filter-form">
<el-form-item label="会议名称"><el-input v-model="q.meetingName" clearable placeholder="输入会议名称" style="width:200px" /></el-form-item>
<el-form-item label="专家姓名"><el-input v-model="q.expertName" clearable placeholder="输入专家姓名" style="width:160px" /></el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查找</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
<el-table :data="rows" v-loading="loading" stripe border>
<el-table-column prop="voucherNo" label="凭证编号" width="140" />
<el-table-column prop="meetingName" label="会议名称" min-width="200" show-overflow-tooltip />
<el-table-column prop="expertName" label="专家姓名" width="110" />
<el-table-column prop="idCard" label="身份证号" width="180" />
<el-table-column prop="bankAccount" label="银行账号" width="220" />
<el-table-column label="金额" width="120" align="right">
<template #default="{ row }">¥ {{ formatMoney(row.amount) }}</template>
</el-table-column>
<el-table-column prop="submitDate" label="提交日期" width="160" />
<el-table-column label="付款凭证.pdf" width="180" align="center">
<template #default="{ row }">
<el-link v-if="row.paymentVoucherUrl" type="primary" @click="previewVoucher(row.paymentVoucherUrl)">查看</el-link>
<span v-else style="color:#c0c4cc">-</span>
</template>
</el-table-column>
<el-table-column label="操作" width="220" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="onView(row)">查看</el-button>
<el-button link type="success" @click="onUploadVoucher(row)">上传付款凭证</el-button>
</template>
</el-table-column>
</el-table>
<div class="pager">
<el-pagination v-model:current-page="page.pageNum" v-model:page-size="page.pageSize"
:total="page.total" :page-sizes="[10,20,50]" layout="total, sizes, prev, pager, next, jumper"
@current-change="load" @size-change="load" />
</div>
<!-- ========== 表格 ( People.vue 风格一致, page-card 包裹) ========== -->
<el-table :data="rows" v-loading="loading" stripe border>
<el-table-column prop="voucherNo" label="凭证编号" width="140" />
<el-table-column prop="meetingName" label="会议名称" min-width="200" show-overflow-tooltip />
<el-table-column prop="expertName" label="专家姓名" width="110" />
<el-table-column prop="idCard" label="身份证号" width="180" />
<el-table-column prop="bankAccount" label="银行账号" width="220" />
<el-table-column label="金额" width="120" align="right">
<template #default="{ row }">¥ {{ formatMoney(row.amount) }}</template>
</el-table-column>
<el-table-column prop="submitDate" label="提交日期" width="160" />
<el-table-column label="付款凭证.pdf" width="180" align="center">
<template #default="{ row }">
<el-link v-if="row.paymentVoucherUrl" type="primary" @click="previewVoucher(row.paymentVoucherUrl)">查看</el-link>
<span v-else style="color:#c0c4cc">-</span>
</template>
</el-table-column>
<el-table-column label="操作" width="220" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="onView(row)">查看</el-button>
<el-button link type="success" @click="onUploadVoucher(row)">上传付款凭证</el-button>
</template>
</el-table-column>
</el-table>
<div class="pager">
<el-pagination v-model:current-page="page.pageNum" v-model:page-size="page.pageSize"
:total="page.total" :page-sizes="[10,20,50]" layout="total, sizes, prev, pager, next, jumper"
@current-change="load" @size-change="load" />
</div>
<el-dialog v-model="detailOpen" title="劳务凭证详情" width="640px">
@@ -198,18 +196,24 @@ onMounted(load)
</script>
<style scoped>
.executor-labor { padding: 0; }
.breadcrumb { color: #909399; font-size: 13px; margin-bottom: 12px; }
.filter-card, .page-card { background: #fff; border-radius: 4px; padding: 16px; border: 1px solid #ebeef5; margin-bottom: 12px; }
.tip { font-size: 12px; color: #f56c6c; margin: 4px 0; }
/* 整页面板 (与 People.vue 风格一致) */
.executor-labor { padding: 16px; }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.page-sub { font-size: 13px; color: #909399; margin-bottom: 16px; }
.filter-form { margin-bottom: 12px; }
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
:deep(.el-button--primary) { background: var(--brand-primary); border-color: var(--brand-primary); border-radius: 4px; }
:deep(.el-button--primary:hover) { background: var(--brand-primary-deep); border-color: var(--brand-primary-deep); }
:deep(.el-table .el-button) { border-radius: 4px; }
:deep(.el-table .el-button.is-link) { color: var(--brand-primary); background: transparent; border: none; }
:deep(.el-table .el-button.is-link:hover) { color: var(--brand-primary-deep, var(--brand-primary)); background: transparent; }
:deep(.el-table .el-button.is-link.is-disabled) { color: #c0c4cc; background: transparent; }
:deep(.el-button--warning) { background: #e6a23c; border-color: #e6a23c; border-radius: 4px; }
:deep(.el-table .el-button) { border-radius: 4px; }
/* 红字提示 (与 People.vue 一致) */
.hint-list {
background: #fff;
border: 1px solid #ebeef5;
border-radius: 8px;
padding: 12px 20px;
margin-bottom: 16px;
font-size: 12px;
color: #ff4d4f;
line-height: 1.8;
}
.hint-list p { margin-bottom: 4px; }
.hint-list p:last-child { margin-bottom: 0; }
</style>
+68 -84
View File
@@ -1,80 +1,70 @@
<template>
<div class="executor-meetings">
<div class="page-card executor-meetings">
<div class="breadcrumb">首页 / 会议列表</div>
<p class="page-sub">查看执行方参与的会议, 提交监督意见</p>
<div class="filter-card">
<el-form :inline="true" :model="q" size="default">
<el-form-item label="项目编号:">
<el-input v-model="q.projectNo" clearable placeholder="输入项目编号" style="width:160px" />
</el-form-item>
<el-form-item label="会议名称:">
<el-input v-model="q.meetingName" clearable placeholder="输入会议名称" style="width:200px" />
</el-form-item>
<el-form-item label="是否系列会:">
<el-select v-model="q.isSeries" clearable placeholder="请选择" style="width:140px">
<el-option label="是" value="1" />
<el-option label="否" value="0" />
</el-select>
</el-form-item>
<el-form-item label="是否结题:">
<el-select v-model="q.isFinished" clearable placeholder="请选择" style="width:140px">
<el-option label="已结题" value="1" />
<el-option label="未结题" value="0" />
</el-select>
</el-form-item>
<el-form-item label="期数:">
<el-input v-model="q.sessionNo" clearable placeholder="输入期数" style="width:100px" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查找</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
</div>
<!-- ========== 筛选区 ( People.vue 风格一致) ========== -->
<el-form inline :model="q" class="filter-form">
<el-form-item label="项目编号"><el-input v-model="q.projectNo" clearable placeholder="输入项目编号" style="width:160px" /></el-form-item>
<el-form-item label="会议名称"><el-input v-model="q.meetingName" clearable placeholder="输入会议名称" style="width:200px" /></el-form-item>
<el-form-item label="是否系列会">
<el-select v-model="q.isSeries" clearable placeholder="请选择" style="width:140px">
<el-option label="是" value="1" />
<el-option label="否" value="0" />
</el-select>
</el-form-item>
<el-form-item label="是否结题">
<el-select v-model="q.isFinished" clearable placeholder="请选择" style="width:140px">
<el-option label="已结题" value="1" />
<el-option label="未结题" value="0" />
</el-select>
</el-form-item>
<el-form-item label="期数"><el-input v-model="q.sessionNo" clearable placeholder="输入期数" style="width:100px" /></el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查找</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
<div class="page-card">
<div class="toolbar">
<span class="hint">已选 {{ selected.length }} </span>
</div>
<el-table :data="rows" v-loading="loading" stripe border @selection-change="onSelect">
<el-table-column type="selection" width="48" />
<el-table-column prop="projectNo" label="项目编号" width="120" />
<el-table-column prop="meetingName" label="会议名称" min-width="200" show-overflow-tooltip />
<el-table-column prop="isSeries" label="是否系列会" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.isSeries === '1' || row.isSeries === '是' ? 'success' : 'info'" size="small">
{{ row.isSeries === '1' || row.isSeries === '是' ? '是' : '否' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="seriesTopic" label="系列会主题" min-width="160" show-overflow-tooltip />
<el-table-column label="期数/总期数" width="110" align="center">
<template #default="{ row }">{{ row.periodNo || 0 }}/{{ row.totalPeriods || 0 }}</template>
</el-table-column>
<el-table-column prop="currentStage" label="当前阶段" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.currentStage === '执行中' ? 'success' : 'info'" size="small">{{ row.currentStage || '-' }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="isFinished" label="是否结题" width="90" align="center">
<template #default="{ row }">
<el-tag :type="row.isFinished === '1' || row.isFinished === '已结题' ? 'warning' : 'info'" size="small">
{{ row.isFinished === '1' || row.isFinished === '已结题' ? '已结题' : '未结题' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="220" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="onView(row)">查看</el-button>
<el-button link type="warning" @click="onSupervision(row)">查看监督意见</el-button>
</template>
</el-table-column>
</el-table>
<div class="pager">
<el-pagination v-model:current-page="page.pageNum" v-model:page-size="page.pageSize"
:total="page.total" :page-sizes="[10,20,50]" layout="total, sizes, prev, pager, next, jumper"
@current-change="load" @size-change="load" />
</div>
<!-- ========== 表格 ( People.vue 风格一致, page-card 包裹) ========== -->
<el-table :data="rows" v-loading="loading" stripe border @selection-change="onSelect">
<el-table-column type="selection" width="48" />
<el-table-column prop="projectNo" label="项目编号" width="120" />
<el-table-column prop="meetingName" label="会议名称" min-width="200" show-overflow-tooltip />
<el-table-column prop="isSeries" label="是否系列会" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.isSeries === '1' || row.isSeries === '是' ? 'success' : 'info'" size="small">
{{ row.isSeries === '1' || row.isSeries === '是' ? '是' : '否' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="seriesTopic" label="系列会主题" min-width="160" show-overflow-tooltip />
<el-table-column label="期数/总期数" width="110" align="center">
<template #default="{ row }">{{ row.periodNo || 0 }}/{{ row.totalPeriods || 0 }}</template>
</el-table-column>
<el-table-column prop="currentStage" label="当前阶段" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.currentStage === '执行中' ? 'success' : 'info'" size="small">{{ row.currentStage || '-' }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="isFinished" label="是否结题" width="90" align="center">
<template #default="{ row }">
<el-tag :type="row.isFinished === '1' || row.isFinished === '已结题' ? 'warning' : 'info'" size="small">
{{ row.isFinished === '1' || row.isFinished === '已结题' ? '已结题' : '未结题' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="220" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="onView(row)">查看</el-button>
<el-button link type="warning" @click="onSupervision(row)">查看监督意见</el-button>
</template>
</el-table-column>
</el-table>
<div class="pager">
<el-pagination v-model:current-page="page.pageNum" v-model:page-size="page.pageSize"
:total="page.total" :page-sizes="[10,20,50]" layout="total, sizes, prev, pager, next, jumper"
@current-change="load" @size-change="load" />
</div>
<!-- 监督意见 弹窗 -->
@@ -182,16 +172,10 @@ onMounted(load)
</script>
<style scoped>
.executor-meetings { padding: 0; }
.breadcrumb { color: #909399; font-size: 13px; margin-bottom: 12px; }
.filter-card, .page-card { background: #fff; border-radius: 4px; padding: 16px; border: 1px solid #ebeef5; margin-bottom: 12px; }
.toolbar { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }
.hint { font-size: 13px; color: #909399; }
/* 整页面板 (与 People.vue 风格一致) */
.executor-meetings { padding: 16px; }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.page-sub { font-size: 13px; color: #909399; margin-bottom: 16px; }
.filter-form { margin-bottom: 12px; }
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
:deep(.el-button--primary) { background: var(--brand-primary); border-color: var(--brand-primary); border-radius: 4px; }
:deep(.el-button--primary:hover) { background: var(--brand-primary-deep); border-color: var(--brand-primary-deep); }
:deep(.el-table .el-button) { border-radius: 4px; }
:deep(.el-table .el-button.is-link) { color: var(--brand-primary); background: transparent; border: none; }
:deep(.el-table .el-button.is-link:hover) { color: var(--brand-primary-deep); background: transparent; }
:deep(.el-table .el-button.is-link.is-disabled) { color: #c0c4cc; background: transparent; }
</style>
+48 -12
View File
@@ -1,7 +1,7 @@
<template>
<div class="executor-overview">
<h1 class="page-title">首页</h1>
<p class="hint">*点击数字跳转到相应的列表页</p>
<div class="page-card executor-overview">
<div class="breadcrumb">首页</div>
<h1 class="page-title" style="display:none"></h1>
<div class="stats-grid">
<router-link class="stat-card" to="/executor/projects">
@@ -62,16 +62,52 @@ onMounted(loadStats)
</script>
<style scoped>
.executor-overview { padding: 0; }
.page-title { font-size: 22px; font-weight: 600; color: #1a1a1a; margin: 0 0 8px; }
.hint { font-size: 12px; color: #f56c6c; margin: 0 0 16px; }
/* 整页面板 (与 People.vue 风格一致) */
.executor-overview { padding: 16px; }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.page-title { display: none; }
/* 统计卡片 */
.stats-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-bottom: 20px; }
.stat-card { background: #fff; border: 1px solid #ebeef5; border-radius: 4px; padding: 32px 24px; text-decoration: none; color: inherit; display: block; text-align: center; }
.stat-card:hover { border-color: var(--brand-primary); }
.stat-label { font-size: 14px; color: #909399; margin-bottom: 16px; }
.stat-value { font-size: 40px; font-weight: 700; color: var(--brand-primary); line-height: 1; }
.section { background: #fff; border-radius: 4px; padding: 20px 24px; border: 1px solid #ebeef5; }
.section-title { font-size: 15px; font-weight: 600; color: #1a1a1a; margin: 0 0 16px; }
.stat-card {
background: #fff;
border: 1px solid #f0f0f0;
border-radius: 8px;
padding: 32px 24px;
text-decoration: none;
color: inherit;
display: block;
text-align: center;
transition: border-color 0.2s, box-shadow 0.2s;
}
.stat-card:hover { border-color: var(--brand-primary); box-shadow: 0 2px 8px rgba(30,58,138,0.15); }
.stat-label { font-size: 14px; color: #8c8c8c; margin-bottom: 16px; }
.stat-value {
font-size: 40px;
font-weight: 700;
line-height: 1;
background: linear-gradient(135deg, var(--brand-primary) 0%, var(--brand-primary-deep) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* 消息通知 */
.section {
background: #fff;
border-radius: 8px;
padding: 20px 24px;
border: 1px solid #f0f0f0;
}
.section-title {
font-size: 15px;
font-weight: 600;
color: #1a1a1a;
margin: 0 0 16px;
padding-left: 10px;
border-left: 3px solid var(--brand-primary);
line-height: 1;
}
.notice-list { list-style: none; padding: 0; margin: 0; }
.notice-item { padding: 12px 0; border-bottom: 1px solid #f0f0f0; display: flex; align-items: center; gap: 12px; }
.notice-item:last-child { border-bottom: none; }
+84 -94
View File
@@ -1,87 +1,82 @@
<template>
<div class="executor-projects">
<div class="page-card executor-projects">
<div class="breadcrumb">首页 / 项目列表</div>
<p class="page-sub">查看执行方承接的项目, 进行项目分配与评价</p>
<div class="filter-card">
<el-form :inline="true" :model="query" size="default">
<el-form-item label="项目编号:">
<el-input v-model="query.projectNo" clearable placeholder="项目编号" style="width:160px" />
</el-form-item>
<el-form-item label="项目名称:">
<el-input v-model="query.projectName" clearable placeholder="项目名称" style="width:200px" />
</el-form-item>
<el-form-item label="项目时间:">
<el-date-picker v-model="dateRange" type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" style="width:240px" />
</el-form-item>
<el-form-item label="是否结题:">
<el-select v-model="query.isFinished" clearable placeholder="请选择" style="width:140px">
<el-option label="已结题" value="1" />
<el-option label="未结题" value="0" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="loadList">查询</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
<!-- ========== 筛选区 ( People.vue 风格一致) ========== -->
<el-form inline :model="query" class="filter-form">
<el-form-item label="项目编号"><el-input v-model="query.projectNo" clearable placeholder="项目编号" style="width:160px" /></el-form-item>
<el-form-item label="项目名称"><el-input v-model="query.projectName" clearable placeholder="项目名称" style="width:200px" /></el-form-item>
<el-form-item label="项目时间">
<el-date-picker v-model="dateRange" type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" style="width:240px" />
</el-form-item>
<el-form-item label="是否结题">
<el-select v-model="query.isFinished" clearable placeholder="请选择" style="width:140px">
<el-option label="已结题" value="1" />
<el-option label="未结题" value="0" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="loadList">查询</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
<!-- ========== 批量按钮 ( People.vue 风格一致) ========== -->
<div class="batch-bar">
<el-button type="primary" :disabled="!selected.length" @click="onAssign">项目分配</el-button>
<el-button type="primary" :disabled="!selected.length" @click="onBatchRate">批量评价</el-button>
<span class="filter-tip">已选 {{ selected.length }} </span>
</div>
<div class="page-card">
<div class="batch-bar">
<el-button type="primary" :disabled="!selected.length" @click="onAssign">项目分配</el-button>
<el-button type="primary" :disabled="!selected.length" @click="onBatchRate">批量评价</el-button>
<span class="hint">已选 {{ selected.length }} </span>
<span class="tip">*支持方管理员在分配项目的时候只能分配监察员</span>
</div>
<el-table :data="list" border stripe v-loading="loading" @selection-change="sel=>selected=sel">
<el-table-column type="selection" width="48" />
<el-table-column prop="projectNo" label="项目编号" width="120" />
<el-table-column prop="projectName" label="项目名称" min-width="200" show-overflow-tooltip />
<el-table-column label="总场次/总期数" width="110" align="center">
<template #default="{ row }">{{ row.totalSessions || 0 }}/{{ row.totalPeriods || 0 }}</template>
</el-table-column>
<el-table-column prop="doneSessions" label="已执行" width="80" align="center" />
<el-table-column prop="todoSessions" label="未执行" width="80" align="center" />
<el-table-column label="总金额" width="120" align="right">
<template #default="{ row }">¥ {{ formatNum(row.totalAmount) }}</template>
</el-table-column>
<el-table-column label="可用金额" width="120" align="right">
<template #default="{ row }">¥ {{ formatNum(row.availableAmount) }}</template>
</el-table-column>
<el-table-column label="已支付劳务费" width="120" align="right">
<template #default="{ row }">¥ {{ formatNum(row.paidLaborAmount) }}</template>
</el-table-column>
<el-table-column label="已支付会务费" width="120" align="right">
<template #default="{ row }">¥ {{ formatNum(row.paidMeetingAmount) }}</template>
</el-table-column>
<el-table-column label="执行单位评价(支持方)" width="180" align="center">
<template #default="{ row }">
<el-rate v-model="row.ratingScore" disabled show-score :max="5" />
</template>
</el-table-column>
<el-table-column prop="isFinished" label="是否结题" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.isFinished === '1' || row.isFinished === '已结题' ? 'warning' : 'info'" size="small">
{{ row.isFinished === '1' || row.isFinished === '已结题' ? '已结题' : '未结题' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="startTime" label="项目开始时间" width="150" />
<el-table-column prop="endTime" label="项目结束时间" width="150" />
<el-table-column label="操作" width="220" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="viewDetail(row)">查看</el-button>
<el-button link type="primary" @click="onAssign(row)">分配</el-button>
<el-button link type="primary" @click="onRate(row)">项目评价</el-button>
</template>
</el-table-column>
</el-table>
<div class="pager">
<el-pagination v-model:current-page="page.pageNum" v-model:page-size="page.pageSize"
:total="page.total" :page-sizes="[10,20,50]" layout="total, sizes, prev, pager, next, jumper"
@current-change="loadList" @size-change="loadList" />
</div>
<p class="footnote">*点击后可查看具体评价维度评价标准与供应商系统保持一致</p>
<!-- ========== 表格 ( People.vue 风格一致) ========== -->
<el-table :data="list" border stripe v-loading="loading" @selection-change="sel=>selected=sel">
<el-table-column type="selection" width="48" />
<el-table-column prop="projectNo" label="项目编号" width="120" />
<el-table-column prop="projectName" label="项目名称" min-width="200" show-overflow-tooltip />
<el-table-column label="总场次/总期数" width="110" align="center">
<template #default="{ row }">{{ row.totalSessions || 0 }}/{{ row.totalPeriods || 0 }}</template>
</el-table-column>
<el-table-column prop="doneSessions" label="已执行" width="80" align="center" />
<el-table-column prop="todoSessions" label="未执行" width="80" align="center" />
<el-table-column label="总金额" width="120" align="right">
<template #default="{ row }">¥ {{ formatNum(row.totalAmount) }}</template>
</el-table-column>
<el-table-column label="可用金额" width="120" align="right">
<template #default="{ row }">¥ {{ formatNum(row.availableAmount) }}</template>
</el-table-column>
<el-table-column label="已支付劳务费" width="120" align="right">
<template #default="{ row }">¥ {{ formatNum(row.paidLaborAmount) }}</template>
</el-table-column>
<el-table-column label="已支付会务费" width="120" align="right">
<template #default="{ row }">¥ {{ formatNum(row.paidMeetingAmount) }}</template>
</el-table-column>
<el-table-column label="执行单位评价(支持方)" width="180" align="center">
<template #default="{ row }">
<el-rate v-model="row.ratingScore" disabled show-score :max="5" />
</template>
</el-table-column>
<el-table-column prop="isFinished" label="是否结题" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.isFinished === '1' || row.isFinished === '已结题' ? 'warning' : 'info'" size="small">
{{ row.isFinished === '1' || row.isFinished === '已结题' ? '已结题' : '未结题' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="startTime" label="项目开始时间" width="150" />
<el-table-column prop="endTime" label="项目结束时间" width="150" />
<el-table-column label="操作" width="220" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="viewDetail(row)">查看</el-button>
<el-button link type="primary" @click="onAssign(row)">分配</el-button>
<el-button link type="primary" @click="onRate(row)">项目评价</el-button>
</template>
</el-table-column>
</el-table>
<div class="pager">
<el-pagination v-model:current-page="page.pageNum" v-model:page-size="page.pageSize"
:total="page.total" :page-sizes="[10,20,50]" layout="total, sizes, prev, pager, next, jumper"
@current-change="loadList" @size-change="loadList" />
</div>
<!-- 项目分配 弹窗 -->
@@ -182,8 +177,8 @@ async function loadList() {
params.endTime = dateRange.value[1]
}
const r = await bizList('project', params)
list.value = r.rows || []
page.total = r.total || 0
list.value = (r.data && r.data.rows) || r.rows || []
page.total = (r.data && r.data.total) || r.total || 0
} catch (e) { list.value = []; page.total = 0 }
finally { loading.value = false }
}
@@ -200,7 +195,7 @@ function reset() {
function viewDetail(row) { detail.value = row; detailOpen.value = true }
async function loadPeople() {
try { const r = await bizList('person', { pageNum: 1, pageSize: 200 }); people.value = (r.rows || []).map(p => ({ userId: p.personId, name: p.name })) }
try { const r = await bizList('person', { pageNum: 1, pageSize: 200 }); people.value = ((r.data && r.data.rows) || r.rows || []).map(p => ({ userId: p.personId, name: p.name })) }
catch { people.value = [] }
}
@@ -259,17 +254,12 @@ onMounted(() => { loadList(); loadPeople() })
</script>
<style scoped>
.executor-projects { padding: 0; }
.breadcrumb { color: #909399; font-size: 13px; margin-bottom: 12px; }
.filter-card, .page-card { background: #fff; border-radius: 4px; padding: 16px; border: 1px solid #ebeef5; margin-bottom: 12px; }
.batch-bar { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; flex-wrap: wrap; }
.hint { font-size: 13px; color: #909399; }
.tip { font-size: 12px; color: #f56c6c; margin-left: 12px; }
/* 整页面板 (与 People.vue 风格一致) */
.executor-projects { padding: 16px; }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.page-sub { font-size: 13px; color: #909399; margin-bottom: 16px; }
.batch-bar { margin-bottom: 12px; display: flex; gap: 8px; align-items: center; }
.filter-tip { color: #909399; font-size: 12px; }
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
.footnote { font-size: 12px; color: #f56c6c; margin-top: 8px; }
:deep(.el-button--primary) { background: var(--brand-primary); border-color: var(--brand-primary); border-radius: 4px; }
:deep(.el-button--primary:hover) { background: var(--brand-primary-deep); border-color: var(--brand-primary-deep); }
:deep(.el-table .el-button) { border-radius: 4px; }
:deep(.el-table .el-button.is-link) { color: var(--brand-primary); }
:deep(.el-table .el-button.is-link:hover) { color: var(--brand-primary-deep); }
.filter-form { margin-bottom: 12px; }
</style>
+1 -1
View File
@@ -81,7 +81,7 @@ async function loadStats() {
async function loadNotices() {
try {
const r = await listMyMessages({ limit: 50 }); const list = r.rows
const r = await listMyMessages({ limit: 50 }); const list = (r.data && r.data.rows) || r.rows || []
notices.value = list.map(n => ({
id: n.id,
title: n.title,
+2 -2
View File
@@ -149,8 +149,8 @@ async function load() {
loading.value = true
try {
const r = await bizList('meeting', { ...q, pageNum: page.pageNum, pageSize: page.pageSize })
rows.value = r.rows || []
page.total = r.total || 0
rows.value = (r.data && r.data.rows) || r.rows || []
page.total = (r.data && r.data.total) || r.total || 0
} catch (e) {
rows.value = []
page.total = 0
+3 -3
View File
@@ -219,8 +219,8 @@ async function load() {
loading.value = true
try {
const r = await bizList('project', { ...q, pageNum: page.pageNum, pageSize: page.pageSize })
rows.value = r.rows || []
page.total = r.total || 0
rows.value = (r.data && r.data.rows) || r.rows || []
page.total = (r.data && r.data.total) || r.total || 0
} catch (e) { rows.value = []; page.total = 0 }
finally { loading.value = false }
}
@@ -228,7 +228,7 @@ async function load() {
async function loadPersons() {
try {
const r = await bizList('person', { pageNum: 1, pageSize: 100 })
personList.value = r.rows || []
personList.value = (r.data && r.data.rows) || r.rows || []
} catch (e) { personList.value = [] }
}
+35 -29
View File
@@ -1,31 +1,31 @@
<template>
<div class="manager-accounts">
<div class="page-card">
<div class="desc">查看当前登录账号修改个人信息与密码</div>
<el-descriptions :column="2" border style="margin-bottom: 24px">
<el-descriptions-item label="账号">{{ profile.userName }}</el-descriptions-item>
<el-descriptions-item label="姓名">{{ profile.nickName || profile.userName }}</el-descriptions-item>
<el-descriptions-item label="角色">{{ (profile.roleGroup || '').split(',')[0] || '合规人员' }}</el-descriptions-item>
<el-descriptions-item label="状态">{{ profile.status === '0' ? '正常' : '停用' }}</el-descriptions-item>
<el-descriptions-item label="手机">{{ profile.phonenumber || '-' }}</el-descriptions-item>
<el-descriptions-item label="邮箱">{{ profile.email || '-' }}</el-descriptions-item>
<el-descriptions-item label="创建时间" :span="2">{{ profile.createTime || '-' }}</el-descriptions-item>
</el-descriptions>
<div class="page-card manager-accounts">
<div class="breadcrumb">首页 / 账号管理</div>
<p class="page-sub">查看当前登录账号修改个人信息与密码</p>
<el-form :model="form" label-width="120px" style="max-width:600px" :rules="rules" ref="formRef">
<div class="section-title">修改个人信息</div>
<el-form-item label="姓名" prop="nickName"><el-input v-model="form.nickName" placeholder="请输入姓名" /></el-form-item>
<el-form-item label="手机号" prop="phonenumber"><el-input v-model="form.phonenumber" placeholder="请输入手机号" /></el-form-item>
<el-form-item label="邮箱" prop="email"><el-input v-model="form.email" placeholder="请输入邮箱" /></el-form-item>
<el-descriptions :column="2" border style="margin-bottom: 24px">
<el-descriptions-item label="账号">{{ profile.userName }}</el-descriptions-item>
<el-descriptions-item label="姓名">{{ profile.nickName || profile.userName }}</el-descriptions-item>
<el-descriptions-item label="角色">{{ (profile.roleGroup || '').split(',')[0] || '合规人员' }}</el-descriptions-item>
<el-descriptions-item label="状态">{{ profile.status === '0' ? '正常' : '停用' }}</el-descriptions-item>
<el-descriptions-item label="手机">{{ profile.phonenumber || '-' }}</el-descriptions-item>
<el-descriptions-item label="邮箱">{{ profile.email || '-' }}</el-descriptions-item>
<el-descriptions-item label="创建时间" :span="2">{{ profile.createTime || '-' }}</el-descriptions-item>
</el-descriptions>
<div class="section-title">修改密码</div>
<el-form-item label="原密码" prop="oldPassword"><el-input v-model="form.oldPassword" type="password" show-password placeholder="请输入原密码" /></el-form-item>
<el-form-item label="新密码" prop="newPassword"><el-input v-model="form.newPassword" type="password" show-password placeholder="请输入新密码" /></el-form-item>
<el-form-item label="确认密码" prop="confirmPassword"><el-input v-model="form.confirmPassword" type="password" show-password placeholder="请再次输入新密码" /></el-form-item>
<div class="form-tip">* 密码长度 6-20 ,支持数字字母特殊字符;留空表示不修改密码</div>
<el-form-item><el-button type="primary" :loading="saving" @click="onSave">保存</el-button><el-button @click="onCancel">取消</el-button></el-form-item>
</el-form>
</div>
<el-form :model="form" label-width="120px" style="max-width:600px" :rules="rules" ref="formRef">
<div class="section-title">修改个人信息</div>
<el-form-item label="姓名" prop="nickName"><el-input v-model="form.nickName" placeholder="请输入姓名" /></el-form-item>
<el-form-item label="手机号" prop="phonenumber"><el-input v-model="form.phonenumber" placeholder="请输入手机号" /></el-form-item>
<el-form-item label="邮箱" prop="email"><el-input v-model="form.email" placeholder="请输入邮箱" /></el-form-item>
<div class="section-title">修改密码</div>
<el-form-item label="原密码" prop="oldPassword"><el-input v-model="form.oldPassword" type="password" show-password placeholder="请输入原密码" /></el-form-item>
<el-form-item label="新密码" prop="newPassword"><el-input v-model="form.newPassword" type="password" show-password placeholder="请输入新密码" /></el-form-item>
<el-form-item label="确认密码" prop="confirmPassword"><el-input v-model="form.confirmPassword" type="password" show-password placeholder="请再次输入新密码" /></el-form-item>
<div class="filter-tip">* 密码长度 6-20 ,支持数字字母特殊字符;留空表示不修改密码</div>
<el-form-item><el-button type="primary" :loading="saving" @click="onSave">保存</el-button><el-button @click="onCancel">取消</el-button></el-form-item>
</el-form>
</div>
</template>
@@ -92,9 +92,15 @@ onMounted(loadProfile)
</script>
<style scoped>
/* 整页面板 (与 People.vue 风格一致) */
.manager-accounts { padding: 16px; }
.page-card { background: #fff; border-radius: 6px; padding: 24px; border: 1px solid #ebeef5; }
.desc { color: #909399; font-size: 13px; margin-bottom: 16px; }
.section-title { font-weight: 600; margin: 16px 0 12px; padding-left: 8px; border-left: 3px solid var(--brand-primary); }
.form-tip { color: #909399; font-size: 12px; margin-bottom: 12px; }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.page-sub { font-size: 13px; color: #909399; margin-bottom: 16px; }
.filter-tip { color: #909399; font-size: 12px; margin-bottom: 12px; }
.section-title {
font-weight: 600;
margin: 16px 0 12px;
padding-left: 8px;
border-left: 3px solid var(--brand-primary);
}
</style>
+41 -38
View File
@@ -1,39 +1,41 @@
<template>
<div class="manager-exec-intent">
<div class="page-card">
<div class="desc">查看专家对项目的执行意向含待签约/已签约/已拒绝/已退出</div>
<el-form inline :model="q">
<el-form-item label="项目编号:"><el-input v-model="q.projectNo" clearable /></el-form-item>
<el-form-item label="项目名称:"><el-input v-model="q.projectName" clearable /></el-form-item>
<el-form-item label="姓名:"><el-input v-model="q.name" clearable /></el-form-item>
<el-form-item label="工作单位:"><el-input v-model="q.workUnit" clearable /></el-form-item>
<el-form-item label="手机号:"><el-input v-model="q.phone" clearable /></el-form-item>
<el-form-item label="签约状态:">
<el-select v-model="q.onboardStatus" clearable>
<el-option label="待签约" value="待签约" /><el-option label="已签约" value="已签约" />
<el-option label="已拒绝" value="已拒绝" /><el-option label="已退出" value="已退出" />
</el-select>
</el-form-item>
<el-form-item><el-button type="primary" @click="load">查找</el-button><el-button @click="reset">重置</el-button></el-form-item>
</el-form>
</div>
<div class="page-card">
<el-table :data="rows" v-loading="loading" stripe border>
<el-table-column type="index" label="#" width="50" />
<el-table-column prop="projectNo" label="意向项目编号" width="140" />
<el-table-column prop="projectName" label="意向项目名称" min-width="180" show-overflow-tooltip />
<el-table-column prop="name" label="姓名" width="100" />
<el-table-column prop="workUnit" label="工作单位" min-width="180" show-overflow-tooltip />
<el-table-column prop="department" label="部门" width="120" />
<el-table-column prop="position" label="职务" width="100" />
<el-table-column prop="phone" label="手机号" width="130" />
<el-table-column label="签约状态" width="100">
<template #default="{ row }">
<el-tag :type="row.onboardStatus==='已签约' ? 'success' : row.onboardStatus==='已拒绝' ? 'danger' : row.onboardStatus==='已退出' ? 'info' : 'warning'">{{ row.onboardStatus }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="170" />
</el-table>
<div class="page-card manager-exec-intent">
<div class="breadcrumb">首页 / 执行意向</div>
<!-- ========== 筛选区 ( People.vue 风格一致) ========== -->
<el-form inline :model="q" class="filter-form">
<el-form-item label="项目编号"><el-input v-model="q.projectNo" clearable /></el-form-item>
<el-form-item label="项目名称"><el-input v-model="q.projectName" clearable /></el-form-item>
<el-form-item label="姓名"><el-input v-model="q.name" clearable /></el-form-item>
<el-form-item label="工作单位"><el-input v-model="q.workUnit" clearable /></el-form-item>
<el-form-item label="手机号"><el-input v-model="q.phone" clearable /></el-form-item>
<el-form-item label="签约状态">
<el-select v-model="q.onboardStatus" clearable>
<el-option label="待签约" value="待签约" /><el-option label="已签约" value="已签约" />
<el-option label="已拒绝" value="已拒绝" /><el-option label="已退出" value="已退出" />
</el-select>
</el-form-item>
<el-form-item><el-button type="primary" @click="load">查找</el-button><el-button @click="reset">重置</el-button></el-form-item>
</el-form>
<!-- ========== 表格 ( People.vue 风格一致, page-card 包裹) ========== -->
<el-table :data="rows" v-loading="loading" stripe border>
<el-table-column type="index" label="#" width="50" />
<el-table-column prop="projectNo" label="意向项目编号" width="140" />
<el-table-column prop="projectName" label="意向项目名称" min-width="180" show-overflow-tooltip />
<el-table-column prop="name" label="姓名" width="100" />
<el-table-column prop="workUnit" label="工作单位" min-width="180" show-overflow-tooltip />
<el-table-column prop="department" label="部门" width="120" />
<el-table-column prop="position" label="职务" width="100" />
<el-table-column prop="phone" label="手机号" width="130" />
<el-table-column label="签约状态" width="100">
<template #default="{ row }">
<el-tag :type="row.onboardStatus==='已签约' ? 'success' : row.onboardStatus==='已拒绝' ? 'danger' : row.onboardStatus==='已退出' ? 'info' : 'warning'">{{ row.onboardStatus }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="170" />
</el-table>
<div class="pager">
<el-pagination v-model:current-page="page.pageNum" v-model:page-size="page.pageSize" :total="page.total" :page-sizes="[10,20,50]" layout="total, sizes, prev, pager, next, jumper" @current-change="load" @size-change="load" />
</div>
</div>
@@ -62,8 +64,9 @@ onMounted(load)
</script>
<style scoped>
/* 整页面板 (与 People.vue 风格一致) */
.manager-exec-intent { padding: 16px; }
.page-card { background: #fff; border-radius: 6px; padding: 16px; border: 1px solid #ebeef5; margin-bottom: 12px; }
.page-title { font-size: 20px; font-weight: 600; margin-bottom: 4px; }
.desc { color: #909399; font-size: 13px; margin-bottom: 12px; }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.filter-form { margin-bottom: 12px; }
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
</style>
+120
View File
@@ -0,0 +1,120 @@
<template>
<!--
角色: 🟨 合规 服务机构查看 (manager/executor-orgs)
说明: manager/Orgs.vue 拆出 executor 一支, 仅查看
接口: bizList('org', q)
-->
<div class="page-card manager-executor-orgs">
<div class="breadcrumb">首页 / 服务机构管理</div>
<el-form inline :model="q" class="filter-form">
<el-form-item label="公司名称">
<el-input v-model="q.orgName" placeholder="输入公司名称" clearable />
</el-form-item>
<el-form-item label="税号/统一社会信用代码">
<el-input v-model="q.taxNo" placeholder="输入税号" clearable />
</el-form-item>
<el-form-item label="合作状态">
<el-select v-model="q.status" placeholder="请选择" clearable>
<el-option label="0正常" value="0" />
<el-option label="合作中" value="合作中" />
<el-option label="1禁用" value="1" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查找</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
<el-table :data="rows" v-loading="loading" stripe border>
<el-table-column type="index" label="#" width="50" />
<el-table-column prop="orgName" label="公司名称" min-width="200" show-overflow-tooltip />
<el-table-column prop="address" label="公司地址" min-width="220" show-overflow-tooltip />
<el-table-column prop="taxNo" label="税号/统一社会信用代码" min-width="180" />
<el-table-column label="合作状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="statusTagType(row.status)" disable-transitions>{{ row.status }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建日期" width="120" align="center">
<template #default="{ row }">{{ row.createTime ? String(row.createTime).slice(0,10) : '' }}</template>
</el-table-column>
<el-table-column prop="contactName" label="联系人" width="100" align="center" />
<el-table-column prop="contactPhone" label="联系电话" width="130" align="center" />
<el-table-column label="操作" width="180" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="view(row)">查看</el-button>
<el-button link type="primary" @click="goPeople(row)">人员管理</el-button>
</template>
</el-table-column>
</el-table>
<div class="pager">
<el-pagination v-model:current-page="page.pageNum" v-model:page-size="page.pageSize" :total="page.total" :page-sizes="[10,20,50]" layout="total, sizes, prev, pager, next, jumper" @current-change="load" @size-change="load" />
</div>
<el-dialog v-model="detailOpen" title="服务机构详情" width="640px" v-if="currentRow">
<el-descriptions :column="1" border>
<el-descriptions-item label="公司类型">
<el-tag type="primary">执行方</el-tag>
</el-descriptions-item>
<el-descriptions-item label="公司名称">{{ currentRow.orgName }}</el-descriptions-item>
<el-descriptions-item label="公司地址">{{ currentRow.address }}</el-descriptions-item>
<el-descriptions-item label="税号/统一社会信用代码">{{ currentRow.taxNo }}</el-descriptions-item>
<el-descriptions-item label="合作状态">
<el-tag :type="statusTagType(currentRow.status)">{{ currentRow.status }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="创建日期">{{ currentRow.createTime ? String(currentRow.createTime).slice(0,10) : '' }}</el-descriptions-item>
<el-descriptions-item label="联系人">{{ currentRow.contactName || '-' }}</el-descriptions-item>
<el-descriptions-item label="联系电话">{{ currentRow.contactPhone || '-' }}</el-descriptions-item>
</el-descriptions>
<template #footer>
<el-button @click="detailOpen=false">关闭</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { bizList } from '@/api/public'
const router = useRouter()
const q = ref({ orgType: 'executor', orgName: '', taxNo: '', status: '' })
const rows = ref([])
const loading = ref(false)
const page = reactive({ pageNum: 1, pageSize: 10, total: 0 })
const currentRow = ref(null)
const detailOpen = ref(false)
function statusTagType(s) {
if (s === '正常' || s === '0' || s === '合作中') return 'success'
if (s === '禁用' || s === '1') return 'danger'
return 'info'
}
async function load() {
loading.value = true
try {
const { data } = await bizList('org', { ...q.value, pageNum: page.pageNum, pageSize: page.pageSize })
rows.value = data?.rows || []; page.total = data?.total || 0
} catch { rows.value = []; page.total = 0 }
finally { loading.value = false }
}
function reset() { q.value = { orgType: 'executor', orgName: '', taxNo: '', status: '' }; page.pageNum = 1; load() }
function view(row) { currentRow.value = row; detailOpen.value = true }
function goPeople(row) {
router.push({ path: '/manager/executor-people', query: { orgId: row.orgId, orgName: row.orgName } })
}
onMounted(load)
</script>
<style scoped>
.manager-executor-orgs { padding: 16px; }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.filter-form { margin-bottom: 12px; }
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
</style>
@@ -0,0 +1,225 @@
<template>
<!--
角色: 🟨 合规 服务机构下人员 (manager/executor-people)
入口: org 表格行内人员管理按钮 (router.push orgId/orgName)
接口: bizList('person', q) / bizUpdate / bizAdd / executorImport / executorImportTemplate
-->
<div class="page-card manager-executor-people">
<div class="breadcrumb">
首页 / 服务机构管理 / 人员管理{{ orgName ? ' (' + orgName + ')' : '' }}
</div>
<el-form inline :model="q" class="filter-form">
<el-form-item label="姓名">
<el-input v-model="q.name" placeholder="姓名" clearable style="width:140px" />
</el-form-item>
<el-form-item label="手机号">
<el-input v-model="q.phone" placeholder="手机号" clearable style="width:140px" />
</el-form-item>
<el-form-item label="企业名称">
<el-input v-model="q.orgName" placeholder="企业名称" clearable style="width:180px" />
</el-form-item>
<el-form-item label="部门">
<el-input v-model="q.department" placeholder="部门" clearable style="width:140px" />
</el-form-item>
<el-form-item label="职务">
<el-input v-model="q.position" placeholder="职务" clearable style="width:140px" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查找</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
<div class="batch-bar">
<el-button type="primary" @click="goNew">新建人员</el-button>
<el-button @click="openImport">批量导入</el-button>
<span class="filter-tip">*批量导入模板与列表表项一致</span>
</div>
<el-table :data="rows" v-loading="loading" stripe border>
<el-table-column prop="name" label="姓名" min-width="100" />
<el-table-column prop="phone" label="手机号" min-width="130" />
<el-table-column prop="orgName" label="企业名称" min-width="180" show-overflow-tooltip />
<el-table-column prop="department" label="部门" min-width="100" />
<el-table-column prop="position" label="职务" min-width="100" />
<el-table-column label="状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="statusTagType(row.status)" disable-transitions>{{ row.status || '正常' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="120" fixed="right">
<template #default="{ row }">
<el-button link :type="isDisabled(row) ? 'success' : 'danger'" @click="onToggleStatus(row)">
{{ isDisabled(row) ? '启用' : '禁用' }}
</el-button>
</template>
</el-table-column>
</el-table>
<div class="pager">
<el-pagination
v-model:current-page="page.pageNum"
v-model:page-size="page.pageSize"
:total="page.total"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next, jumper"
@current-change="load"
@size-change="load"
/>
</div>
<el-dialog v-model="importOpen" title="批量导入" width="520px" :close-on-click-modal="false">
<el-form label-width="100px">
<el-form-item label="选择文件">
<el-upload ref="uploadRef" :auto-upload="false" :limit="1" accept=".xls,.xlsx" :on-change="onFileChange" :on-remove="onFileRemove">
<el-button>选择文件</el-button>
</el-upload>
</el-form-item>
<el-form-item label="">
<el-button link type="primary" @click="downloadTemplate">批量导入模板下载</el-button>
</el-form-item>
<el-form-item><span class="filter-tip">*导入列: 姓名/手机号/所属公司/部门/职务/角色</span></el-form-item>
</el-form>
<template #footer>
<el-button @click="importOpen = false">取消</el-button>
<el-button type="primary" :loading="importing" @click="submitImport">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { bizList, bizUpdate, importPerson, downloadImportTemplate } from '@/api/public'
import { ElMessage, ElMessageBox } from 'element-plus'
const route = useRoute()
const router = useRouter()
const orgId = computed(() => route.query.orgId ? Number(route.query.orgId) : null)
const orgName = computed(() => route.query.orgName || '')
const q = reactive({ name: '', phone: '', orgName: '', department: '', position: '' })
const rows = ref([])
const loading = ref(false)
const page = reactive({ pageNum: 1, pageSize: 10, total: 0 })
function statusTagType(s) {
if (s === '禁用' || s === '1') return 'danger'
return 'success'
}
function isDisabled(row) {
return row.status === '禁用' || row.status === '1'
}
async function load() {
loading.value = true
try {
const params = {
...q,
orgType: 'executor',
...(orgId.value ? { orgId: orgId.value } : {}),
pageNum: page.pageNum,
pageSize: page.pageSize,
}
const { data } = await bizList('person', params)
rows.value = (data && data.rows) || []
page.total = (data && data.total) || 0
} catch { rows.value = []; page.total = 0 }
finally { loading.value = false }
}
function reset() {
Object.assign(q, { name: '', phone: '', orgName: '', department: '', position: '' })
page.pageNum = 1
load()
}
async function onToggleStatus(row) {
const disabled = isDisabled(row)
const newStatus = disabled ? '正常' : '禁用'
const label = disabled ? '启用' : '禁用'
try {
await ElMessageBox.confirm(
`确定要${label}${row.name}」吗?`,
'确认',
{ type: 'warning' }
)
} catch { return }
try {
await bizUpdate('person', { personId: row.personId, status: newStatus })
ElMessage.success('已' + label)
load()
} catch (e) { ElMessage.error(e?.msg || '操作失败') }
}
// ========== 新建人员 ==========
function goNew() {
router.push({
path: '/manager/executor-people/new',
query: {
...(orgId.value ? { orgId: orgId.value } : {}),
...(orgName.value ? { orgName: orgName.value } : {})
}
})
}
// ========== 批量导入 ==========
const importOpen = ref(false)
const importing = ref(false)
const uploadRef = ref()
const importFile = ref(null)
function openImport() { importFile.value = null; importOpen.value = true }
function onFileChange(file) { importFile.value = file.raw }
function onFileRemove() { importFile.value = null }
async function downloadTemplate() {
try {
const res = await downloadImportTemplate('executor')
const blob = new Blob([res.data], { type: res.data.type || 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = '服务机构人员导入模板.xlsx'
a.click()
URL.revokeObjectURL(url)
} catch { ElMessage.error('模板下载失败') }
}
async function submitImport() {
if (!importFile.value) { ElMessage.warning('请先选择文件'); return }
importing.value = true
try {
const res = await importPerson('executor', importFile.value)
const payload = res.data || res
const ok = payload.ok ?? 0
const total = payload.total ?? 0
const failed = (payload.results || []).filter(r => !r.ok)
if (failed.length === 0) {
ElMessage.success(`导入成功 ${ok}/${total}`)
} else {
ElMessageBox.alert(
`成功 ${ok} 条, 失败 ${failed.length} 条:\n` +
failed.map(f => `${f.rowNo}${f.name || ''}: ${f.message}`).join('\n'),
'导入结果', { type: 'warning' }
)
}
importOpen.value = false
load()
} catch (e) {
ElMessage.error(e?.msg || '导入失败')
} finally {
importing.value = false
}
}
onMounted(load)
</script>
<style scoped>
.manager-executor-people { padding: 16px; }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.filter-form { margin-bottom: 12px; }
.batch-bar { display: flex; gap: 8px; margin-bottom: 12px; align-items: center; }
.filter-tip { color: #909399; font-size: 12px; }
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
</style>
+43 -51
View File
@@ -1,8 +1,8 @@
<template>
<div class="page-card">
<div class="desc">审核专家注册信息</div>
<div class="page-card manager-experts">
<div class="breadcrumb">首页 / 专家审核</div>
<!-- ========== 筛选区 (按原型 top=174: 姓名/手机号/工作单位/审核状态) ========== -->
<!-- ========== 筛选区 ( People.vue 风格一致) ========== -->
<el-form inline :model="q" class="filter-form">
<el-form-item label="姓名">
<el-input v-model="q.name" placeholder="" clearable style="width:160px" />
@@ -15,9 +15,10 @@
</el-form-item>
<el-form-item label="审核状态">
<el-select v-model="q.auditStatus" placeholder="请选择" clearable style="width:140px">
<el-option label="待审核" value="0" />
<el-option label="通过" value="1" />
<el-option label="退回" value="2" />
<el-option label="未提交" value="0" />
<el-option label="待审核" value="1" />
<el-option label="通过" value="2" />
<el-option label="拒绝" value="3" />
</el-select>
</el-form-item>
<el-form-item>
@@ -25,10 +26,8 @@
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
</div>
<!-- ========== 批量按钮区 (按原型 top=226: 新建专家/批量导入/批量审核/批量导入执业证书/批量导入职称证明) ========== -->
<div class="page-card">
<!-- ========== 批量按钮区 ( People.vue 风格一致) ========== -->
<div class="batch-bar">
<el-button type="primary" @click="openAdd">新建专家</el-button>
<el-button @click="openImport">批量导入</el-button>
@@ -65,7 +64,7 @@
</el-table-column>
<el-table-column label="审核状态" width="100" align="center">
<template #default="{ row }">
<span :class="['stage-tag', auditStatusClass(row.auditStatus)]">{{ auditStatusText(row.auditStatus) }}</span>
<audit-status-tag :status="row.auditStatus" />
</template>
</el-table-column>
<el-table-column prop="auditTime" label="审批时间" width="160" align="center">
@@ -77,16 +76,18 @@
<el-button link type="primary" @click="onAudit(row)">审核</el-button>
<el-button link type="primary" @click="onEdit(row)">修改</el-button>
<el-button link type="primary" @click="onView(row)">查看</el-button>
<el-button link :type="row.status === '1' ? 'danger' : 'success'" @click="row.status === '1' ? onDisable(row) : onEnable(row)">
{{ row.status === '1' ? '禁用' : '恢复' }}
<el-button link :type="row.status === 'N' ? 'success' : 'danger'" @click="row.status === 'N' ? onEnable(row) : onDisable(row)">
{{ row.status === 'N' ? '恢复' : '禁用' }}
</el-button>
<el-button link type="info" v-if="row.auditStatus === '2'" @click="onViewAuditOpinion(row)">退回</el-button>
<el-button link type="info" v-if="row.auditStatus === '3'" @click="onViewAuditOpinion(row)">拒绝</el-button>
<el-button link type="primary" v-if="row.practiceCertUrl || row.titleCertUrl || row.resumeUrl" @click="onDownloadAll(row)">下载</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination v-model:current-page="page.pageNum" v-model:page-size="page.pageSize" :total="page.total" :page-sizes="[10,20,50]" layout="total, sizes, prev, pager, next, jumper" @current-change="load" @size-change="load" />
<div class="pager">
<el-pagination v-model:current-page="page.pageNum" v-model:page-size="page.pageSize" :total="page.total" :page-sizes="[10,20,50]" layout="total, sizes, prev, pager, next, jumper" @current-change="load" @size-change="load" />
</div>
</div>
<!-- ========== Dialog: 新建/编辑专家 (按原型 top=239 确定/取消) ========== -->
@@ -121,8 +122,8 @@
<el-form :model="auditForm" label-width="100px">
<el-form-item label="审核结果">
<el-radio-group v-model="auditForm.result">
<el-radio value="1">通过</el-radio>
<el-radio value="2">退回</el-radio>
<el-radio value="2">通过</el-radio>
<el-radio value="3">拒绝</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="审核意见">
@@ -144,8 +145,8 @@
<el-form label-width="100px">
<el-form-item label="审核结果">
<el-radio-group v-model="importAuditResult">
<el-radio value="1">通过</el-radio>
<el-radio value="2">退回</el-radio>
<el-radio value="2">通过</el-radio>
<el-radio value="3">拒绝</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="审核意见">
@@ -190,7 +191,7 @@
<el-descriptions-item label="工作单位">{{ currentRow.workUnit }}</el-descriptions-item>
<el-descriptions-item label="科室">{{ currentRow.department }}</el-descriptions-item>
<el-descriptions-item label="职称">{{ currentRow.title }}</el-descriptions-item>
<el-descriptions-item label="审核状态">{{ auditStatusText(currentRow.auditStatus) }}</el-descriptions-item>
<el-descriptions-item label="审核状态"><audit-status-tag :status="currentRow.auditStatus" /></el-descriptions-item>
<el-descriptions-item label="执业证书">
<el-link v-if="currentRow.practiceCertUrl" type="primary" @click="onViewCert(currentRow.practiceCertUrl)">查看证书</el-link>
<span v-else>-</span>
@@ -208,6 +209,7 @@
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { bizList, bizAdd, bizUpdate } from '@/api/public'
import AuditStatusTag from '@/components/AuditStatusTag.vue'
import { ElMessage, ElMessageBox } from 'element-plus'
const q = ref({ name: '', phone: '', workUnit: '', auditStatus: '' })
@@ -233,17 +235,7 @@ function reset() {
}
function onSel(rs) { selection.value = rs }
// ========== 工具: 审核状态显示/颜色 ==========
function auditStatusText(s) {
if (s === '1') return '通过'
if (s === '2') return '退回'
return '待审核'
}
function auditStatusClass(s) {
if (s === '1') return 'stage-done'
if (s === '2') return 'stage-reject'
return 'stage-reviewing'
}
// auditStatusText / auditStatusClass 已删除: 改用 AuditStatusTag 组件
function fmtTime(d) {
if (!d) return ''
const dt = new Date(d)
@@ -285,7 +277,7 @@ async function submitEdit() {
await bizUpdate('expert', { ...editForm })
ElMessage.success('修改成功')
} else {
await bizAdd('expert', { ...editForm, auditStatus: '1', auditBy: 'admin', auditTime: new Date().toISOString().slice(0, 19).replace('T', ' ') })
await bizAdd('expert', { ...editForm, auditStatus: '2', auditBy: 'admin', auditTime: new Date().toISOString().slice(0, 19).replace('T', ' ') })
ElMessage.success('新建成功')
}
editOpen.value = false
@@ -296,12 +288,13 @@ async function submitEdit() {
// ========== 审核 dialog ==========
const auditOpen = ref(false)
const auditForm = reactive({ expertId: '', name: '', phone: '', result: '1', opinion: '' })
// result: '2'=通过, '3'=拒绝 (BizAuditStatusEnum: 0未提交 1待审核 2通过 3拒绝)
const auditForm = reactive({ expertId: '', name: '', phone: '', result: '2', opinion: '' })
function onAudit(row) {
auditForm.expertId = row.expertId
auditForm.name = row.name
auditForm.phone = row.phone
auditForm.result = '1'
auditForm.result = '2'
auditForm.opinion = ''
auditOpen.value = true
}
@@ -315,7 +308,7 @@ async function submitAudit() {
auditBy: 'admin', // 后端应取登录用户
auditTime: new Date().toISOString().slice(0, 19).replace('T', ' ')
})
ElMessage.success(auditForm.result === '1' ? '审核通过' : '已退回')
ElMessage.success(auditForm.result === '2' ? '审核通过' : '已拒绝')
auditOpen.value = false
load()
} catch { ElMessage.error('审核失败') }
@@ -329,7 +322,7 @@ async function onBatchAudit() {
submitting.value = true
let ok = 0
for (const r of selection.value) {
try { await bizUpdate('expert', { expertId: r.expertId, auditStatus: '1', auditOpinion: '批量通过', auditBy: 'admin', auditTime: new Date().toISOString().slice(0, 19).replace('T', ' ') }); ok++ } catch {}
try { await bizUpdate('expert', { expertId: r.expertId, auditStatus: '2', auditOpinion: '批量通过', auditBy: 'admin', auditTime: new Date().toISOString().slice(0, 19).replace('T', ' ') }); ok++ } catch {}
}
submitting.value = false
ElMessage.success(`批量审核通过 ${ok}/${selection.value.length}`)
@@ -339,10 +332,10 @@ async function onBatchAudit() {
// ========== 禁用/恢复 ==========
async function onDisable(row) {
try { await bizUpdate('expert', { expertId: row.expertId, status: '0' }); ElMessage.success('已禁用'); load() } catch { ElMessage.error('禁用失败') }
try { await bizUpdate('expert', { expertId: row.expertId, status: 'N' }); ElMessage.success('已禁用'); load() } catch { ElMessage.error('禁用失败') }
}
async function onEnable(row) {
try { await bizUpdate('expert', { expertId: row.expertId, status: '1' }); ElMessage.success('已恢复'); load() } catch { ElMessage.error('恢复失败') }
try { await bizUpdate('expert', { expertId: row.expertId, status: 'Y' }); ElMessage.success('已恢复'); load() } catch { ElMessage.error('恢复失败') }
}
// ========== 查看详情 ==========
@@ -350,11 +343,11 @@ const viewOpen = ref(false)
const currentRow = ref(null)
function onView(row) { currentRow.value = row; viewOpen.value = true }
// ========== 已退回: 查看退回意见 ==========
// ========== 已拒绝: 查看拒绝意见 ==========
function onViewAuditOpinion(row) {
ElMessageBox.alert(
`退回时间: ${fmtTime(row.auditTime) || '-'}\n审批人: ${row.auditBy || '-'}\n退回意见: ${row.auditOpinion || '(无)'}`,
`专家 ${row.name} 审核已退回`,
`拒绝时间: ${fmtTime(row.auditTime) || '-'}\n审批人: ${row.auditBy || '-'}\n拒绝意见: ${row.auditOpinion || '(无)'}`,
`专家 ${row.name} 审核已拒绝`,
{ type: 'warning', confirmButtonText: '知道了' }
).catch(() => {})
}
@@ -382,12 +375,12 @@ function onDownloadAll(row) {
// ========== 批量导入 dialog ==========
const importOpen = ref(false)
const importAuditResult = ref('1')
const importAuditResult = ref('2')
const importAuditOpinion = ref('')
const importFileUrl = ref('')
const importing = ref(false)
function openImport() {
importAuditResult.value = '1'
importAuditResult.value = '2'
importAuditOpinion.value = ''
importFileUrl.value = ''
importOpen.value = true
@@ -400,7 +393,7 @@ async function submitImport() {
importing.value = true
await new Promise(r => setTimeout(r, 500))
importing.value = false
ElMessage.info(`已提交批量导入任务 (审核结果: ${importAuditResult.value === '1' ? '通过' : '退回'}) — 待后端实现`)
ElMessage.info(`已提交批量导入任务 (审核结果: ${importAuditResult.value === '2' ? '通过' : '拒绝'}) — 待后端实现`)
importOpen.value = false
}
@@ -434,13 +427,12 @@ onMounted(load)
</script>
<style scoped>
.page-card { background: #fff; border: 1px solid #ebeef5; border-radius: 6px; padding: 16px; margin-bottom: 12px; }
.desc { color: #606266; font-size: 13px; margin-bottom: 12px; }
.filter-form .el-form-item { margin-right: 12px; margin-bottom: 8px; }
.batch-bar { display: flex; gap: 8px; flex-wrap: wrap; }
/* 整页面板 (与 People.vue 风格一致) */
.manager-experts { padding: 16px; }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.filter-form { margin-bottom: 12px; }
.batch-bar { display: flex; gap: 8px; margin-bottom: 12px; align-items: center; flex-wrap: wrap; }
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
.stage-tag { display: inline-block; padding: 2px 10px; border-radius: 3px; font-size: 12px; border: 1px solid; }
.stage-done { color: #67c23a; border-color: #c2e7b0; background: #f0f9eb; }
.stage-reviewing { color: #e6a23c; border-color: #f5dab1; background: #fdf6ec; }
.stage-reject { color: #f56c6c; border-color: #fbc4c4; background: #fef0f0; }
/* stage-* CSS 已删除: 改用 AuditStatusTag 组件 (el-tag 自带颜色) */
</style>
+7 -4
View File
@@ -1,9 +1,9 @@
<template>
<div class="page-card manager-meeting-new">
<h2 style="margin-bottom:16px">新建会议</h2>
<div class="breadcrumb">首页 / 会议管理 / 新建会议</div>
<p class="page-sub" v-if="preselectProjectId">已选择项目: {{ preselectProjectId }}</p>
<el-form :model="form" :rules="rules" ref="formRef" label-width="120px" style="max-width:700px">
<el-form :model="form" :rules="rules" ref="formRef" label-width="120px" class="meeting-form">
<el-form-item label="项目编号" prop="projectNo">
<el-input v-model="form.projectNo" :disabled="!!preselectProjectId" placeholder="项目编号" />
</el-form-item>
@@ -100,13 +100,16 @@ onMounted(async () => {
if (preselectProjectId) {
try {
const r = await bizGet('project', preselectProjectId)
form.projectNo = r.data?.projectNo || ''
form.projectNo = (r.data && r.data.projectNo) || ''
} catch (e) {}
}
})
</script>
<style scoped>
/* 整页面板 (与 People.vue 风格一致) */
.manager-meeting-new { padding: 16px; }
.page-sub { color: #909399; font-size: 13px; margin-bottom: 12px; }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.page-sub { color: #909399; font-size: 13px; margin-bottom: 16px; }
.meeting-form { max-width: 700px; }
</style>
+87 -100
View File
@@ -1,89 +1,80 @@
<template>
<div class="manager-meetings">
<!-- ========== 筛选区 (严格按 manager 角色原型 会议管理_1.html 第一行 6 + 第二行 3 ) ========== -->
<div class="page-card">
<el-form inline :model="q" class="filter-form">
<el-form-item label="项目编号">
<el-input v-model="q.projectNo" placeholder="" clearable style="width:140px" />
</el-form-item>
<el-form-item label="会议ID">
<el-input v-model="q.meetingId" placeholder="" clearable style="width:140px" />
</el-form-item>
<el-form-item label="会议名称">
<el-input v-model="q.meetingName" placeholder="" clearable style="width:160px" />
</el-form-item>
<el-form-item label="第?期">
<el-input v-model="q.periodNo" placeholder="" clearable style="width:80px" />
</el-form-item>
<el-form-item label="会议时间">
<el-date-picker v-model="q.startTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" style="width:170px" />
<span style="margin:0 4px"></span>
<el-date-picker v-model="q.endTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" style="width:170px" />
</el-form-item>
<el-form-item label="项目形式">
<el-select v-model="q.projectForm" placeholder="请选择" clearable style="width:140px">
<el-option label="线上" value="线上" />
<el-option label="线下" value="线下" />
<el-option label="线上+线下" value="线上+线下" />
<el-option label="其他" value="其他" />
</el-select>
</el-form-item>
<el-form-item label="当前阶段">
<el-select v-model="q.currentStage" placeholder="请选择" clearable style="width:140px">
<el-option label="未执行" value="未执行" />
<el-option label="执行中" value="执行中" />
<el-option label="已执行" value="已执行" />
<el-option label="已完结" value="已完结" />
</el-select>
</el-form-item>
<el-form-item label="备注">
<el-input v-model="q.remark" placeholder="" clearable style="width:140px" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查找</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
<div class="page-card manager-meetings">
<div class="breadcrumb">首页 / 会议管理</div>
<!-- ========== 筛选区 ( People.vue 风格一致) ========== -->
<el-form inline :model="q" class="filter-form">
<el-form-item label="项目编号"><el-input v-model="q.projectNo" placeholder="" clearable style="width:140px" /></el-form-item>
<el-form-item label="会议ID"><el-input v-model="q.meetingId" placeholder="" clearable style="width:140px" /></el-form-item>
<el-form-item label="会议名称"><el-input v-model="q.meetingName" placeholder="" clearable style="width:160px" /></el-form-item>
<el-form-item label="第?期"><el-input v-model="q.periodNo" placeholder="" clearable style="width:80px" /></el-form-item>
<el-form-item label="会议时间">
<el-date-picker v-model="q.startTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" style="width:170px" />
<span style="margin:0 4px"></span>
<el-date-picker v-model="q.endTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" style="width:170px" />
</el-form-item>
<el-form-item label="项目形式">
<el-select v-model="q.projectForm" placeholder="请选择" clearable style="width:140px">
<el-option label="线上" value="线上" />
<el-option label="线下" value="线下" />
<el-option label="线上+线下" value="线上+线下" />
<el-option label="其他" value="其他" />
</el-select>
</el-form-item>
<el-form-item label="当前阶段">
<el-select v-model="q.currentStage" placeholder="请选择" clearable style="width:140px">
<el-option label="未执行" value="未执行" />
<el-option label="执行中" value="执行中" />
<el-option label="已执行" value="已执行" />
<el-option label="已完结" value="已完结" />
</el-select>
</el-form-item>
<el-form-item label="备注"><el-input v-model="q.remark" placeholder="" clearable style="width:140px" /></el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查找</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
<!-- ========== 批量提交按钮 (manager 角色专有) ========== -->
<div class="batch-bar">
<el-button type="primary" plain :disabled="!selectedIds.length" @click="onBatchSubmit">批量提交</el-button>
<span v-if="selectedIds.length" class="filter-tip">已选 {{ selectedIds.length }} </span>
</div>
<!-- ========== 表格区 ( manager 角色原型 会议管理_1.html: 操作列 4 按钮 查看/修改/提交/复制) ========== -->
<div class="page-card">
<!-- 批量提交按钮 (manager 角色才有) -->
<div class="batch-bar" v-if="selectedIds.length">
<el-button type="primary" plain @click="onBatchSubmit" :disabled="!selectedIds.length">批量提交</el-button>
<span class="selected-tip">已选 {{ selectedIds.length }} </span>
</div>
<el-table :data="rows" v-loading="loading" stripe border @selection-change="onSelectionChange">
<el-table-column type="selection" width="44" />
<el-table-column prop="projectNo" label="项目编号" width="130" fixed />
<el-table-column prop="meetingId" label="会议ID" width="120" align="center" />
<el-table-column prop="projectForm" label="项目形式" width="100" align="center" />
<el-table-column prop="meetingName" label="会议名称" min-width="180" show-overflow-tooltip />
<el-table-column label="会议开始时间" width="160" align="center">
<template #default="{ row }">{{ fmtTime(row.startTime) }}</template>
</el-table-column>
<el-table-column label="会议结束时间" width="160" align="center">
<template #default="{ row }">{{ fmtTime(row.endTime) }}</template>
</el-table-column>
<el-table-column prop="totalPeriods" label="总期数" width="80" align="center" />
<el-table-column label="期数" width="80" align="center">
<template #default="{ row }"> {{ row.periodNo || 0 }} </template>
</el-table-column>
<el-table-column prop="currentStage" label="当前阶段" width="100" align="center">
<template #default="{ row }">
<span :class="['stage-tag', 'stage-' + stageClass(row.currentStage)]">{{ row.currentStage }}</span>
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" min-width="120" show-overflow-tooltip />
<el-table-column label="操作" width="240" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="onView(row)">查看</el-button>
<el-button link type="primary" @click="onEdit(row)">修改</el-button>
<el-button link type="primary" @click="onSubmit(row)">提交</el-button>
<el-button link type="primary" @click="onCopy(row)">复制</el-button>
</template>
</el-table-column>
</el-table>
<!-- ========== 表格 ( People.vue 风格一致, page-card 包裹) ========== -->
<el-table :data="rows" v-loading="loading" stripe border @selection-change="onSelectionChange">
<el-table-column type="selection" width="44" />
<el-table-column prop="projectNo" label="项目编号" width="130" fixed />
<el-table-column prop="meetingId" label="会议ID" width="120" align="center" />
<el-table-column prop="projectForm" label="项目形式" width="100" align="center" />
<el-table-column prop="meetingName" label="会议名称" min-width="180" show-overflow-tooltip />
<el-table-column label="会议开始时间" width="160" align="center">
<template #default="{ row }">{{ fmtTime(row.startTime) }}</template>
</el-table-column>
<el-table-column label="会议结束时间" width="160" align="center">
<template #default="{ row }">{{ fmtTime(row.endTime) }}</template>
</el-table-column>
<el-table-column prop="totalPeriods" label="总期数" width="80" align="center" />
<el-table-column label="期数" width="80" align="center">
<template #default="{ row }"> {{ row.periodNo || 0 }} </template>
</el-table-column>
<el-table-column prop="currentStage" label="当前阶段" width="100" align="center">
<template #default="{ row }">
<span :class="['stage-tag', 'stage-' + stageClass(row.currentStage)]">{{ row.currentStage }}</span>
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" min-width="120" show-overflow-tooltip />
<el-table-column label="操作" width="240" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="onView(row)">查看</el-button>
<el-button link type="primary" @click="onEdit(row)">修改</el-button>
<el-button link type="primary" @click="onSubmit(row)">提交</el-button>
<el-button link type="primary" @click="onCopy(row)">复制</el-button>
</template>
</el-table-column>
</el-table>
<div class="pager">
<el-pagination
v-model:current-page="page.pageNum"
v-model:page-size="page.pageSize"
@@ -289,19 +280,15 @@ onMounted(load)
</script>
<style scoped>
/* 整页面板 (与 People.vue 风格一致) */
.manager-meetings { padding: 16px; }
.page-card {
background: #fff;
border-radius: 6px;
padding: 16px;
border: 1px solid #ebeef5;
margin-bottom: 12px;
}
.filter-form .el-form-item { margin-right: 12px; margin-bottom: 12px; }
.batch-bar { margin-bottom: 12px; display: flex; align-items: center; gap: 8px; }
.batch-bar .selected-tip { color: #909399; font-size: 13px; margin-left: 8px; }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.filter-form { margin-bottom: 12px; }
.batch-bar { display: flex; gap: 8px; margin-bottom: 12px; align-items: center; }
.filter-tip { color: #909399; font-size: 12px; }
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
/* 当前阶段 tag 颜色 */
/* 当前阶段 tag 颜色 (业务专用, 其它页没有) */
.stage-tag {
display: inline-block;
padding: 2px 10px;
@@ -309,11 +296,11 @@ onMounted(load)
font-size: 12px;
border: 1px solid;
}
.stage-pending { color: #909399; border-color: #dcdfe6; background: #f5f7fa; }
.stage-running { color: #409eff; border-color: #b3d8ff; background: #ecf5ff; }
.stage-done { color: #67c23a; border-color: #c2e7b0; background: #f0f9eb; }
.stage-pending { color: #909399; border-color: #dcdfe6; background: #f5f7fa; }
.stage-running { color: var(--brand-primary); border-color: #b3c5e0; background: #eef1f8; }
.stage-done { color: #67c23a; border-color: #c2e7b0; background: #f0f9eb; }
.stage-reviewing { color: #e6a23c; border-color: #f5dab1; background: #fdf6ec; }
.stage-waiting { color: #f56c6c; border-color: #fbc4c4; background: #fef0f0; }
.stage-frozen { color: #909399; border-color: #dcdfe6; background: #f5f7fa; opacity: 0.6; }
.stage-default { color: #606266; border-color: #dcdfe6; background: #fafafa; }
.stage-waiting { color: #ff4d4f; border-color: #fbc4c4; background: #fef0f0; }
.stage-frozen { color: #909399; border-color: #dcdfe6; background: #f5f7fa; opacity: 0.6; }
.stage-default { color: #606266; border-color: #dcdfe6; background: #fafafa; }
</style>
+63 -77
View File
@@ -4,69 +4,66 @@
重构说明: manager/SupportUnits.vue (仅看支持单位) 改为可看所有公司
接口: bizList('org', q) / bizUpdate
-->
<div class="manager-orgs">
<div class="page-card">
<div class="page-title">公司查看</div>
<div class="desc">公司管理 &gt; 公司查看 (赞助方 + 执行方)</div>
<el-form inline :model="q">
<el-form-item label="公司类型:">
<el-select v-model="q.orgType" placeholder="全部" clearable style="width:140px">
<el-option label="赞助方" value="sponsor" />
<el-option label="执行方" value="executor" />
</el-select>
</el-form-item>
<el-form-item label="公司名称:">
<el-input v-model="q.orgName" placeholder="输入公司名称" clearable />
</el-form-item>
<el-form-item label="税号/统一社会信用代码:">
<el-input v-model="q.taxNo" placeholder="输入税号" clearable />
</el-form-item>
<el-form-item label="合作状态:">
<el-select v-model="q.status" placeholder="请选择" clearable>
<el-option label="正常/合作中" value="0" />
<el-option label="合作中" value="合作中" />
<el-option label="禁用" value="禁用" />
</el-select>
</el-form-item>
<el-form-item>
<el-button class="action-btn" @click="load">查找</el-button>
<el-button class="action-btn secondary" @click="reset">重置</el-button>
</el-form-item>
</el-form>
</div>
<div class="page-card manager-orgs">
<div class="breadcrumb">首页 / 公司管理</div>
<div class="page-card">
<div class="page-toolbar">
<el-button class="action-btn secondary" @click="goBack">返回</el-button>
</div>
<el-table :data="rows" v-loading="loading" stripe border>
<el-table-column type="index" label="#" width="50" />
<el-table-column label="类型" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.orgType === 'sponsor' ? 'success' : 'primary'" disable-transitions>
{{ row.orgType === 'sponsor' ? '赞助方' : '执行方' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="orgName" label="公司名称" min-width="200" show-overflow-tooltip />
<el-table-column prop="address" label="公司地址" min-width="220" show-overflow-tooltip />
<el-table-column prop="taxNo" label="税号/统一社会信用代码" min-width="180" />
<el-table-column label="合作状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="statusTagType(row.status)" disable-transitions>{{ row.status }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建日期" width="120" align="center">
<template #default="{ row }">{{ row.createTime ? String(row.createTime).slice(0,10) : '' }}</template>
</el-table-column>
<el-table-column prop="contactName" label="联系人" width="100" align="center" />
<el-table-column prop="contactPhone" label="联系电话" width="130" align="center" />
<el-table-column label="操作" width="160" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="view(row)">查看</el-button>
</template>
</el-table-column>
</el-table>
<!-- ========== 筛选区 ( People.vue 风格一致) ========== -->
<el-form inline :model="q" class="filter-form">
<el-form-item label="公司类型">
<el-select v-model="q.orgType" placeholder="全部" clearable style="width:140px">
<el-option label="赞助方" value="sponsor" />
<el-option label="执行方" value="executor" />
</el-select>
</el-form-item>
<el-form-item label="公司名称">
<el-input v-model="q.orgName" placeholder="输入公司名称" clearable />
</el-form-item>
<el-form-item label="税号/统一社会信用代码">
<el-input v-model="q.taxNo" placeholder="输入税号" clearable />
</el-form-item>
<el-form-item label="合作状态">
<el-select v-model="q.status" placeholder="请选择" clearable>
<el-option label="正常/合作中" value="0" />
<el-option label="合作中" value="合作中" />
<el-option label="禁用" value="禁用" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查找</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
<!-- ========== 表格 ( People.vue 风格一致, page-card 包裹) ========== -->
<el-table :data="rows" v-loading="loading" stripe border>
<el-table-column type="index" label="#" width="50" />
<el-table-column label="类型" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.orgType === 'sponsor' ? 'success' : 'primary'" disable-transitions>
{{ row.orgType === 'sponsor' ? '赞助方' : '执行方' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="orgName" label="公司名称" min-width="200" show-overflow-tooltip />
<el-table-column prop="address" label="公司地址" min-width="220" show-overflow-tooltip />
<el-table-column prop="taxNo" label="税号/统一社会信用代码" min-width="180" />
<el-table-column label="合作状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="statusTagType(row.status)" disable-transitions>{{ row.status }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建日期" width="120" align="center">
<template #default="{ row }">{{ row.createTime ? String(row.createTime).slice(0,10) : '' }}</template>
</el-table-column>
<el-table-column prop="contactName" label="联系人" width="100" align="center" />
<el-table-column prop="contactPhone" label="联系电话" width="130" align="center" />
<el-table-column label="操作" width="160" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="view(row)">查看</el-button>
</template>
</el-table-column>
</el-table>
<div class="pager">
<el-pagination v-model:current-page="page.pageNum" v-model:page-size="page.pageSize" :total="page.total" :page-sizes="[10,20,50]" layout="total, sizes, prev, pager, next, jumper" @current-change="load" @size-change="load" />
</div>
@@ -129,20 +126,9 @@ onMounted(load)
</script>
<style scoped>
/* 整页面板 (与 People.vue 风格一致) */
.manager-orgs { padding: 16px; }
.page-card { background: #fff; border-radius: 6px; padding: 16px; border: 1px solid #ebeef5; margin-bottom: 12px; }
.page-title { font-size: 20px; font-weight: 600; margin-bottom: 4px; padding-left: 10px; border-left: 3px solid var(--brand-primary); }
.desc { color: #909399; font-size: 13px; margin-bottom: 12px; }
.page-toolbar { display: flex; gap: 12px; margin-bottom: 12px; flex-wrap: wrap; align-items: center; }
.page-toolbar :deep(.action-btn) {
height: 36px; padding: 0 20px; font-size: 14px; border-radius: 4px;
background: var(--brand-primary); color: #fff; border: 1px solid var(--brand-primary);
display: inline-flex; align-items: center; justify-content: center; white-space: nowrap;
transition: opacity .15s; margin-left: 0;
}
.page-toolbar :deep(.action-btn:hover) { opacity: .85; }
.page-toolbar :deep(.action-btn.secondary) {
background: #fff; color: var(--brand-primary); border: 1px solid var(--brand-primary);
}
.page-toolbar :deep(.action-btn.secondary:hover) { background: rgba(30,58,138,.06); }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.filter-form { margin-bottom: 12px; }
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
</style>
+112 -112
View File
@@ -1,95 +1,92 @@
<template>
<div class="manager-plans">
<div class="page-card">
<div class="page-title">项目策划方案管理</div>
<div class="desc">审核 / 修改 / 结算 项目策划方案</div>
<el-form inline :model="q">
<!-- 按原型 top=171-178 的筛选区: 项目编号(52) / 项目编号(141) / 策划方案名称(254) / 项目方向(519) / 策划方案名称(591) / 状态(939) / 备注(1138) / 备注(1177) -->
<el-form-item label="项目编号:"><el-input v-model="q.projectNo" placeholder="输入项目编号" clearable /></el-form-item>
<el-form-item label="策划方案名称:"><el-input v-model="q.planName" placeholder="输入策划方案名称" clearable /></el-form-item>
<el-form-item label="项目方向:">
<el-select v-model="q.planDirection" placeholder="请选择" clearable>
<el-option label="专项计划一" value="专项计划一" /><el-option label="专项计划二" value="专项计划二" />
</el-select>
</el-form-item>
<el-form-item label="项目类别:">
<el-select v-model="q.planCategory" placeholder="请选择" clearable>
<el-option label="共识" value="共识" /><el-option label="对外交流" value="对外交流" />
<el-option label="学术项目" value="学术项目" /><el-option label="科研项目" value="科研项目" />
<el-option label="指南" value="指南" /><el-option label="会议项目" value="会议项目" />
</el-select>
</el-form-item>
<el-form-item label="状态:">
<el-select v-model="q.status" placeholder="请选择" clearable>
<el-option label="待审核" value="0" /><el-option label="审核通过" value="1" /><el-option label="已退回" value="2" />
</el-select>
</el-form-item>
<el-form-item label="是否结算:">
<el-select v-model="q.isSettled" placeholder="请选择" clearable>
<el-option label="已结算" value="1" /><el-option label="未结算" value="0" />
</el-select>
</el-form-item>
<el-form-item label="备注:"><el-input v-model="q.remark" placeholder="输入备注" clearable /></el-form-item>
<el-form-item>
<el-button class="action-btn" @click="load">查找</el-button>
<el-button class="action-btn secondary" @click="reset">重置</el-button>
</el-form-item>
</el-form>
<div class="filter-tip">*批量审核策划案名称投稿ID审核结果审核意见只能审核不通过</div>
<div class="page-card manager-plans">
<div class="breadcrumb">首页 / 项目策划方案</div>
<div class="hint-list">
<p>*批量审核策划案名称投稿ID审核结果审核意见只能审核不通过</p>
</div>
<div class="page-card">
<!-- 批量按钮 (按原型 left: 批量审核 266 / 批量结算 432) -->
<div class="page-toolbar">
<el-button class="action-btn" :disabled="!selection.length" @click="batchAudit">
批量审核<span v-if="selection.length" class="badge">{{ selection.length }}</span>
</el-button>
<el-button class="action-btn secondary" :disabled="!selection.length" @click="batchSettle">
批量结算<span v-if="selection.length" class="badge">{{ selection.length }}</span>
</el-button>
</div>
<!-- 列头顺序严格按原型 top=294-300 left: 策划方案名称 342 / 项目方向 569 / 项目类别 739 / 项目形式 864 / 设计文件 999 / 状态 1133 / 是否结算 1246 / 项目编号 1348 / 备注 1492 / 操作 1640 -->
<el-table :data="rows" v-loading="loading" stripe border @selection-change="onSel">
<el-table-column type="selection" width="44" />
<el-table-column prop="planName" label="策划方案名称" min-width="200" fixed show-overflow-tooltip />
<el-table-column prop="planDirection" label="项目方向" min-width="220" show-overflow-tooltip />
<el-table-column prop="planCategory" label="项目类别" width="100" align="center" />
<el-table-column prop="projectForm" label="项目形式" width="100" align="center" />
<el-table-column label="设计文件" width="160" align="center">
<template #default="{ row }">
<template v-if="row.designFileUrl">
<el-button link type="primary" @click="onFile(row)">查看</el-button>
<el-button link type="primary" @click="downloadFile(row)">下载</el-button>
</template>
<span v-else style="color:#c0c4cc">-</span>
<!-- ========== 筛选区 ( People.vue 风格一致) ========== -->
<el-form inline :model="q" class="filter-form">
<el-form-item label="项目编号"><el-input v-model="q.projectNo" placeholder="输入项目编号" clearable /></el-form-item>
<el-form-item label="策划方案名称"><el-input v-model="q.planName" placeholder="输入策划方案名称" clearable /></el-form-item>
<el-form-item label="项目方向">
<el-select v-model="q.planDirection" placeholder="请选择" clearable>
<el-option label="专项计划一" value="专项计划一" /><el-option label="专项计划二" value="专项计划二" />
</el-select>
</el-form-item>
<el-form-item label="项目类别">
<el-select v-model="q.planCategory" placeholder="请选择" clearable>
<el-option label="共识" value="共识" /><el-option label="对外交流" value="对外交流" />
<el-option label="学术项目" value="学术项目" /><el-option label="科研项目" value="科研项目" />
<el-option label="指南" value="指南" /><el-option label="会议项目" value="会议项目" />
</el-select>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="q.status" placeholder="请选择" clearable>
<el-option label="待审核" value="0" /><el-option label="审核通过" value="1" /><el-option label="已退回" value="2" />
</el-select>
</el-form-item>
<el-form-item label="是否结算">
<el-select v-model="q.isSettled" placeholder="请选择" clearable>
<el-option label="已结算" value="1" /><el-option label="未结算" value="0" />
</el-select>
</el-form-item>
<el-form-item label="备注"><el-input v-model="q.remark" placeholder="输入备注" clearable /></el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查找</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
<!-- ========== 批量按钮 ( People.vue 风格一致) ========== -->
<div class="batch-bar">
<el-button type="primary" :disabled="!selection.length" @click="batchAudit">批量审核</el-button>
<el-button type="primary" :disabled="!selection.length" @click="batchSettle">批量结算</el-button>
<span v-if="selection.length" class="filter-tip">已选 {{ selection.length }} </span>
</div>
<!-- ========== 表格 ( People.vue 风格一致, page-card 包裹) ========== -->
<el-table :data="rows" v-loading="loading" stripe border @selection-change="onSel">
<el-table-column type="selection" width="44" />
<el-table-column prop="planName" label="策划方案名称" min-width="200" fixed show-overflow-tooltip />
<el-table-column prop="planDirection" label="项目方向" min-width="220" show-overflow-tooltip />
<el-table-column prop="planCategory" label="项目类别" width="100" align="center" />
<el-table-column prop="projectForm" label="项目形式" width="100" align="center" />
<el-table-column label="设计文件" width="160" align="center">
<template #default="{ row }">
<template v-if="row.designFileUrl">
<el-button link type="primary" @click="onFile(row)">查看</el-button>
<el-button link type="primary" @click="downloadFile(row)">下载</el-button>
</template>
</el-table-column>
<el-table-column label="状态" width="100" align="center">
<template #default="{ row }">
<el-tag size="small" :type="row.status==='1' ? 'success' : row.status==='2' ? 'danger' : 'warning'">
{{ row.status==='1' ? '审核通过' : row.status==='2' ? '已退回' : '待审核' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="是否结算" width="100" align="center">
<template #default="{ row }">
<el-tag size="small" :type="row.isSettled==='1' ? 'success' : 'warning'">
{{ row.isSettled==='1' ? '已结算' : '未结算' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="projectNo" label="项目编号" width="140" align="center" />
<el-table-column prop="remark" label="备注" min-width="160" show-overflow-tooltip />
<!-- 操作列按原型 left 顺序: 审核 1586 / 修改 1640 / 结算 1693 -->
<el-table-column label="操作" width="240" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="onAudit(row)">审核</el-button>
<el-button link type="primary" @click="onEdit(row)">修改</el-button>
<el-button link type="primary" @click="onSettle(row)" v-if="row.isSettled!=='1'">结算</el-button>
<el-button link type="primary" v-if="row.designFileUrl" @click="downloadFile(row)">下载</el-button>
</template>
</el-table-column>
</el-table>
<span v-else style="color:#c0c4cc">-</span>
</template>
</el-table-column>
<el-table-column label="状态" width="100" align="center">
<template #default="{ row }">
<el-tag size="small" :type="row.status==='1' ? 'success' : row.status==='2' ? 'danger' : 'warning'">
{{ row.status==='1' ? '审核通过' : row.status==='2' ? '已退回' : '待审核' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="是否结算" width="100" align="center">
<template #default="{ row }">
<el-tag size="small" :type="row.isSettled==='1' ? 'success' : 'warning'">
{{ row.isSettled==='1' ? '已结算' : '未结算' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="projectNo" label="项目编号" width="140" align="center" />
<el-table-column prop="remark" label="备注" min-width="160" show-overflow-tooltip />
<el-table-column label="操作" width="240" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="onAudit(row)">审核</el-button>
<el-button link type="primary" @click="onEdit(row)">修改</el-button>
<el-button link type="primary" @click="onSettle(row)" v-if="row.isSettled!=='1'">结算</el-button>
<el-button link type="primary" v-if="row.designFileUrl" @click="downloadFile(row)">下载</el-button>
</template>
</el-table-column>
</el-table>
<div class="pager">
<el-pagination v-model:current-page="page.pageNum" v-model:page-size="page.pageSize" :total="page.total" :page-sizes="[10,20,50]" layout="total, sizes, prev, pager, next, jumper" @current-change="load" @size-change="load" />
</div>
@@ -282,31 +279,34 @@ onMounted(load)
</script>
<style scoped>
/* 整页面板 (与 People.vue 风格一致) */
.manager-plans { padding: 16px; }
.page-card { background: #fff; border-radius: 6px; padding: 16px; border: 1px solid #ebeef5; margin-bottom: 12px; }
.page-title { font-size: 20px; font-weight: 600; margin-bottom: 4px; padding-left: 10px; border-left: 3px solid var(--brand-primary); }
.desc { color: #909399; font-size: 13px; margin-bottom: 12px; }
.filter-tip { color: #ff4d4f; font-size: 12px; margin-top: 8px; }
.page-toolbar { display: flex; gap: 12px; margin-bottom: 12px; flex-wrap: wrap; align-items: center; }
.page-toolbar :deep(.action-btn) {
height: 36px; padding: 0 20px; font-size: 14px; border-radius: 4px;
background: var(--brand-primary); color: #fff; border: 1px solid var(--brand-primary);
display: inline-flex; align-items: center; justify-content: center; white-space: nowrap;
transition: opacity .15s; margin-left: 0;
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.page-title {
font-size: 22px;
font-weight: 600;
color: #1a1a1a;
margin: 0 0 6px;
padding-left: 10px;
border-left: 3px solid var(--brand-primary);
}
.page-toolbar :deep(.action-btn:hover) { opacity: .85; }
.page-toolbar :deep(.action-btn.secondary) {
background: #fff; color: var(--brand-primary); border: 1px solid var(--brand-primary);
.page-sub { font-size: 13px; color: #909399; margin: 0 0 16px; }
.filter-form { margin-bottom: 12px; }
.batch-bar { display: flex; gap: 8px; margin-bottom: 12px; align-items: center; }
.filter-tip { color: #909399; font-size: 12px; }
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
/* 红字提示 (与 People.vue 一致) */
.hint-list {
background: #fff;
border: 1px solid #f0f0f0;
border-radius: 8px;
padding: 12px 20px;
margin-bottom: 16px;
font-size: 12px;
color: #ff4d4f;
line-height: 1.8;
}
.page-toolbar :deep(.action-btn.secondary:hover) { background: rgba(30,58,138,.06); }
.page-toolbar :deep(.action-btn.is-disabled),
.page-toolbar :deep(.action-btn:disabled) { background: #f5f7fa; color: #c0c4cc; border-color: #e4e7ed; opacity: 1; cursor: not-allowed; }
.page-toolbar :deep(.action-btn.secondary.is-disabled),
.page-toolbar :deep(.action-btn.secondary:disabled) { background: #fff; color: #c0c4cc; border-color: #e4e7ed; }
.page-toolbar :deep(.action-btn .badge) {
display: inline-block; margin-left: 6px; padding: 0 6px; min-width: 18px; height: 18px;
line-height: 18px; font-size: 11px; text-align: center;
background: #fff; color: var(--brand-primary); border-radius: 9px;
}
.page-toolbar :deep(.action-btn.secondary .badge) { background: var(--brand-primary); color: #fff; }
.hint-list p { margin-bottom: 4px; }
.hint-list p:last-child { margin-bottom: 0; }
</style>
+92 -93
View File
@@ -1,89 +1,84 @@
<template>
<div class="page-card">
<!-- 面包屑 ( sponsor 端一致: 首页 / XXX / 当前页) -->
<!-- 面包屑 -->
<div class="breadcrumb">
首页 / 项目管理 / <span class="current">{{ isEdit ? '编辑项目' : '新建项目' }}</span>
</div>
<!-- 第一区块项目信息 -->
<el-card shadow="never" class="new-card">
<el-form :model="form" :rules="rules" ref="formRef" label-width="120px" class="project-form">
<!-- ========== 第一区块项目信息 ========== -->
<div class="new-card-title">项目信息</div>
<el-form :model="form" :rules="rules" ref="formRef" label-width="120px">
<el-row :gutter="12">
<el-col :span="12">
<el-form-item label="项目名称" prop="projectName">
<el-input v-model="form.projectName" placeholder="请输入项目名称" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="项目编号" prop="projectNo">
<el-input v-model="form.projectNo" placeholder="请输入项目编号" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="12">
<el-col :span="12">
<el-form-item label="项目形式" prop="projectForm">
<el-select v-model="form.projectForm" placeholder="请选择" style="width:100%">
<el-option label="线" value="线" />
<el-option label="线下" value="线下" />
<el-option label="线上+线下" value="线上+线下" />
<el-option label="其他" value="其他" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="总场次/总期数" prop="totalSessions">
<el-input-number v-model="form.totalSessions" :min="1" style="width:100%" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="12">
<el-col :span="12">
<el-form-item label="总金额(元)" prop="totalAmount">
<el-input-number v-model="form.totalAmount" :min="0" :precision="2" style="width:100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="管理费(元)">
<el-input-number v-model="form.manageFee" :min="0" :precision="2" style="width:100%" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="12">
<el-col :span="12">
<el-form-item label="开始时间" prop="startTime">
<el-date-picker v-model="form.startTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" style="width:100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="结束时间" prop="endTime">
<el-date-picker v-model="form.endTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" style="width:100%" />
</el-form-item>
</el-col>
</el-row>
<!-- 支持单位 -->
<el-row :gutter="12">
<el-col :span="12">
<el-form-item label="提交截止(天)">
<el-input-number v-model="form.submitDeadlineDays" :min="0" controls-position="right" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="支持单位">
<el-select v-model="form.orgId" placeholder="请选择赞助公司" filterable clearable style="width:100%" @change="onSupporterPick">
<el-option v-for="u in supporterOptions" :key="u.userId"
:label="`${u.userName}${u.nickName ? ' (' + u.nickName + ')' : ''}`"
:value="u.userId" />
</el-select>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
<el-row :gutter="12">
<el-col :span="12">
<el-form-item label="项目名称" prop="projectName">
<el-input v-model="form.projectName" placeholder="请输入项目名称" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="项目编号" prop="projectNo">
<el-input v-model="form.projectNo" placeholder="请输入项目编号" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="12">
<el-col :span="12">
<el-form-item label="项目形式" prop="projectForm">
<el-select v-model="form.projectForm" placeholder="请选择" style="width:100%">
<el-option label="线上" value="线上" />
<el-option label="线" value="线" />
<el-option label="线上+线下" value="线上+线下" />
<el-option label="其他" value="其他" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="总场次/总期数" prop="totalSessions">
<el-input-number v-model="form.totalSessions" :min="1" style="width:100%" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="12">
<el-col :span="12">
<el-form-item label="总金额(元)" prop="totalAmount">
<el-input-number v-model="form.totalAmount" :min="0" :precision="2" style="width:100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="管理费(元)">
<el-input-number v-model="form.manageFee" :min="0" :precision="2" style="width:100%" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="12">
<el-col :span="12">
<el-form-item label="开始时间" prop="startTime">
<el-date-picker v-model="form.startTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" style="width:100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="结束时间" prop="endTime">
<el-date-picker v-model="form.endTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" style="width:100%" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="12">
<el-col :span="12">
<el-form-item label="提交截止(天)">
<el-input-number v-model="form.submitDeadlineDays" :min="0" controls-position="right" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="支持单位">
<el-select v-model="form.orgId" placeholder="请选择赞助公司" filterable clearable style="width:100%" @change="onSupporterPick">
<el-option v-for="u in supporterOptions" :key="u.userId"
:label="`${u.userName}${u.nickName ? ' (' + u.nickName + ')' : ''}`"
:value="u.userId" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<!-- 第二区块角色劳务设置 -->
<el-card shadow="never" class="new-card">
<!-- ========== 第二区块角色劳务设置 ========== -->
<div class="new-card-title">角色劳务设置</div>
<div class="hint-text">
<p>*角色劳务设置针对该项目所有会议生效</p>
@@ -100,17 +95,14 @@
</el-select>
<el-input v-if="r.role === '其他'" v-model="r.customName" placeholder="角色名" class="role-custom" />
<el-input-number v-model="r.amount" :min="0" :precision="2" placeholder="劳务金额" class="role-amount" />
<!-- 行尾按钮: 每行末尾 [增加角色] + [删除本行] (不靠右, 紧贴字段) -->
<div class="row-actions">
<el-button link type="primary" @click="addRoleRow">增加角色</el-button>
<el-button link type="danger" :disabled="form.roleRows.length<=1" @click="removeRoleRowAt(idx)">删除角色</el-button>
</div>
</div>
<p class="hint-text">*当选择其他时,需手动填写角色名称,显示填写框</p>
</el-card>
<!-- 第三区块公告文件 -->
<el-card shadow="never" class="new-card">
<!-- ========== 第三区块公告文件 ========== -->
<div class="new-card-title">公告文件</div>
<p class="hint-text">*勾选公告类型后上传对应文件发布公告时可同时发布多个类型</p>
<div class="notice-list">
@@ -143,9 +135,9 @@
</div>
</div>
<p class="hint-text">*名称规则:公告文件类型+取项目编号的后两段+项目名称</p>
</el-card>
</el-form>
<!-- 底部按钮 (与 sponsor 工具栏风格一致) -->
<!-- 底部按钮 -->
<div class="form-actions">
<el-button @click="confirmCancel">取消</el-button>
<el-button type="warning" :loading="submitting" @click="submit('publish')">保存并发布公告</el-button>
@@ -183,7 +175,7 @@ const form = reactive({
projectName: '', projectNo: '', projectForm: '',
totalSessions: 0, totalAmount: 0, manageFee: 0,
startTime: '', endTime: '',
// 赞助公司 (TODO: 下拉源应改为 biz_org,orgType='sponsor', 当前是 sys_user)
// 赞助公司 (TODO 后续: 下拉源应改为 biz_org,orgType='sponsor' 列表, 当前是 sys_user)
orgId: null,
orgName: '',
// 提交截止(天)
@@ -286,7 +278,7 @@ async function loadSupporters(query = '') {
const params = { roleType: 'sponsor', pageNum: 1, pageSize: 50 }
if (query) params.userName = query
const r = await listSupporters(params)
supporterOptions.value = r.rows || r.data || []
supporterOptions.value = (r.data && r.data.rows) || r.rows || []
} catch (e) { supporterOptions.value = [] }
}
@@ -454,18 +446,25 @@ async function submit(mode = 'save') {
</script>
<style scoped>
/* 1:1 抄 sponsor/SponsorProjects.vue */
/* 整页面板 (与 People.vue 风格一致, 1 层) */
.page-card { background: #fff; padding: 16px 20px; border-radius: 6px; border: 1px solid #f0f0f0; }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.breadcrumb .current { color: #262626; font-weight: 500; }
/* 卡片分块 (与 sponsor 一致: 左边竖线标识 + 标题) */
.new-card { margin-bottom: 14px; border: 1px solid #f0f0f0; border-radius: 4px; }
.new-card :deep(.el-card__body) { padding: 16px 20px; }
/* 表单 (整页一个 form) */
.project-form { max-width: 100%; }
/* 章节标题 (替换原 .new-card 容器, 用分隔线分割 3 块) */
.new-card-title {
font-size: 14px; font-weight: 600; color: #262626; margin-bottom: 12px;
padding-left: 8px; border-left: 3px solid var(--brand-primary, #1890ff); line-height: 1;
font-size: 14px;
font-weight: 600;
color: #262626;
margin: 16px 0 12px;
padding-left: 8px;
border-left: 3px solid var(--brand-primary);
line-height: 1;
}
.new-card-title:first-child { margin-top: 0; }
.hint-text { font-size: 12px; color: #909399; margin: 8px 0; line-height: 1.6; }
.hint-text p { margin-bottom: 4px; }
@@ -499,6 +498,6 @@ async function submit(mode = 'save') {
.notice-actions .el-button { padding: 2px 6px; font-size: 12px; }
.notice-disabled { flex: 1; color: #c0c4cc; font-size: 13px; padding: 8px 0; }
/* 底部按钮 (居中, 与 sponsor 风格一致) */
/* 底部按钮 */
.form-actions { display: flex; justify-content: center; gap: 16px; padding: 20px 0 4px; border-top: 1px solid #f0f0f0; margin-top: 8px; }
</style>
+120
View File
@@ -0,0 +1,120 @@
<template>
<!--
角色: 🟨 合规 支持单位查看 (manager/sponsor-orgs)
说明: manager/Orgs.vue 拆出 sponsor 一支, 仅查看
接口: bizList('org', q)
-->
<div class="page-card manager-sponsor-orgs">
<div class="breadcrumb">首页 / 支持单位管理</div>
<el-form inline :model="q" class="filter-form">
<el-form-item label="公司名称">
<el-input v-model="q.orgName" placeholder="输入公司名称" clearable />
</el-form-item>
<el-form-item label="税号/统一社会信用代码">
<el-input v-model="q.taxNo" placeholder="输入税号" clearable />
</el-form-item>
<el-form-item label="合作状态">
<el-select v-model="q.status" placeholder="请选择" clearable>
<el-option label="正常/合作中" value="0" />
<el-option label="合作中" value="合作中" />
<el-option label="禁用" value="禁用" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查找</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
<el-table :data="rows" v-loading="loading" stripe border>
<el-table-column type="index" label="#" width="50" />
<el-table-column prop="orgName" label="公司名称" min-width="200" show-overflow-tooltip />
<el-table-column prop="address" label="公司地址" min-width="220" show-overflow-tooltip />
<el-table-column prop="taxNo" label="税号/统一社会信用代码" min-width="180" />
<el-table-column label="合作状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="statusTagType(row.status)" disable-transitions>{{ row.status }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建日期" width="120" align="center">
<template #default="{ row }">{{ row.createTime ? String(row.createTime).slice(0,10) : '' }}</template>
</el-table-column>
<el-table-column prop="contactName" label="联系人" width="100" align="center" />
<el-table-column prop="contactPhone" label="联系电话" width="130" align="center" />
<el-table-column label="操作" width="180" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="view(row)">查看</el-button>
<el-button link type="primary" @click="goPeople(row)">人员管理</el-button>
</template>
</el-table-column>
</el-table>
<div class="pager">
<el-pagination v-model:current-page="page.pageNum" v-model:page-size="page.pageSize" :total="page.total" :page-sizes="[10,20,50]" layout="total, sizes, prev, pager, next, jumper" @current-change="load" @size-change="load" />
</div>
<el-dialog v-model="detailOpen" title="支持单位详情" width="640px" v-if="currentRow">
<el-descriptions :column="1" border>
<el-descriptions-item label="公司类型">
<el-tag type="success">赞助方</el-tag>
</el-descriptions-item>
<el-descriptions-item label="公司名称">{{ currentRow.orgName }}</el-descriptions-item>
<el-descriptions-item label="公司地址">{{ currentRow.address }}</el-descriptions-item>
<el-descriptions-item label="税号/统一社会信用代码">{{ currentRow.taxNo }}</el-descriptions-item>
<el-descriptions-item label="合作状态">
<el-tag :type="statusTagType(currentRow.status)">{{ currentRow.status }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="创建日期">{{ currentRow.createTime ? String(currentRow.createTime).slice(0,10) : '' }}</el-descriptions-item>
<el-descriptions-item label="联系人">{{ currentRow.contactName || '-' }}</el-descriptions-item>
<el-descriptions-item label="联系电话">{{ currentRow.contactPhone || '-' }}</el-descriptions-item>
</el-descriptions>
<template #footer>
<el-button @click="detailOpen=false">关闭</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { bizList } from '@/api/public'
const router = useRouter()
const q = ref({ orgType: 'sponsor', orgName: '', taxNo: '', status: '' })
const rows = ref([])
const loading = ref(false)
const page = reactive({ pageNum: 1, pageSize: 10, total: 0 })
const currentRow = ref(null)
const detailOpen = ref(false)
function statusTagType(s) {
if (s === '正常' || s === '0' || s === '合作中') return 'success'
if (s === '禁用' || s === '1') return 'danger'
return 'info'
}
async function load() {
loading.value = true
try {
const { data } = await bizList('org', { ...q.value, pageNum: page.pageNum, pageSize: page.pageSize })
rows.value = data?.rows || []; page.total = data?.total || 0
} catch { rows.value = []; page.total = 0 }
finally { loading.value = false }
}
function reset() { q.value = { orgType: 'sponsor', orgName: '', taxNo: '', status: '' }; page.pageNum = 1; load() }
function view(row) { currentRow.value = row; detailOpen.value = true }
function goPeople(row) {
router.push({ path: '/manager/sponsor-people', query: { orgId: row.orgId, orgName: row.orgName } })
}
onMounted(load)
</script>
<style scoped>
.manager-sponsor-orgs { padding: 16px; }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.filter-form { margin-bottom: 12px; }
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
</style>
+225
View File
@@ -0,0 +1,225 @@
<template>
<!--
角色: 🟨 合规 支持单位下人员 (manager/sponsor-people)
入口: org 表格行内人员管理按钮 (router.push orgId/orgName)
接口: bizList('person', q) / bizUpdate / bizAdd / sponsorImport / sponsorImportTemplate
-->
<div class="page-card manager-sponsor-people">
<div class="breadcrumb">
首页 / 支持单位管理 / 人员管理{{ orgName ? ' (' + orgName + ')' : '' }}
</div>
<el-form inline :model="q" class="filter-form">
<el-form-item label="姓名">
<el-input v-model="q.name" placeholder="姓名" clearable style="width:140px" />
</el-form-item>
<el-form-item label="手机号">
<el-input v-model="q.phone" placeholder="手机号" clearable style="width:140px" />
</el-form-item>
<el-form-item label="企业名称">
<el-input v-model="q.orgName" placeholder="企业名称" clearable style="width:180px" />
</el-form-item>
<el-form-item label="部门">
<el-input v-model="q.department" placeholder="部门" clearable style="width:140px" />
</el-form-item>
<el-form-item label="职务">
<el-input v-model="q.position" placeholder="职务" clearable style="width:140px" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查找</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
<div class="batch-bar">
<el-button type="primary" @click="goNew">新建人员</el-button>
<el-button @click="openImport">批量导入</el-button>
<span class="filter-tip">*批量导入模板与列表表项一致</span>
</div>
<el-table :data="rows" v-loading="loading" stripe border>
<el-table-column prop="name" label="姓名" min-width="100" />
<el-table-column prop="phone" label="手机号" min-width="130" />
<el-table-column prop="orgName" label="企业名称" min-width="180" show-overflow-tooltip />
<el-table-column prop="department" label="部门" min-width="100" />
<el-table-column prop="position" label="职务" min-width="100" />
<el-table-column label="状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="statusTagType(row.status)" disable-transitions>{{ row.status || '正常' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="120" fixed="right">
<template #default="{ row }">
<el-button link :type="isDisabled(row) ? 'success' : 'danger'" @click="onToggleStatus(row)">
{{ isDisabled(row) ? '启用' : '禁用' }}
</el-button>
</template>
</el-table-column>
</el-table>
<div class="pager">
<el-pagination
v-model:current-page="page.pageNum"
v-model:page-size="page.pageSize"
:total="page.total"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next, jumper"
@current-change="load"
@size-change="load"
/>
</div>
<el-dialog v-model="importOpen" title="批量导入" width="520px" :close-on-click-modal="false">
<el-form label-width="100px">
<el-form-item label="选择文件">
<el-upload ref="uploadRef" :auto-upload="false" :limit="1" accept=".xls,.xlsx" :on-change="onFileChange" :on-remove="onFileRemove">
<el-button>选择文件</el-button>
</el-upload>
</el-form-item>
<el-form-item label="">
<el-button link type="primary" @click="downloadTemplate">批量导入模板下载</el-button>
</el-form-item>
<el-form-item><span class="filter-tip">*导入列: 姓名/手机号/所属公司/部门/职务/角色</span></el-form-item>
</el-form>
<template #footer>
<el-button @click="importOpen = false">取消</el-button>
<el-button type="primary" :loading="importing" @click="submitImport">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { bizList, bizUpdate, importPerson, downloadImportTemplate } from '@/api/public'
import { ElMessage, ElMessageBox } from 'element-plus'
const route = useRoute()
const router = useRouter()
const orgId = computed(() => route.query.orgId ? Number(route.query.orgId) : null)
const orgName = computed(() => route.query.orgName || '')
const q = reactive({ name: '', phone: '', orgName: '', department: '', position: '' })
const rows = ref([])
const loading = ref(false)
const page = reactive({ pageNum: 1, pageSize: 10, total: 0 })
function statusTagType(s) {
if (s === '禁用' || s === '1') return 'danger'
return 'success'
}
function isDisabled(row) {
return row.status === '禁用' || row.status === '1'
}
async function load() {
loading.value = true
try {
const params = {
...q,
orgType: 'sponsor',
...(orgId.value ? { orgId: orgId.value } : {}),
pageNum: page.pageNum,
pageSize: page.pageSize,
}
const { data } = await bizList('person', params)
rows.value = (data && data.rows) || []
page.total = (data && data.total) || 0
} catch { rows.value = []; page.total = 0 }
finally { loading.value = false }
}
function reset() {
Object.assign(q, { name: '', phone: '', orgName: '', department: '', position: '' })
page.pageNum = 1
load()
}
async function onToggleStatus(row) {
const disabled = isDisabled(row)
const newStatus = disabled ? '正常' : '禁用'
const label = disabled ? '启用' : '禁用'
try {
await ElMessageBox.confirm(
`确定要${label}${row.name}」吗?`,
'确认',
{ type: 'warning' }
)
} catch { return }
try {
await bizUpdate('person', { personId: row.personId, status: newStatus })
ElMessage.success('已' + label)
load()
} catch (e) { ElMessage.error(e?.msg || '操作失败') }
}
// ========== 新建人员 ==========
function goNew() {
router.push({
path: '/manager/sponsor-people/new',
query: {
...(orgId.value ? { orgId: orgId.value } : {}),
...(orgName.value ? { orgName: orgName.value } : {})
}
})
}
// ========== 批量导入 ==========
const importOpen = ref(false)
const importing = ref(false)
const uploadRef = ref()
const importFile = ref(null)
function openImport() { importFile.value = null; importOpen.value = true }
function onFileChange(file) { importFile.value = file.raw }
function onFileRemove() { importFile.value = null }
async function downloadTemplate() {
try {
const res = await downloadImportTemplate('sponsor')
const blob = new Blob([res.data], { type: res.data.type || 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = '支持单位人员导入模板.xlsx'
a.click()
URL.revokeObjectURL(url)
} catch { ElMessage.error('模板下载失败') }
}
async function submitImport() {
if (!importFile.value) { ElMessage.warning('请先选择文件'); return }
importing.value = true
try {
const res = await importPerson('sponsor', importFile.value)
const payload = res.data || res
const ok = payload.ok ?? 0
const total = payload.total ?? 0
const failed = (payload.results || []).filter(r => !r.ok)
if (failed.length === 0) {
ElMessage.success(`导入成功 ${ok}/${total}`)
} else {
ElMessageBox.alert(
`成功 ${ok} 条, 失败 ${failed.length} 条:\n` +
failed.map(f => `${f.rowNo}${f.name || ''}: ${f.message}`).join('\n'),
'导入结果', { type: 'warning' }
)
}
importOpen.value = false
load()
} catch (e) {
ElMessage.error(e?.msg || '导入失败')
} finally {
importing.value = false
}
}
onMounted(load)
</script>
<style scoped>
.manager-sponsor-people { padding: 16px; }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.filter-form { margin-bottom: 12px; }
.batch-bar { display: flex; gap: 8px; margin-bottom: 12px; align-items: center; }
.filter-tip { color: #909399; font-size: 12px; }
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
</style>
+54 -49
View File
@@ -1,41 +1,43 @@
<template>
<div class="manager-support-intent">
<div class="page-card">
<div class="page-title">支持意向</div>
<!-- 筛选区严格按原型 top=169-217 left 顺序: 意向项目名称(276) / ZH-2026-658(371) / 姓名(541) / 工作单位(580) / 工作单位(750) / 工作单位(817) / 项目编号(996) / 提示(1113) -->
<el-form inline :model="q">
<el-form-item label="项目编号:"><el-input v-model="q.projectNo" placeholder="如 ZH-2026-658" clearable /></el-form-item>
<el-form-item label="意向项目名称:"><el-input v-model="q.projectName" placeholder="输入意向项目名称" clearable /></el-form-item>
<el-form-item label="姓名:"><el-input v-model="q.name" placeholder="输入姓名" clearable /></el-form-item>
<el-form-item label="工作单位:"><el-input v-model="q.workUnit" placeholder="输入工作单位" clearable /></el-form-item>
<el-form-item>
<el-button class="action-btn" @click="load">查找</el-button>
<el-button class="action-btn secondary" @click="reset">重置</el-button>
</el-form-item>
</el-form>
<!-- 提示语严格按原型 top=217-243 -->
<div class="filter-tip">*项目公示中点击支持意向后显示在此</div>
<div class="filter-tip">*支持意向不需要注册但是如果已经注册账号则显示账号状态</div>
<div class="filter-tip">*根据手机号与系统账号进行匹配</div>
<div class="page-card manager-support-intent">
<div class="breadcrumb">首页 / 支持意向</div>
<!-- 提示语 (按原型 top=217-243) -->
<div class="hint-list">
<p>*项目公示中点击支持意向后显示在此</p>
<p>*支持意向不需要注册但是如果已经注册账号则显示账号状态</p>
<p>*根据手机号与系统账号进行匹配</p>
</div>
<div class="page-card">
<!-- 列头严格按原型 top=296 left 顺序: 支持意向项目编号 272 / 支持意向项目名称 437 / 姓名 610 / 工作单位名称 685 / 部门 815 / 职务 925 / 手机号 1054 / 账号状态 1164 -->
<el-table :data="rows" v-loading="loading" stripe border>
<el-table-column type="index" label="#" width="50" />
<el-table-column prop="projectNo" label="支持意向项目编号" width="140" align="center" />
<el-table-column prop="projectName" label="支持意向项目名称" min-width="200" show-overflow-tooltip />
<el-table-column prop="name" label="姓名" width="80" align="center" />
<el-table-column prop="workUnit" label="工作单位名称" min-width="180" show-overflow-tooltip />
<el-table-column prop="department" label="部门" width="120" align="center" />
<el-table-column prop="position" label="职务" width="120" align="center" />
<el-table-column prop="phone" label="手机号" width="130" align="center" />
<el-table-column label="账号状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.accountStatus==='存在' ? 'success' : 'warning'">{{ row.accountStatus || '不存在' }}</el-tag>
</template>
</el-table-column>
</el-table>
<!-- ========== 筛选区 ( People.vue 风格一致) ========== -->
<el-form inline :model="q" class="filter-form">
<el-form-item label="项目编号"><el-input v-model="q.projectNo" placeholder="如 ZH-2026-658" clearable /></el-form-item>
<el-form-item label="意向项目名称"><el-input v-model="q.projectName" placeholder="输入意向项目名称" clearable /></el-form-item>
<el-form-item label="姓名"><el-input v-model="q.name" placeholder="输入姓名" clearable /></el-form-item>
<el-form-item label="工作单位"><el-input v-model="q.workUnit" placeholder="输入工作单位" clearable /></el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查找</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
<!-- ========== 表格 ( People.vue 风格一致, page-card 包裹) ========== -->
<el-table :data="rows" v-loading="loading" stripe border>
<el-table-column type="index" label="#" width="50" />
<el-table-column prop="projectNo" label="支持意向项目编号" width="140" align="center" />
<el-table-column prop="projectName" label="支持意向项目名称" min-width="200" show-overflow-tooltip />
<el-table-column prop="name" label="姓名" width="80" align="center" />
<el-table-column prop="workUnit" label="工作单位名称" min-width="180" show-overflow-tooltip />
<el-table-column prop="department" label="部门" width="120" align="center" />
<el-table-column prop="position" label="职务" width="120" align="center" />
<el-table-column prop="phone" label="手机号" width="130" align="center" />
<el-table-column label="账号状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.accountStatus==='存在' ? 'success' : 'warning'">{{ row.accountStatus || '不存在' }}</el-tag>
</template>
</el-table-column>
</el-table>
<div class="pager">
<el-pagination v-model:current-page="page.pageNum" v-model:page-size="page.pageSize" :total="page.total" :page-sizes="[10,20,50]" layout="total, sizes, prev, pager, next, jumper" @current-change="load" @size-change="load" />
</div>
</div>
@@ -64,20 +66,23 @@ onMounted(load)
</script>
<style scoped>
/* 整页面板 (与 People.vue 风格一致) */
.manager-support-intent { padding: 16px; }
.page-card { background: #fff; border-radius: 6px; padding: 16px; border: 1px solid #ebeef5; margin-bottom: 12px; }
.page-title { font-size: 20px; font-weight: 600; margin-bottom: 4px; padding-left: 10px; border-left: 3px solid var(--brand-primary); }
.filter-tip { color: #909399; font-size: 12px; line-height: 1.8; }
.page-toolbar { display: flex; gap: 12px; margin-bottom: 12px; flex-wrap: wrap; align-items: center; }
.page-toolbar :deep(.action-btn) {
height: 36px; padding: 0 20px; font-size: 14px; border-radius: 4px;
background: var(--brand-primary); color: #fff; border: 1px solid var(--brand-primary);
display: inline-flex; align-items: center; justify-content: center; white-space: nowrap;
transition: opacity .15s; margin-left: 0;
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.filter-form { margin-bottom: 12px; }
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
/* 红字提示 (与 People.vue 一致) */
.hint-list {
background: #fff;
border: 1px solid #f0f0f0;
border-radius: 8px;
padding: 12px 20px;
margin-bottom: 16px;
font-size: 12px;
color: #ff4d4f;
line-height: 1.8;
}
.page-toolbar :deep(.action-btn:hover) { opacity: .85; }
.page-toolbar :deep(.action-btn.secondary) {
background: #fff; color: var(--brand-primary); border: 1px solid var(--brand-primary);
}
.page-toolbar :deep(.action-btn.secondary:hover) { background: rgba(30,58,138,.06); }
.hint-list p { margin-bottom: 4px; }
.hint-list p:last-child { margin-bottom: 0; }
</style>
+50 -14
View File
@@ -1,10 +1,8 @@
<template>
<div class="manager-workbench">
<div class="page-card manager-workbench">
<div class="breadcrumb">首页 / 工作台</div>
<!-- 我的项目策划方案 标题 ( top=142) -->
<h2 class="page-title">我的项目策划方案</h2>
<!-- 5 KPI ( top=165 已结算/已结题 + top=458 已执行/未执行) -->
<!-- 5 KPI -->
<div class="stats-grid">
<router-link class="stat-card" to="/manager/projects">
<div class="stat-label">项目总数量</div>
@@ -30,7 +28,7 @@
<p class="hint-text">*点击数字跳转到相应的列表页</p>
<!-- 消息通知 (新增) -->
<!-- 消息通知 -->
<section class="section">
<h2 class="section-title">消息通知<a class="more" href="javascript:void(0)">更多 </a></h2>
<ul class="notice-list">
@@ -100,23 +98,61 @@ onMounted(() => { loadStats(); loadNotices() })
</script>
<style scoped>
/* 整页面板 (与 People.vue 风格一致) */
.manager-workbench { padding: 16px; }
.page-title { font-size: 20px; font-weight: 600; color: #1a1a1a; margin-bottom: 16px; padding-left: 10px; border-left: 3px solid var(--brand-primary); }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
/* 5 个 KPI 卡 (跟 executor/Overview 一致) */
.stats-grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 16px; margin-bottom: 12px; }
.stat-card {
background: #fff; border-radius: 4px; padding: 24px;
border: 1px solid #ebeef5; cursor: pointer; text-decoration: none; color: inherit;
display: flex; flex-direction: column; align-items: center; justify-content: center;
background: #fff;
border: 1px solid #f0f0f0;
border-radius: 8px;
padding: 24px;
cursor: pointer;
text-decoration: none;
color: inherit;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
transition: opacity .15s, border-color .15s;
min-height: 120px;
}
.stat-card:hover { border-color: var(--brand-primary); opacity: .9; }
.stat-label { font-size: 14px; color: #606266; margin-bottom: 16px; }
.stat-value { font-size: 32px; font-weight: 600; color: var(--brand-primary); line-height: 1.2; }
.stat-label { font-size: 14px; color: #8c8c8c; margin-bottom: 16px; }
.stat-value {
font-size: 32px;
font-weight: 600;
line-height: 1.2;
background: linear-gradient(135deg, var(--brand-primary) 0%, var(--brand-primary-deep) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.hint-text { font-size: 12px; color: #ff4d4f; margin: 12px 0 20px; text-align: center; }
.section { background: #fff; border-radius: 4px; padding: 20px 24px; border: 1px solid #ebeef5; }
.section-title { font-size: 15px; font-weight: 600; color: #1a1a1a; margin-bottom: 16px; padding-left: 10px; border-left: 3px solid var(--brand-primary); display: flex; justify-content: space-between; }
/* 消息通知 */
.section {
background: #fff;
border-radius: 8px;
padding: 20px 24px;
border: 1px solid #f0f0f0;
}
.section-title {
font-size: 15px;
font-weight: 600;
color: #1a1a1a;
margin-bottom: 16px;
padding-left: 10px;
border-left: 3px solid var(--brand-primary);
line-height: 1;
display: flex;
justify-content: space-between;
}
.more { font-size: 12px; color: var(--brand-primary); text-decoration: none; }
.more:hover { text-decoration: underline; }
.notice-list { list-style: none; border-top: 1px solid #f0f0f0; }
.notice-item { padding: 12px 0; border-bottom: 1px solid #f0f0f0; display: flex; align-items: center; gap: 12px; }
.notice-item:last-child { border-bottom: none; }
+1 -1
View File
@@ -59,7 +59,7 @@
<el-input
v-if="myPersonId"
v-model="form.orgName"
placeholder="请填写所属公司 (TODO: 改为 biz_org 下拉)"
placeholder="请填写所属公司"
/>
<el-input
v-else
+26 -7
View File
@@ -5,7 +5,8 @@
筛选区 (top 165-205, left): 项目策划方案管理 / 项目总数量 / 已结算的会议 / 已结题的项目
列头 (top 285-335, left): 工作台 / 会议管理
-->
<div class="sponsor-home">
<div class="page-card sponsor-home">
<div class="breadcrumb">首页</div>
<!-- KPI: 5 个数字面板, 1 行展示 -->
<div class="stats-grid stats-grid-5">
@@ -100,7 +101,9 @@ onMounted(() => {
</script>
<style scoped>
/* 整页面板 (与 People.vue 风格一致) */
.sponsor-home { padding: 16px; }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.stats-grid { display: grid; gap: 16px; margin-bottom: 16px; }
.stats-grid-5 { grid-template-columns: repeat(5, 1fr); }
@@ -109,28 +112,44 @@ onMounted(() => {
.stat-card {
background: #fff;
border-radius: 4px;
padding: 24px;
border: 1px solid #f0f0f0;
border-radius: 8px;
padding: 24px;
cursor: pointer;
text-decoration: none;
color: inherit;
display: block;
transition: border-color 0.2s, box-shadow 0.2s;
}
.stat-card:hover { border-color: var(--brand-primary); box-shadow: 0 2px 8px rgba(30,58,138,0.15); }
.stat-label { font-size: 14px; color: #8c8c8c; margin-bottom: 16px; }
.stat-value { font-size: 32px; font-weight: 700; color: var(--brand-primary); line-height: 1; margin-bottom: 8px; }
.stat-value {
font-size: 32px;
font-weight: 700;
line-height: 1;
margin-bottom: 8px;
background: linear-gradient(135deg, var(--brand-primary) 0%, var(--brand-primary-deep) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.stat-desc { font-size: 12px; color: #bfbfbf; }
.hint-text { font-size: 12px; color: #ff4d4f; margin: 12px 0; text-align: center; }
.section { background: #fff; border-radius: 4px; padding: 20px 24px; border: 1px solid #f0f0f0; }
.section {
background: #fff;
border-radius: 8px;
padding: 20px 24px;
border: 1px solid #f0f0f0;
}
.section-title {
font-size: 15px; font-weight: 600; color: #1a1a1a;
margin-bottom: 16px; padding-left: 10px;
border-left: 3px solid var(--brand-primary);
line-height: 1;
display: flex; justify-content: space-between;
}
.more { font-size: 12px; color: var(--brand-primary); text-decoration: none; }
.more:hover { text-decoration: underline; }
.notice-list { list-style: none; border-top: 1px solid #f0f0f0; }
.notice-item {
padding: 12px 0; border-bottom: 1px solid #f0f0f0;
+2 -2
View File
@@ -196,8 +196,8 @@ async function loadList() {
}
delete params.startTimeRange
const r = await bizList('meeting', params)
list.value = r.rows || []
page.total = r.total || 0
list.value = (r.data && r.data.rows) || r.rows || []
page.total = (r.data && r.data.total) || r.total || 0
} catch (e) { list.value = []; page.total = 0 }
finally { loading.value = false }
}
+4
View File
@@ -14,6 +14,10 @@ export default defineConfig({
resolve: {
alias: { '@': path.resolve(__dirname, 'src') }
},
build: {
// 老浏览器兼容: 把 ?. / ?? / 箭头函数等 ES2020+ 语法降级到 es2015
target: 'es2015'
},
server: {
port: 5173,
host: '0.0.0.0',