|
|
@@ -0,0 +1,448 @@
|
|
|
+<template>
|
|
|
+ <ContentWrap>
|
|
|
+ <el-form ref="queryFormRef" :model="queryParams" label-width="92px" class="query-form">
|
|
|
+ <el-row :gutter="16">
|
|
|
+ <el-col :xs="24" :sm="12" :md="12" :lg="5" :xl="6">
|
|
|
+ <TenantSelect v-model="queryParams.tenantIds" placeholder="请选择院区" prop="tenantIds" />
|
|
|
+ </el-col>
|
|
|
+ <el-col :xs="24" :sm="12" :md="12" :lg="6" :xl="6">
|
|
|
+ <el-form-item label="填写身份" prop="role">
|
|
|
+ <el-select v-model="queryParams.role" clearable placeholder="请选择身份" class="!w-full">
|
|
|
+ <el-option
|
|
|
+ v-for="item in roleOptions"
|
|
|
+ :key="item.value"
|
|
|
+ :label="item.label"
|
|
|
+ :value="item.value"
|
|
|
+ />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :xs="24" :sm="12" :md="12" :lg="6" :xl="6">
|
|
|
+ <el-form-item label="填写日期" prop="recordDate">
|
|
|
+ <el-date-picker
|
|
|
+ v-model="queryParams.recordDate"
|
|
|
+ type="daterange"
|
|
|
+ value-format="YYYY-MM-DD"
|
|
|
+ range-separator="至"
|
|
|
+ start-placeholder="开始日期"
|
|
|
+ end-placeholder="结束日期"
|
|
|
+ class="!w-full"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :xs="24" :sm="12" :md="12" :lg="6" :xl="6">
|
|
|
+ <el-form-item class="query-actions" label-width="0">
|
|
|
+ <el-button @click="handleQuery">
|
|
|
+ <Icon icon="ep:search" class="mr-5px" />
|
|
|
+ 搜索
|
|
|
+ </el-button>
|
|
|
+ <el-button @click="resetQuery">
|
|
|
+ <Icon icon="ep:refresh" class="mr-5px" />
|
|
|
+ 重置
|
|
|
+ </el-button>
|
|
|
+ <el-button v-hasPermi="['satisfactionSurvey:export']" type="warning" plain :loading="exportLoading" @click="handleExport">
|
|
|
+ <Icon icon="ep:download" class="mr-5px" />
|
|
|
+ 导出
|
|
|
+ </el-button>
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ </el-form>
|
|
|
+ </ContentWrap>
|
|
|
+
|
|
|
+ <ContentWrap>
|
|
|
+ <div class="mb-12px flex items-center justify-between">
|
|
|
+ <div class="text-[14px] text-[#606266]">
|
|
|
+ 共筛选出 <span class="text-[#409eff]">{{ total }}</span> 条满意度问卷
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <el-table v-loading="loading" :data="pageList" border>
|
|
|
+ <!-- <el-table-column label="序号" type="index" width="60" align="center" /> -->
|
|
|
+ <el-table-column label="总得分" min-width="90" align="center">
|
|
|
+ <template #default="{ row }">
|
|
|
+ {{ getGrandTotal(row) }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="填写日期" prop="recordDate" min-width="110" />
|
|
|
+ <el-table-column label="院区/用户" min-width="200" show-overflow-tooltip>
|
|
|
+ <template #default="{ row }">
|
|
|
+ {{ getTenantUserText(row) }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="填写身份" prop="role" min-width="90">
|
|
|
+ <template #default="{ row }">
|
|
|
+ {{ row.role }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="膳食质量总分" prop="qualityTotal" min-width="120" align="center" />
|
|
|
+ <el-table-column label="膳食服务总分" prop="serviceTotal" min-width="120" align="center" />
|
|
|
+ <el-table-column label="软食/流食满足" min-width="130">
|
|
|
+ <template #default="{ row }">
|
|
|
+ {{ row.specialNeeds?.softFood || '-' }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="素食需求满足" min-width="130">
|
|
|
+ <template #default="{ row }">
|
|
|
+ {{ row.specialNeeds?.vegetarian || '-' }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="过敏食材标注" min-width="140">
|
|
|
+ <template #default="{ row }">
|
|
|
+ {{ row.specialNeeds?.allergyLabel || '-' }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="最需要改进的方面" min-width="220" show-overflow-tooltip>
|
|
|
+ <template #default="{ row }">
|
|
|
+ {{ row.suggestions?.improve || '-' }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="希望新增哪些菜品或服务" min-width="240" show-overflow-tooltip>
|
|
|
+ <template #default="{ row }">
|
|
|
+ {{ row.suggestions?.newDishes || '-' }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="其他想反馈的内容" min-width="200" show-overflow-tooltip>
|
|
|
+ <template #default="{ row }">
|
|
|
+ {{ row.suggestions?.other || '-' }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="操作" width="140" fixed="right" align="center">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-button link type="primary" @click="openDetail(row)">查看详情</el-button>
|
|
|
+ <el-button v-hasPermi="['satisfactionSurvey:delete']" link type="danger" @click="handleDelete(row.id)">删除</el-button>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+
|
|
|
+ <Pagination
|
|
|
+ :total="total"
|
|
|
+ v-model:page="queryParams.pageNo"
|
|
|
+ v-model:limit="queryParams.pageSize"
|
|
|
+ @pagination="getList"
|
|
|
+ />
|
|
|
+ </ContentWrap>
|
|
|
+
|
|
|
+ <el-drawer v-model="detailVisible" title="满意度调查详情" size="760px">
|
|
|
+ <template v-if="currentRecord">
|
|
|
+ <el-descriptions :column="2" border class="mb-16px">
|
|
|
+ <el-descriptions-item label="填写身份">
|
|
|
+ {{ currentRecord.role }}
|
|
|
+ </el-descriptions-item>
|
|
|
+ <el-descriptions-item label="填写日期">
|
|
|
+ {{ currentRecord.recordDate || '-' }}
|
|
|
+ </el-descriptions-item>
|
|
|
+ <el-descriptions-item label="院区名称">
|
|
|
+ {{ currentRecord.tenantName || '-' }}
|
|
|
+ </el-descriptions-item>
|
|
|
+ <el-descriptions-item label="提交用户">
|
|
|
+ {{ currentRecord.submitter || '-' }}
|
|
|
+ </el-descriptions-item>
|
|
|
+ <el-descriptions-item label="膳食质量总分">
|
|
|
+ {{ currentRecord.qualityTotal }} 分
|
|
|
+ </el-descriptions-item>
|
|
|
+ <el-descriptions-item label="膳食服务总分">
|
|
|
+ {{ currentRecord.serviceTotal }} 分
|
|
|
+ </el-descriptions-item>
|
|
|
+ <el-descriptions-item label="综合总分">
|
|
|
+ {{ getGrandTotal(currentRecord) }} 分
|
|
|
+ </el-descriptions-item>
|
|
|
+ <el-descriptions-item label="软食/流食需求">
|
|
|
+ {{ currentRecord.specialNeeds?.softFood || '-' }}
|
|
|
+ </el-descriptions-item>
|
|
|
+ <el-descriptions-item label="素食需求">
|
|
|
+ {{ currentRecord.specialNeeds?.vegetarian || '-' }}
|
|
|
+ </el-descriptions-item>
|
|
|
+ <el-descriptions-item label="过敏食材标注" :span="2">
|
|
|
+ {{ currentRecord.specialNeeds?.allergyLabel || '-' }}
|
|
|
+ </el-descriptions-item>
|
|
|
+ </el-descriptions>
|
|
|
+
|
|
|
+ <div class="detail-section">
|
|
|
+ <div class="detail-title">膳食质量评价</div>
|
|
|
+ <el-table :data="currentRecord.qualityItems" border>
|
|
|
+ <el-table-column label="评价项" prop="label" min-width="260" />
|
|
|
+ <el-table-column label="评分" prop="score" width="100" align="center">
|
|
|
+ <template #default="{ row }">{{ row.score }} 分</template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="detail-section">
|
|
|
+ <div class="detail-title">膳食服务评价</div>
|
|
|
+ <el-table :data="currentRecord.serviceItems" border>
|
|
|
+ <el-table-column label="评价项" prop="label" min-width="260" />
|
|
|
+ <el-table-column label="评分" prop="score" width="100" align="center">
|
|
|
+ <template #default="{ row }">{{ row.score }} 分</template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="detail-section">
|
|
|
+ <div class="detail-title">建议与意见</div>
|
|
|
+ <el-descriptions :column="1" border>
|
|
|
+ <el-descriptions-item label="最需要改进的方面">
|
|
|
+ {{ currentRecord.suggestions.improve || '无' }}
|
|
|
+ </el-descriptions-item>
|
|
|
+ <el-descriptions-item label="希望新增哪些菜品或服务">
|
|
|
+ {{ currentRecord.suggestions.newDishes || '无' }}
|
|
|
+ </el-descriptions-item>
|
|
|
+ <el-descriptions-item label="其他反馈内容">
|
|
|
+ {{ currentRecord.suggestions.other || '无' }}
|
|
|
+ </el-descriptions-item>
|
|
|
+ </el-descriptions>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-drawer>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script lang="ts" setup>
|
|
|
+import { reactive, ref } from 'vue'
|
|
|
+import dayjs from 'dayjs'
|
|
|
+import { useUserStore } from '@/store/modules/user'
|
|
|
+import download from '@/utils/download'
|
|
|
+import {
|
|
|
+ deleteFoodSatisfactionSurvey,
|
|
|
+ exportFoodSatisfactionSurveyExcel,
|
|
|
+ getFoodSatisfactionSurvey,
|
|
|
+ getFoodSatisfactionSurveyPage
|
|
|
+} from '@/api/system/foods'
|
|
|
+
|
|
|
+defineOptions({ name: 'SatisfactionSurvey' })
|
|
|
+
|
|
|
+type RoleType = 'RELATIVE' | 'EMPLOYEE'
|
|
|
+
|
|
|
+interface ScoreItem {
|
|
|
+ key: string
|
|
|
+ label: string
|
|
|
+ score: number
|
|
|
+}
|
|
|
+
|
|
|
+interface SuggestionInfo {
|
|
|
+ improve: string
|
|
|
+ newDishes: string
|
|
|
+ other: string
|
|
|
+}
|
|
|
+
|
|
|
+interface SurveyRecord {
|
|
|
+ id: number
|
|
|
+ tenantId: number
|
|
|
+ tenantName?: string
|
|
|
+ submitter?: string
|
|
|
+ role?: RoleType
|
|
|
+ recordDate?: string
|
|
|
+ qualityItems: ScoreItem[]
|
|
|
+ qualityTotal?: number
|
|
|
+ serviceItems: ScoreItem[]
|
|
|
+ serviceTotal?: number
|
|
|
+ specialNeeds: {
|
|
|
+ softFood?: string
|
|
|
+ vegetarian?: string
|
|
|
+ allergyLabel?: string
|
|
|
+ }
|
|
|
+ suggestions: SuggestionInfo
|
|
|
+ createTime?: string
|
|
|
+}
|
|
|
+
|
|
|
+interface QueryParams {
|
|
|
+ pageNo: number
|
|
|
+ pageSize: number
|
|
|
+ tenantIds: number[]
|
|
|
+ role?: RoleType
|
|
|
+ recordDate: string[]
|
|
|
+}
|
|
|
+
|
|
|
+const roleOptions = [
|
|
|
+ { label: '家属', value: '家属' },
|
|
|
+ { label: '员工', value: '员工' }
|
|
|
+]
|
|
|
+
|
|
|
+const userStore = useUserStore()
|
|
|
+const message = useMessage()
|
|
|
+
|
|
|
+const qualityItemTemplates: Omit<ScoreItem, 'score'>[] = [
|
|
|
+ { key: 'fresh', label: '1. 菜品新鲜度' },
|
|
|
+ { key: 'taste', label: '2. 菜品口味(咸淡、口感等)' },
|
|
|
+ { key: 'variety', label: '3. 菜品种类丰富度' },
|
|
|
+ { key: 'staple', label: '4. 主食多样性(米饭、面食等)' },
|
|
|
+ { key: 'soup', label: '5. 汤品 / 饮品口感与温度' },
|
|
|
+ { key: 'nutrition', label: '6. 菜品营养搭配合理性' }
|
|
|
+]
|
|
|
+
|
|
|
+const serviceItemTemplates: Omit<ScoreItem, 'score'>[] = [
|
|
|
+ { key: 'tableware', label: '1. 餐具清洁程度' },
|
|
|
+ { key: 'environment', label: '2. 用餐环境卫生(桌椅、地面等)' },
|
|
|
+ { key: 'attitude', label: '3. 工作人员服务态度' },
|
|
|
+ { key: 'timely', label: '4. 菜品供应及时性(未出现长时间断供)' },
|
|
|
+ { key: 'portion', label: '5. 菜品分量充足度' }
|
|
|
+]
|
|
|
+const loading = ref(false)
|
|
|
+const exportLoading = ref(false)
|
|
|
+const total = ref(0)
|
|
|
+const pageList = ref<SurveyRecord[]>([])
|
|
|
+const currentRecord = ref<SurveyRecord>()
|
|
|
+const detailVisible = ref(false)
|
|
|
+const queryFormRef = ref()
|
|
|
+
|
|
|
+const createDefaultQueryParams = (): QueryParams => ({
|
|
|
+ pageNo: 1,
|
|
|
+ pageSize: 10,
|
|
|
+ tenantIds: userStore.orgTenantId || [],
|
|
|
+ role: undefined,
|
|
|
+ recordDate: []
|
|
|
+})
|
|
|
+
|
|
|
+const queryParams = reactive<QueryParams>(createDefaultQueryParams())
|
|
|
+
|
|
|
+const getRoleLabel = (role?: string) => {
|
|
|
+ if (!role) return '-'
|
|
|
+ const hit = roleOptions.find((item) => item.value === role)
|
|
|
+ return hit?.label || role
|
|
|
+}
|
|
|
+
|
|
|
+const getGrandTotal = (row: SurveyRecord) =>
|
|
|
+ Number(row.qualityTotal || 0) + Number(row.serviceTotal || 0)
|
|
|
+
|
|
|
+const getTenantNameById = (tenantId?: number) => {
|
|
|
+ const options = userStore.orgTenant?.options || []
|
|
|
+ const found = options.find((item) => Number(item.value ?? item.id) === Number(tenantId))
|
|
|
+ return found?.label ?? found?.name ?? '-'
|
|
|
+}
|
|
|
+
|
|
|
+const getTenantUserText = (record: SurveyRecord) =>
|
|
|
+ `${record.tenantName || getTenantNameById(record.tenantId)} / ${record.submitter || '-'}`
|
|
|
+
|
|
|
+const normalizeRecord = (raw: any): SurveyRecord => {
|
|
|
+ const qualityItems = qualityItemTemplates.map((item) => ({
|
|
|
+ ...item,
|
|
|
+ score: Number(raw?.[item.key] ?? 0)
|
|
|
+ }))
|
|
|
+ const serviceItems = serviceItemTemplates.map((item) => ({
|
|
|
+ ...item,
|
|
|
+ score: Number(raw?.[item.key] ?? 0)
|
|
|
+ }))
|
|
|
+ return {
|
|
|
+ id: Number(raw?.id),
|
|
|
+ tenantId: Number(raw?.tenantId),
|
|
|
+ tenantName: raw?.tenantName || getTenantNameById(raw?.tenantId),
|
|
|
+ submitter: raw?.submitter,
|
|
|
+ role: raw?.role,
|
|
|
+ recordDate: raw?.recordDate,
|
|
|
+ qualityItems,
|
|
|
+ qualityTotal: Number(raw?.qualityTotal ?? qualityItems.reduce((sum, x) => sum + x.score, 0)),
|
|
|
+ serviceItems,
|
|
|
+ serviceTotal: Number(raw?.serviceTotal ?? serviceItems.reduce((sum, x) => sum + x.score, 0)),
|
|
|
+ specialNeeds: {
|
|
|
+ softFood: raw?.softFood,
|
|
|
+ vegetarian: raw?.vegetarian,
|
|
|
+ allergyLabel: raw?.allergyLabel
|
|
|
+ },
|
|
|
+ suggestions: {
|
|
|
+ improve: raw?.improve,
|
|
|
+ newDishes: raw?.newDishes,
|
|
|
+ other: raw?.other
|
|
|
+ },
|
|
|
+ createTime: raw?.createTime
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const buildParams = (isExport = false) => {
|
|
|
+ const params: any = { ...queryParams }
|
|
|
+ params.tenantIds = params.tenantIds && params.tenantIds.length ? params.tenantIds : undefined
|
|
|
+ params.recordDate = params.recordDate && params.recordDate.length ? params.recordDate : undefined
|
|
|
+ if (isExport) {
|
|
|
+ params.pageNo = 1
|
|
|
+ params.pageSize = 99999
|
|
|
+ }
|
|
|
+ return params
|
|
|
+}
|
|
|
+
|
|
|
+const getList = async () => {
|
|
|
+ loading.value = true
|
|
|
+ try {
|
|
|
+ const data = await getFoodSatisfactionSurveyPage(buildParams())
|
|
|
+ pageList.value = (data?.list || []).map((item) => normalizeRecord(item))
|
|
|
+ total.value = data?.total || 0
|
|
|
+ } finally {
|
|
|
+ loading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleExport = async () => {
|
|
|
+ if (exportLoading.value) return
|
|
|
+ exportLoading.value = true
|
|
|
+ const fullscreenLoading = ElLoading.service({
|
|
|
+ lock: true,
|
|
|
+ text: '正在导出,请稍候...',
|
|
|
+ background: 'rgba(0, 0, 0, 0.7)'
|
|
|
+ })
|
|
|
+ try {
|
|
|
+ const data = await exportFoodSatisfactionSurveyExcel(buildParams(true))
|
|
|
+ download.excel(data as unknown as Blob, `膳食满意度调查_${dayjs().format('YYYY-MM-DD_HH-mm-ss')}.xls`)
|
|
|
+ message.success('导出成功')
|
|
|
+ } catch (error) {
|
|
|
+ console.error('导出失败:', error)
|
|
|
+ message.error('导出失败!')
|
|
|
+ } finally {
|
|
|
+ fullscreenLoading.close()
|
|
|
+ exportLoading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleQuery = () => {
|
|
|
+ queryParams.pageNo = 1
|
|
|
+ getList()
|
|
|
+}
|
|
|
+
|
|
|
+const resetQuery = () => {
|
|
|
+ queryFormRef.value?.resetFields()
|
|
|
+ Object.assign(queryParams, createDefaultQueryParams())
|
|
|
+ handleQuery()
|
|
|
+}
|
|
|
+
|
|
|
+const openDetail = async (row: SurveyRecord) => {
|
|
|
+ try {
|
|
|
+ const data = await getFoodSatisfactionSurvey(Number(row.id))
|
|
|
+ currentRecord.value = normalizeRecord(data)
|
|
|
+ detailVisible.value = true
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取详情失败:', error)
|
|
|
+ message.error('获取详情失败!')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleDelete = async (id: number) => {
|
|
|
+ try {
|
|
|
+ await message.delConfirm()
|
|
|
+ await deleteFoodSatisfactionSurvey(Number(id))
|
|
|
+ message.success('删除成功')
|
|
|
+ await getList()
|
|
|
+ } catch {}
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ getList()
|
|
|
+})
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+.query-form :deep(.el-form-item) {
|
|
|
+ margin-bottom: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.query-actions :deep(.el-form-item__content) {
|
|
|
+ justify-content: flex-start;
|
|
|
+}
|
|
|
+
|
|
|
+.detail-section {
|
|
|
+ margin-top: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.detail-title {
|
|
|
+ margin-bottom: 12px;
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #303133;
|
|
|
+}
|
|
|
+
|
|
|
+</style>
|