zs 2 weeks ago
parent
commit
2f6bfde32b

+ 14 - 0
package-lock.json

@@ -17,6 +17,7 @@
         "dayjs": "^1.11.13",
         "element-plus": "^2.5.6",
         "lodash-es": "^4.17.21",
+        "moment": "^2.30.1",
         "nprogress": "^0.2.0",
         "path-browserify": "^1.0.1",
         "path-to-regexp": "^6.2.1",
@@ -4003,6 +4004,14 @@
         "ufo": "^1.3.2"
       }
     },
+    "node_modules/moment": {
+      "version": "2.30.1",
+      "resolved": "https://registry.npmmirror.com/moment/-/moment-2.30.1.tgz",
+      "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
+      "engines": {
+        "node": "*"
+      }
+    },
     "node_modules/mrmime": {
       "version": "2.0.0",
       "resolved": "https://registry.npmmirror.com/mrmime/-/mrmime-2.0.0.tgz",
@@ -9297,6 +9306,11 @@
         "ufo": "^1.3.2"
       }
     },
+    "moment": {
+      "version": "2.30.1",
+      "resolved": "https://registry.npmmirror.com/moment/-/moment-2.30.1.tgz",
+      "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how=="
+    },
     "mrmime": {
       "version": "2.0.0",
       "resolved": "https://registry.npmmirror.com/mrmime/-/mrmime-2.0.0.tgz",

+ 1 - 0
package.json

@@ -20,6 +20,7 @@
     "dayjs": "^1.11.13",
     "element-plus": "^2.5.6",
     "lodash-es": "^4.17.21",
+    "moment": "^2.30.1",
     "nprogress": "^0.2.0",
     "path-browserify": "^1.0.1",
     "path-to-regexp": "^6.2.1",

+ 40 - 0
src/store/api/degree.js

@@ -0,0 +1,40 @@
+import { defineStore } from 'pinia'
+import { AxiosWrapper } from '@/utils/axios-wrapper'
+import { get } from 'lodash-es'
+const url = '/degree'
+const axios = new AxiosWrapper()
+
+export const DegreeStore = defineStore('degree', () => {
+  const query = async ({ skip = 0, limit = undefined, ...info } = {}) => {
+    let cond = {}
+    if (skip) cond.skip = skip
+    if (limit) cond.limit = limit
+    cond = { ...cond, ...info }
+    const res = await axios.$get(`${url}`, cond)
+    return res
+  }
+  const fetch = async (payload) => {
+    const res = await axios.$get(`${url}/${payload}`)
+    return res
+  }
+  const create = async (payload) => {
+    const res = await axios.$post(`${url}`, payload)
+    return res
+  }
+  const update = async (payload) => {
+    const id = get(payload, 'id', get(payload, 'id'))
+    const res = await axios.$post(`${url}/${id}`, payload)
+    return res
+  }
+  const del = async (payload) => {
+    const res = await axios.$delete(`${url}/${payload}`)
+    return res
+  }
+  return {
+    query,
+    fetch,
+    create,
+    update,
+    del
+  }
+})

+ 40 - 0
src/store/api/result.js

@@ -0,0 +1,40 @@
+import { defineStore } from 'pinia'
+import { AxiosWrapper } from '@/utils/axios-wrapper'
+import { get } from 'lodash-es'
+const url = '/result'
+const axios = new AxiosWrapper()
+
+export const ResultStore = defineStore('result', () => {
+  const query = async ({ skip = 0, limit = undefined, ...info } = {}) => {
+    let cond = {}
+    if (skip) cond.skip = skip
+    if (limit) cond.limit = limit
+    cond = { ...cond, ...info }
+    const res = await axios.$get(`${url}`, cond)
+    return res
+  }
+  const fetch = async (payload) => {
+    const res = await axios.$get(`${url}/${payload}`)
+    return res
+  }
+  const create = async (payload) => {
+    const res = await axios.$post(`${url}`, payload)
+    return res
+  }
+  const update = async (payload) => {
+    const id = get(payload, 'id', get(payload, 'id'))
+    const res = await axios.$post(`${url}/${id}`, payload)
+    return res
+  }
+  const del = async (payload) => {
+    const res = await axios.$delete(`${url}/${payload}`)
+    return res
+  }
+  return {
+    query,
+    fetch,
+    create,
+    update,
+    del
+  }
+})

+ 62 - 2
src/views/result/index.vue

@@ -1,9 +1,69 @@
 <template>
-  <div class="main animate__animated animate__backInRight" v-loading="loading">满意度调查结果</div>
+  <div class="main animate__animated animate__backInRight" v-loading="loading">
+    <custom-table
+      :data="data"
+      :fields="fields"
+      @query="search"
+      :total="total"
+      :opera="opera"
+      @view="toView"
+    >
+    </custom-table>
+    <el-dialog
+      v-model="dialog.show"
+      :title="dialog.title"
+      :destroy-on-close="false"
+      @close="toClose"
+      :top="dialog.top"
+    >
+      <el-row>
+        <el-col :span="24" v-if="dialog.type == '1'">
+          <setInfo></setInfo>
+        </el-col>
+      </el-row>
+    </el-dialog>
+  </div>
 </template>
 
 <script setup>
+const { t } = useI18n()
+import setInfo from './parts/info.vue'
+// 接口
+import { ResultStore } from '@/store/api/result'
+const store = ResultStore()
+const data = ref([])
+const searchForm = ref({})
+const fields = [
+  { label: '客户名称', model: 'name' },
+  { label: '创建时间', model: 'created_time' }
+]
+const opera = [{ label: t('common.view'), method: 'view' }]
+let skip = 0
+let limit = inject('limit')
+const total = ref(20)
 // 加载中
 const loading = ref(false)
+const dialog = ref({ type: '1', show: false, title: '满意度调查表', top: '15vh' })
+const form = ref({ problem: [] })
+// 请求
+onMounted(async () => {
+  loading.value = true
+  await search({ skip, limit })
+  loading.value = false
+})
+const search = async (query = { skip: 0, limit }) => {
+  const info = { skip: query.skip, limit: query.limit, ...searchForm.value }
+  const res = await store.query(info)
+  if (res.errcode == '0') {
+    data.value = res.data.data
+    total.value = res.data.total
+  }
+}
+// 查看
+const toView = (data) => {
+  form.value = data
+  dialog.value = { type: '1', show: true, title: '查看满意度调查表', top: '15vh' }
+}
+provide('form', form)
 </script>
-<style scoped></style>
+<style scoped lang="scss"></style>

+ 111 - 0
src/views/result/parts/info.vue

@@ -0,0 +1,111 @@
+<template>
+  <el-col :span="24" class="problem" v-if="form.problem && form.problem.length > 0">
+    <div class="list" v-for="(item, index) in form.problem" :key="index">
+      <div class="listName">
+        <el-icon v-if="item.is_must == '0'" color="red"><StarFilled /></el-icon>
+        <span style="margin: 0 0 0 10px" v-if="item.is_must == '0'">{{ item.name }}:</span>
+        <span style="margin: 0 0 0 10px" v-else>{{ item.name }}:</span>
+      </div>
+      <div class="type" v-if="item.type == '0'">
+        <div class="remark" v-if="item.remark">{{ item.remark }}</div>
+        <div class="text">{{ item.reply || '暂无' }}</div>
+      </div>
+      <div class="type" v-if="item.type == '1'">
+        <div class="remark" v-if="item.remark">{{ item.remark }}</div>
+        <div class="text">{{ item.reply || '暂无' }}</div>
+      </div>
+      <div class="type" v-if="item.type == '2'">
+        <div class="remark" v-if="item.remark">{{ item.remark }}</div>
+        <div class="text">{{ item.reply || '暂无' }}</div>
+      </div>
+      <div class="type" v-if="item.type == '3'">
+        <div class="remark" v-if="item.remark">{{ item.remark }}</div>
+        <div class="text">{{ item.reply || '暂无' }}</div>
+      </div>
+      <div class="type" v-if="item.type == '4'">
+        <div class="remark" v-if="item.remark">{{ item.remark }}</div>
+        <div class="text">{{ item.reply || '暂无' }}</div>
+      </div>
+      <div class="type" v-if="item.type == '5'">
+        <div class="remark" v-if="item.remark">{{ item.remark }}</div>
+        <el-date-picker
+          v-model="item.reply"
+          clearable
+          placeholder="请选项"
+          type="date"
+          format="YYYY-MM-DD"
+          value-format="YYYY-MM-DD"
+        />
+      </div>
+    </div>
+  </el-col>
+</template>
+
+<script setup>
+const form = inject('form')
+</script>
+<style scoped lang="scss">
+.problem {
+  .list {
+    .listName {
+      display: flex;
+      align-items: center;
+      font-size: 18px;
+      margin: 10px 0;
+    }
+
+    .type {
+      padding: 0 0 10px 15px;
+
+      .list {
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        margin: 10px 0;
+
+        .name {
+          width: 50%;
+          text-align: center;
+        }
+
+        .input {
+          width: 50%;
+          margin: 0 5px 0 0;
+
+          .name {
+            width: 50%;
+            text-align: center;
+            margin: 0 0 10px 0;
+          }
+        }
+
+        .icon {
+          display: none;
+          margin: 5px 0 0 0;
+        }
+      }
+
+      .list:hover {
+        .icon {
+          display: block;
+        }
+      }
+
+      .remark {
+        margin: 10px 0 10px 20px;
+        font-size: 14px;
+        color: red;
+      }
+      .text {
+        font-size: 16px;
+        padding: 10px;
+        color: #606266;
+        background-color: #fff;
+        border: 1px solid #dcdfe6;
+        border-radius: 4px;
+        transition: border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
+      }
+    }
+  }
+}
+</style>

+ 1 - 4
src/views/system/config/index.vue

@@ -20,10 +20,7 @@
             </el-radio-group>
           </el-form-item>
           <el-form-item label="首页宣传图">
-            <custom-upload
-              v-model="config.index_img"
-              url="/warter/admin/api/files/homeImg/upload"
-            ></custom-upload>
+            <custom-upload v-model="config.index_img" url="/warter/admin/api/files/homeImg/upload"></custom-upload>
           </el-form-item>
           <el-row>
             <el-col :span="24" style="text-align: center">

+ 1 - 9
src/views/system/role/parts/form.vue

@@ -16,15 +16,7 @@
       <el-input v-model="form.brief" type="textarea" :rows="3"></el-input>
     </el-form-item>
     <el-form-item label="权限配置">
-      <el-tree
-        ref="roleTree"
-        :data="getMenus()"
-        node-key="code"
-        default-expand-all
-        show-checkbox
-        @check-change="seletNode"
-        :default-checked-keys="['home']"
-      >
+      <el-tree ref="roleTree" :data="getMenus()" node-key="code" default-expand-all show-checkbox @check-change="seletNode" :default-checked-keys="['home']">
         <template #default="{ data }">
           <span>{{ data.name }}</span>
         </template>

+ 129 - 2
src/views/system/setting/index.vue

@@ -1,9 +1,136 @@
 <template>
-  <div class="main animate__animated animate__backInRight" v-loading="loading">满意度调查设置</div>
+  <div class="main animate__animated animate__backInRight" v-loading="loading">
+    <custom-search-bar :fields="fields.filter((f) => f.isSearch)" v-model="searchForm" @search="search" @reset="toReset"></custom-search-bar>
+    <custom-button-bar :fields="buttonFields" @add="toAdd"></custom-button-bar>
+    <custom-table :data="data" :fields="fields" @query="search" :total="total" :opera="opera" @edit="toEdit" @delete="toDelete">
+      <template #is_use="{ row }">
+        <el-tag v-if="row.is_use == '0'" type="success">启用</el-tag>
+        <el-tag v-else type="info">禁用</el-tag>
+      </template>
+    </custom-table>
+    <el-dialog v-model="dialog.show" :title="dialog.title" :destroy-on-close="false" @close="toClose" :top="dialog.top">
+      <el-row>
+        <el-col :span="24" v-if="dialog.type == '1'">
+          <custom-form v-model="form" :fields="formFields" :rules="rules" @save="toSave">
+            <template #is_use>
+              <el-radio v-for="i in isUseList" :key="i.id" :label="i.value">{{ i.label }}</el-radio>
+            </template>
+            <template #problem>
+              <setTable></setTable>
+            </template>
+            <template #brief>
+              <WangEditor v-model="form.brief" />
+            </template>
+          </custom-form>
+        </el-col>
+      </el-row>
+    </el-dialog>
+  </div>
 </template>
 
 <script setup>
+const $checkRes = inject('$checkRes')
+import { cloneDeep, get } from 'lodash-es'
+const { t } = useI18n()
+
+import setTable from './parts/table.vue'
+
+// 接口
+import { DegreeStore } from '@/store/api/degree'
+import { DictDataStore } from '@/store/api/system/dictData'
+const store = DegreeStore()
+const dictDataStore = DictDataStore()
+const data = ref([])
+const searchForm = ref({})
+const fields = [
+  { label: '标题', model: 'title' },
+  { label: '是否启用', model: 'is_use', format: (i) => getDict(i), custom: true }
+]
+const opera = [
+  { label: t('common.update'), method: 'edit' },
+  { label: t('common.delete'), method: 'delete', confirm: true, type: 'danger' }
+]
+const buttonFields = [{ label: t('common.add'), method: 'add' }]
+let skip = 0
+let limit = inject('limit')
+const total = ref(20)
+const isUseList = ref([])
 // 加载中
 const loading = ref(false)
+const formFields = [
+  { label: '标题', model: 'title' },
+  { label: '问题', model: 'problem', custom: true },
+  { label: '描述', model: 'brief', custom: true },
+  { label: '是否启用', model: 'is_use', type: 'radio' }
+]
+const rules = reactive({
+  title: [{ required: true, message: '请输入调查表标题', trigger: 'blur' }],
+  brief: [{ required: true, message: '请输入描述', trigger: 'blur' }],
+  problem: [{ required: true, message: '请输入问题', trigger: 'blur' }]
+})
+const dialog = ref({ type: '1', show: false, title: '满意度调查表', top: '15vh' })
+const form = ref({ problem: [] })
+// 请求
+onMounted(async () => {
+  loading.value = true
+  await searchOther()
+  await search({ skip, limit })
+  loading.value = false
+})
+
+const searchOther = async () => {
+  const result = await dictDataStore.query({ code: 'isUse', is_use: '0' })
+  if ($checkRes(result)) isUseList.value = result.data.data
+}
+const search = async (query = { skip: 0, limit }) => {
+  const info = { skip: query.skip, limit: query.limit, ...searchForm.value }
+  const res = await store.query(info)
+  if (res.errcode == '0') {
+    data.value = res.data.data
+    total.value = res.data.total
+  }
+}
+// 字典数据转换
+const getDict = (data) => {
+  const res = isUseList.value.find((f) => f.value == data)
+  return get(res, 'label')
+}
+// 添加
+const toAdd = () => {
+  dialog.value = { type: '1', show: true, title: '新增满意度调查表', top: '15vh' }
+}
+// 修改
+const toEdit = (data) => {
+  form.value = data
+  dialog.value = { type: '1', show: true, title: '修改满意度调查表', top: '15vh' }
+}
+// 删除
+const toDelete = async (data) => {
+  const res = await store.del(data.id)
+  if ($checkRes(res, true)) {
+    search({ skip: 0, limit })
+  }
+}
+const toSave = async () => {
+  const data = cloneDeep(form.value)
+  let res
+  if (get(data, 'id')) res = await store.update(data)
+  else res = await store.create(data)
+  if ($checkRes(res, true)) {
+    search({ skip: 0, limit })
+    toClose()
+  }
+}
+// 重置
+const toReset = async () => {
+  searchForm.value = {}
+  await search({ skip, limit })
+}
+const toClose = () => {
+  form.value = { problem: [] }
+  dialog.value = { show: false }
+}
+provide('form', form)
+provide('isUseList', isUseList)
 </script>
-<style scoped></style>
+<style scoped lang="scss"></style>

+ 160 - 0
src/views/system/setting/parts/table.vue

@@ -0,0 +1,160 @@
+<template>
+  <el-col :span="24" class="table">
+    <el-col :span="24" class="table_1">
+      <el-button type="primary" @click="addProblem()"> 添加 </el-button>
+    </el-col>
+    <el-col :span="24">
+      <el-table :data="form.problem" border>
+        <el-table-column type="index" label="序号" width="80" align="center"> </el-table-column>
+        <el-table-column prop="name" label="问题" align="center" />
+        <el-table-column prop="type" label="类型" align="center">
+          <template #default="{ row }">{{ getDict(row.type) }}</template>
+        </el-table-column>
+        <el-table-column label="操作" align="center">
+          <template #default="scope">
+            <el-button type="primary" @click="updateProblem(scope.row)"> 修改 </el-button>
+            <el-button type="danger" @click="delProblem(scope.row)"> 删除 </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+    </el-col>
+    <el-dialog v-model="dialog" title="问题信息" :destroy-on-close="false" @close="toClose" width="40%">
+      <custom-form v-model="proForm" :fields="proFormFields" :rules="rules" @save="toSave">
+        <template #type>
+          <el-option v-for="i in typeList" :key="i.value" :label="i.label" :value="i.value"></el-option>
+        </template>
+        <template #answer>
+          <div class="answer" v-if="proForm.type == '2' || proForm.type == '3'">
+            <el-col :span="24" class="add">
+              <el-button type="primary" @click="addAnswer()">添加</el-button>
+            </el-col>
+            <el-table :data="answerList" border>
+              <el-table-column type="index" label="序号" width="80" align="center" />
+              <el-table-column prop="text" label="答案" align="center">
+                <template #default="scope">
+                  <el-input v-model="scope.row.text" placeholder="请输入答案" />
+                </template>
+              </el-table-column>
+              <el-table-column label="操作" align="center" width="100">
+                <template #default="scope">
+                  <el-button type="danger" @click="delAnswer(scope.row)">删除</el-button>
+                </template>
+              </el-table-column>
+            </el-table>
+          </div>
+        </template>
+        <template #is_must>
+          <el-radio v-for="i in isUseList" :key="i.id" :label="i.value">{{ i.label }}</el-radio>
+        </template>
+      </custom-form>
+    </el-dialog>
+  </el-col>
+</template>
+
+<script setup>
+import moment from 'moment'
+import { get } from 'lodash-es'
+
+const form = inject('form')
+const isUseList = inject('isUseList')
+
+const proForm = ref({})
+const dialog = ref(false)
+const proFormFields = [
+  { label: '标题', model: 'name' },
+  { label: '类型', model: 'type', type: 'select' },
+  {
+    label: '答案',
+    model: 'answer',
+    custom: true,
+    display: () => proForm.value.type == '2' || proForm.value.type == '3'
+  },
+  { label: '是否必填', model: 'is_must', type: 'radio' },
+  { label: '备注', model: 'remark', type: 'textarea' }
+]
+
+const answerList = ref([])
+const typeList = ref([
+  { value: '0', label: '单行文本' },
+  { value: '1', label: '多行文本' },
+  { value: '2', label: '单选' },
+  { value: '3', label: '下拉' },
+  { value: '4', label: '数值类型' },
+  { value: '5', label: '日期类型' }
+])
+
+// 问题添加
+const addProblem = () => {
+  dialog.value = true
+  proForm.value = {
+    id: moment().valueOf(),
+    name: '',
+    type: '',
+    is_must: '0',
+    reply: '',
+    answer: '',
+    remark: ''
+  }
+}
+// 保存
+const toSave = async (data) => {
+  if (answerList.value && answerList.value.length > 0) data.answer = answerList.value
+  let investigate = form.value.problem.find((i) => i.id == data.id)
+  if (investigate) {
+    form.value.problem = form.value.problem.map((i) => {
+      if (i.id == data.id) return data
+      else return i
+    })
+  } else form.value.problem.push(data)
+  await toClose()
+}
+// 问题修改
+const updateProblem = (e) => {
+  dialog.value = true
+  if (e.answer && e.answer.length > 0) answerList.value = e.answer
+  proForm.value = e
+}
+// 问题删除
+const delProblem = (e) => {
+  let problem = form.value.problem.filter((i) => i.id != e.id)
+  form.value.problem = problem
+}
+// 答案添加
+const addAnswer = () => {
+  let list = answerList.value || []
+  list.push({ id: moment().valueOf(), text: '' })
+  answerList.value = list
+}
+//  答案删除
+const delAnswer = async (e) => {
+  let list = answerList.value.filter((i) => i.id != e.id)
+  answerList.value = list
+}
+// 字典数据转换
+const getDict = (data) => {
+  const res = typeList.value.find((f) => f.value == data)
+  return get(res, 'label')
+}
+const toClose = () => {
+  proForm.value = {}
+  answerList.value = []
+  dialog.value = false
+}
+</script>
+<style scoped lang="scss">
+.table {
+  .table_1 {
+    margin: 0 0 10px 0;
+  }
+  .answer {
+    width: 100%;
+    .add {
+      margin: 0 0 10px 0;
+    }
+  }
+
+  :deep(.el-form-item) {
+    margin-bottom: 15px !important;
+  }
+}
+</style>