tangning hai 1 ano
pai
achega
8d32a0b093

+ 5 - 0
auto-imports.d.ts

@@ -0,0 +1,5 @@
+// Generated by 'unplugin-auto-import'
+export {}
+declare global {
+
+}

+ 2 - 2
build/vite/plugins/autoImport.ts

@@ -4,7 +4,7 @@
  */
 
 import AutoImport from 'unplugin-auto-import/vite';
-import { VarletUIResolver, VantResolver } from 'unplugin-vue-components/resolvers';
+import { VarletUIResolver, VantResolver, AntDesignVueResolver } from 'unplugin-vue-components/resolvers';
 
 export const AutoImportDeps = () => {
   return AutoImport({
@@ -17,6 +17,6 @@ export const AutoImportDeps = () => {
         '@vueuse/core': [],
       },
     ],
-    resolvers: [VarletUIResolver(), VantResolver()],
+    resolvers: [VarletUIResolver(), VantResolver(), AntDesignVueResolver()],
   });
 };

+ 20 - 5
components.d.ts

@@ -1,22 +1,37 @@
-// generated by unplugin-vue-components
-// We suggest you to commit this file into source control
+/* eslint-disable */
+/* prettier-ignore */
+// @ts-nocheck
+// Generated by unplugin-vue-components
 // Read more: https://github.com/vuejs/core/pull/3399
-import '@vue/runtime-core'
-
 export {}
 
-declare module '@vue/runtime-core' {
+declare module 'vue' {
   export interface GlobalComponents {
+    AButton: typeof import('ant-design-vue/es')['Button']
+    ACascader: typeof import('ant-design-vue/es')['Cascader']
+    AForm: typeof import('ant-design-vue/es')['Form']
+    AFormItem: typeof import('ant-design-vue/es')['FormItem']
+    AInput: typeof import('ant-design-vue/es')['Input']
+    ARadioButton: typeof import('ant-design-vue/es')['RadioButton']
+    ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
+    ASelect: typeof import('ant-design-vue/es')['Select']
+    ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
+    ATextarea: typeof import('ant-design-vue/es')['Textarea']
+    AUpload: typeof import('ant-design-vue/es')['Upload']
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterView: typeof import('vue-router')['RouterView']
     TitleBar: typeof import('./src/components/TitleBar/index.vue')['default']
+    VanArea: typeof import('vant/es')['Area']
     VanButton: typeof import('vant/es')['Button']
+    VanCascader: typeof import('vant/es')['Cascader']
     VanCellGroup: typeof import('vant/es')['CellGroup']
     VanDivider: typeof import('vant/es')['Divider']
     VanField: typeof import('vant/es')['Field']
     VanForm: typeof import('vant/es')['Form']
     VanIcon: typeof import('vant/es')['Icon']
     VanList: typeof import('vant/es')['List']
+    VanPicker: typeof import('vant/es')['Picker']
+    VanPopup: typeof import('vant/es')['Popup']
     VanRadio: typeof import('vant/es')['Radio']
     VanRadioGroup: typeof import('vant/es')['RadioGroup']
     VanRate: typeof import('vant/es')['Rate']

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 26397 - 0
package-lock.json


+ 10 - 4
package.json

@@ -17,10 +17,13 @@
     "commit": "git add . && git-cz"
   },
   "dependencies": {
+    "@ant-design/icons-vue": "^7.0.1",
     "@nutui/nutui": "^3.3.1",
+    "@vant/touch-emulator": "^1.4.0",
     "@varlet/ui": "^2.4.2",
     "@vueuse/core": "9.8.2",
     "@vueuse/integrations": "9.8.2",
+    "ant-design-vue": "4.x",
     "axios": "1.2.1",
     "dayjs": "^1.11.7",
     "mitt": "^3.0.0",
@@ -39,7 +42,7 @@
     "@vitejs/plugin-vue": "^4.0.0",
     "@vitejs/plugin-vue-jsx": "^3.0.0",
     "amfe-flexible": "^2.2.1",
-    "autoprefixer": "^10.4.13",
+    "autoprefixer": "^10.4.17",
     "cnjm-postcss-px-to-viewport": "^1.0.0",
     "consola": "^2.15.3",
     "cross-env": "^7.0.3",
@@ -52,7 +55,7 @@
     "husky": "8.0.2",
     "lint-staged": "13.1.0",
     "mockjs": "^1.1.0",
-    "postcss": "^8.4.20",
+    "postcss": "^8.4.33",
     "postcss-html": "1.5.0",
     "postcss-less": "^6.0.0",
     "prettier": "^2.8.1",
@@ -63,10 +66,11 @@
     "stylelint-config-recommended-vue": "^1.4.0",
     "stylelint-config-standard": "^29.0.0",
     "stylelint-order": "^5.0.0",
+    "tailwindcss": "^3.4.1",
     "terser": "^5.16.1",
     "typescript": "^4.9.4",
     "unplugin-auto-import": "^0.12.1",
-    "unplugin-vue-components": "^0.22.12",
+    "unplugin-vue-components": "^0.26.0",
     "vite": "^4.0.2",
     "vite-plugin-compression": "^0.5.1",
     "vite-plugin-eruda": "^1.0.1",
@@ -78,8 +82,10 @@
     "vite-plugin-style-import": "^2.0.0",
     "vite-plugin-svg-icons": "^2.0.1",
     "vite-plugin-vue-setup-extend": "^0.4.0",
+    "vite-plugin-windicss": "^1.9.3",
     "vue-eslint-parser": "^9.1.0",
-    "vue-tsc": "^1.0.16"
+    "vue-tsc": "^1.0.16",
+    "windicss": "^3.5.6"
   },
   "husky": {
     "hooks": {

+ 0 - 30
postcss.config.js

@@ -1,30 +0,0 @@
-const path = require('path');
-
-const judgeComponent = (file) => {
-  const ignore = ['vant', '@vant'];
-  return ignore.some((item) => path.join(file).includes(path.join('node_modules', item)));
-};
-
-module.exports = {
-  plugins: {
-    autoprefixer: { overrideBrowserslist: ['Android 4.1', 'iOS 7.1', 'Chrome > 31', 'ff > 31', 'ie >= 8'] },
-    'cnjm-postcss-px-to-viewport': {
-      unitToConvert: 'px', // 要转化的单位
-      viewportWidth: 375, // UI设计稿的宽度
-      unitPrecision: 6, // 转换后的精度,即小数点位数
-      propList: ['*'], // 指定转换的css属性的单位,*代表全部css属性的单位都进行转换
-      viewportUnit: 'vw', // 指定需要转换成的视窗单位,默认vw
-      fontViewportUnit: 'vw', // 指定字体需要转换成的视窗单位,默认vw
-      minPixelValue: 1, // 默认值1,小于或等于1px则不进行转换
-      mediaQuery: true, // 是否在媒体查询的css代码中也进行转换,默认false
-      replace: true, // 是否转换后直接更换属性值
-      include: [],
-      exclude: [], // 设置忽略文件,用正则做目录名匹配
-      customFun: ({ file }) => {
-        // 这个自定义的方法是针对处理vant组件下的设计稿为375问题
-        const designWidth = judgeComponent(file) ? 375 : 375;
-        return designWidth;
-      },
-    },
-  },
-};

+ 20 - 12
src/App.vue

@@ -6,13 +6,20 @@
   import { onMounted, computed } from 'vue';
   import { useUserStore } from '/@/store/modules/user';
   import { useCookies } from '@vueuse/integrations/useCookies';
+  import { useRouter } from 'vue-router';
   const userStore = useUserStore();
   const wxOpenId = computed(() => {
     return userStore.getWxOpenId;
   });
   onMounted(async () => {
     let code = getUrlKey('code'); //获取url参数code
-    if (wxOpenId.value) {
+    // let isWeixn = is_weixn();
+    const router = useRouter();
+    const routersName = router.currentRoute.value;
+    console.log('routersName', routersName);
+    if (routersName == 'feedback') {
+      console.log('当前面无需登录');
+    } else if (wxOpenId.value) {
       //
       console.log('已登录', wxOpenId.value);
     } else if (code) {
@@ -23,8 +30,8 @@
       getCodeApi(123);
     }
     let clear = getUrlKey('clear'); //获取url参数code
-    if(clear){
-      useCookies().set('wxOpenId', '')
+    if (clear) {
+      useCookies().set('wxOpenId', '');
       getCodeApi(123);
     }
   });
@@ -38,11 +45,12 @@
   }
   function getCodeApi(state) {
     //获取code
+    console.log('getCodeApi');
     let urlNow = encodeURIComponent(window.location.href);
     let scope = 'snsapi_base'; //snsapi_userinfo   //静默授权 用户无感知
     let appid = 'wxac3d59ea82d9b82a';
     let url = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appid}&redirect_uri=${urlNow}&response_type=code&scope=${scope}&state=${state}#wechat_redirect`;
-    window.location.replace(url);
+    // window.location.replace(url);
   }
 </script>
 <style>
@@ -57,13 +65,13 @@
   .van-toast {
     background: rgba(0, 0, 0, 0.7) !important;
   }
-  .htmlText{
-      overflow: hidden;
-      text-overflow: ellipsis;
-      width: 100%;
-      display: -webkit-box;
-      -webkit-box-orient: vertical;
-      -webkit-line-clamp: 3;
-      word-break: break-all;
+  .htmlText {
+    overflow: hidden;
+    text-overflow: ellipsis;
+    width: 100%;
+    display: -webkit-box;
+    -webkit-box-orient: vertical;
+    -webkit-line-clamp: 3;
+    word-break: break-all;
   }
 </style>

+ 33 - 0
src/api/feedback.ts

@@ -0,0 +1,33 @@
+import useAxiosApi from '/@/utils/useAxiosApi';
+import request from '/@/utils/request';
+
+/**
+ * 用户反馈管理/h5获取默认地址
+ * @returns UseAxiosReturn
+ */
+export const getDefaultAddress = () =>
+  request({
+    url: `/service/manage/feedback/h5/getDefaultAddress`,
+    method: 'get',
+    params: {},
+  });
+/**
+ * 选项配置/获取全部选项设置列表
+ * @returns UseAxiosReturn
+ */
+export const getAllByTypeId = (typeId) =>
+  request({
+    url: `/service/manage/feedback/h5/getAllByTypeId/${typeId}`,
+    method: 'get',
+    params: {},
+  });
+/**
+ * 账号密码登录
+ * @returns UseAxiosReturn
+ */
+export const feedbackAdd = (params) =>
+  request({
+    url: `/service/manage/feedback/h5/add`,
+    method: 'post',
+    data: params,
+  });

+ 7 - 0
src/assets/app.css

@@ -9,3 +9,10 @@ h6,
 p {
   margin: 0;
 }
+
+/* :root {
+  --van-font-size-xs: 12px !important;
+  --van-font-size-sm: 14px !important;
+  --van-font-size-md: 16px !important;
+  --van-font-size-lg: 18px !important;
+} */

BIN=BIN
src/assets/image/bg_mc.png


BIN=BIN
src/assets/image/bg_pc.png


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 25 - 0
src/assets/image/logo_4dage.svg


+ 4 - 0
src/i18n/lang/lang-base.ts

@@ -15,4 +15,8 @@ export type langType = {
     support: string;
     cssMultiLanguage: string;
   };
+  feedback: {
+    title: string;
+    title1: string;
+  };
 };

+ 19 - 0
src/i18n/lang/zh-cn.ts

@@ -17,4 +17,23 @@ export const lang: langType = {
     support: '支持',
     cssMultiLanguage: 'css图片多语言',
   },
+  feedback: {
+    upload: '上传',
+    title: '用户反馈',
+    title1: '问题描述',
+    title2: '期望解决方案',
+    title3: '所在行业',
+    title4: '硬件产品',
+    title5: '软件产品',
+    title6: '姓名',
+    title61: '联系方式',
+    title7: '所在国家和地区',
+    title71: '所在城市',
+    title8: '产品评分',
+    title9: '评分理由',
+    Submit: '提交',
+    settext: '请输入',
+    setselcet: '请选择',
+    fileTips: '请上传图片或视频,支持jpg、png格式,不大于5MB;mp4不大于50MB;',
+  }
 };

+ 11 - 6
src/main.ts

@@ -1,21 +1,26 @@
 import { createApp } from 'vue';
 import App from './App.vue';
 // import { nutUiComponents } from './plugins/nutUI';
+import 'virtual:windi.css';
 import { Toast } from 'vant';
 import { i18n } from '/@/i18n';
 import router from '/@/router';
 // ① 引入createPinia方法从pinia
-import { createPinia } from 'pinia'
-// ② 拿到pinia实例
-const pinia = createPinia()
+import { createPinia } from 'pinia';
+// ② 拿到pinia实例;
+const pinia = createPinia();
 // 1 引入数据持久化插件
-import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
+import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
 // 2 pinia使用数据持久化插件
-pinia.use(piniaPluginPersistedstate)
+pinia.use(piniaPluginPersistedstate);
 
-import 'vant/lib/index.css'
+import 'vant/lib/index.css';
 import './assets/font/iconfont.css';
 import './assets/app.css';
+// import Antd from 'ant-design-vue';
+// import 'ant-design-vue/dist/reset.css';
+// 全局引入Antd
+// app.use(Antd);
 const app = createApp(App);
 app.use(Toast);
 // 路由

+ 26 - 1
src/router/index.ts

@@ -1,4 +1,5 @@
 import { createRouter, createWebHashHistory, Router } from 'vue-router';
+import { useCookies } from '@vueuse/integrations/useCookies';
 import routes from './routes';
 const router: Router = createRouter({
   history: createWebHashHistory(),
@@ -6,7 +7,31 @@ const router: Router = createRouter({
 });
 
 router.beforeEach(async (_to, _from, next) => {
-  next();
+  console.log('_to', _to);
+  let wxOpenId = useCookies().get('wxOpenId');
+  if (wxOpenId || _to.name == 'feedback' || getUrlKey('code')) {
+    next();
+  } else {
+    getCodeApi(123);
+  }
 });
 
+function getUrlKey(name) {
+  //获取url 参数
+  return (
+    decodeURIComponent(
+      (new RegExp('[?|&]' + name + '=' + '([^&;]+?)(&|#|;|$)').exec(location.href) || [, ''])[1].replace(/\+/g, '%20'),
+    ) || null
+  );
+}
+
+function getCodeApi(state) {
+  //获取code
+  console.log('getCodeApi');
+  let urlNow = encodeURIComponent(window.location.href);
+  let scope = 'snsapi_base'; //snsapi_userinfo   //静默授权 用户无感知
+  let appid = 'wxac3d59ea82d9b82a';
+  let url = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appid}&redirect_uri=${urlNow}&response_type=code&scope=${scope}&state=${state}#wechat_redirect`;
+  window.location.replace(url);
+}
 export default router;

+ 9 - 0
src/router/routes.ts

@@ -59,6 +59,15 @@ const routes = [
         },
       },
       {
+        path: 'feedback',
+        name: 'feedback',
+        component: () => import('/@/views/feedback/index.vue'),
+        meta: {
+          title: '用户意见反馈',
+          keepAlive: true,
+        },
+      },
+      {
         path: 'invoice/:id',
         name: 'invoice',
         component: () => import('/@/views/detail/invoice.vue'),

+ 1 - 2
src/utils/request.ts

@@ -48,7 +48,6 @@ const request = async <T = any>(
     );
   const { data } = await service.request<ResponseData<T>>(config);
   // 请求失败
-  console.log('request',data)
 //   reduce()
   if (typeof data !== "object") {
     showFailToast({
@@ -58,7 +57,7 @@ const request = async <T = any>(
     });
     return Promise.reject(data);
   }
-  if (data.code != 200) {
+  if (!(data.code == 200 || data.code == 0 )) {
     if (data.message) showFailToast({
       message:data.message,
       icon: 'warning-o',

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 5354 - 0
src/views/feedback/area.json


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 2484 - 0
src/views/feedback/country.json


+ 171 - 0
src/views/feedback/index.vue

@@ -0,0 +1,171 @@
+<template>
+  <div class="feedback">
+    <div class="feedbackTop">
+      <img src="/src/assets/image/logo_4dage.svg" alt="" />
+      <div class="title">用户反馈</div>
+    </div>
+    <div class="submit bg-white">
+      <mcSubmit class="submitMc" ref="submitMc" :columns="columns" @submit="onSubmit" />
+      <pcSubmit class="submitPc" ref="submitPc" :columns="columns" @submit="onSubmit" />
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup name="HomeSubmit">
+  // import { showLoadingToast, closeToast } from 'vant';
+  import { computed, reactive, unref, ref, onMounted } from 'vue';
+  import { useUserStore } from '/@/store/modules/user';
+  import { showToast, showSuccessToast } from 'vant';
+  import mcSubmit from './mcSubmit.vue';
+  import pcSubmit from './pcSubmit.vue';
+  import axios from 'axios';
+  // import useAxiosApi from '/@/utils/useAxiosApi';
+  // import { Toast } from '@nutui/nutui';
+  import { getAllByTypeId, feedbackAdd } from '/@/api/feedback';
+  // import { setLang } from '/@/i18n';
+  // import { useI18n } from 'vue-i18n';
+  const emit = defineEmits(['setActive']);
+  const submitPc = ref(null);
+  const submitMc = ref(null);
+  const userStore = useUserStore();
+  const wxOpenId = computed(() => {
+    return userStore.getWxOpenId;
+  });
+  const columns = reactive({
+    industryOptionId: [],
+    softwareOptionId: [],
+    hardwareOptionId: [],
+  });
+  async function onSubmit(formData) {
+    console.log('feedbackAdd', formData);
+    // return new Promise((resolve) => {
+    let apiData = {
+      ...formData,
+      address: formData.countries ? `${formData.country},${formData.countries}` : formData.country,
+      problemDescImgs: formData.problemDescImgs.map((ele) => {
+        return ele.response ? ele.response.data : ele.url;
+      } ),
+      solutionImgs: formData.solutionImgs.map((ele) => {
+        return ele.response ? ele.response.data : ele.url;
+      } ),
+    };
+    const { response } = await feedbackAdd(apiData);
+    let data = unref(response);
+    showSuccessToast('提交成功');
+    formData.problemDesc = '';
+    formData.problemDescImgs = [];
+    formData.solution = '';
+    formData.solutionImgs = [];
+    formData.hardwareOptionId = '';
+    formData.softwareOptionId = '';
+    formData.industryOptionId = '';
+    formData.nickName = '';
+    formData.phone = '';
+    formData.country = '';
+    formData.address = '';
+    formData.countries = [];
+    formData.score = 5;
+    formData.scoreReason = '';
+  }
+  onMounted(async () => {
+    let industryRes = await getAllByTypeId(1);
+    columns.industryOptionId = industryRes.data.map((ele) => {
+      return {
+        text: ele.nameCn,
+        label: ele.nameCn,
+        value: ele.id,
+      };
+    });
+    let hardwareRes = await getAllByTypeId(2);
+    columns.hardwareOptionId = hardwareRes.data.map((ele) => {
+      return {
+        text: ele.nameCn,
+        label: ele.nameCn,
+        value: ele.id,
+      };
+    });
+    let softwareRes = await getAllByTypeId(3);
+    columns.softwareOptionId = softwareRes.data.map((ele) => {
+      return {
+        text: ele.nameCn,
+        label: ele.nameCn,
+        value: ele.id,
+      };
+    });
+  });
+</script>
+
+<style lang="scss" scoped>
+  .feedback {
+    background: #e5edf9;
+    min-height: 100vh;
+    min-width: 100vw;
+    padding: 16px;
+    .submit {
+      max-width: 1088px;
+      padding: 64px 48px;
+      margin: 0 auto;
+      .submitMc {
+        display: none;
+      }
+      .submitPc {
+        display: block;
+      }
+    }
+    @media screen and (max-width: 450px) {
+      ::v-deep .submit {
+        padding: 24px 0;
+        .submitPc {
+          display: none;
+        }
+        .submitMc {
+          display: block;
+        }
+      }
+    }
+    /* 在屏幕宽度小于 800px 时应用的样式 */
+    .feedbackTop {
+      max-width: 1088px;
+      margin: 0 auto;
+      padding-top: 14px;
+      width: 100%;
+      .logo {
+        height: 34px;
+        width: auto;
+        margin: 32px 0 16px 0;
+      }
+      .title {
+        font-size: 40px;
+        font-family: Microsoft YaHei, Microsoft YaHei;
+        font-weight: bold;
+        color: #042d5c;
+        line-height: 47px;
+        letter-spacing: 8px;
+        text-align: center;
+        height: 120px;
+        background: url(../../assets/image/bg_pc.png) no-repeat;
+        margin-top: 30px;
+        background-size: 1088px 120px;
+      }
+    }
+    @media screen and (max-width: 450px) {
+      ::v-deep .feedbackTop {
+        .title {
+          margin-top: 32px;
+          background: url(../../assets/image/bg_mc.png) no-repeat;
+          background-size: 100% auto;
+          background-position-y: -14px;
+          font-size: 24px;
+          font-family: Source Han Sans CN, Source Han Sans CN;
+          height: 76px;
+          font-weight: bold;
+          color: #042d5c;
+          line-height: 28px;
+        }
+        .submit {
+          margin: 24px;
+        }
+      }
+    }
+  }
+</style>

+ 361 - 0
src/views/feedback/mcSubmit.vue

@@ -0,0 +1,361 @@
+<template>
+  <div class="mcSubmit">
+    <van-form @submit="onSubmit" style="background: #fff">
+      <van-cell-group style="margin: none" inset>
+        <div class="myTitle required"><span class="number">01</span>{{ t('feedback.title1') }}</div>
+        <van-field
+          v-model="formData.problemDesc"
+          label-align="top"
+          name="故障描述"
+          label=""
+          :before-upload="beforeUpload"
+          rows="2"
+          autoSize
+          :max-count="5"
+          type="textarea"
+          maxlength="500"
+          show-word-limit
+          :rules="[{ required: true, message: t('feedback.settext') + t('feedback.title1') }]"
+          :placeholder="t('feedback.settext')"
+        />
+        <van-field name="uploader" label-align="top">
+          <template #input>
+            <div>
+              <div>
+                <van-uploader
+                  :before-read="beforeRead"
+                  :after-read="clzpAfterRead"
+                  accept=".jpg,.png,.mp4"
+                  :max-count="6"
+                  v-model="formData.problemDescImgs"
+                />
+              </div>
+              <div class="tips">{{ t('feedback.fileTips') }}</div>
+            </div>
+          </template>
+        </van-field>
+        <div class="myTitle required"><span class="number">02</span>{{ t('feedback.title2') }}</div>
+        <van-field
+          v-model="formData.solution"
+          label-align="top"
+          name="故障描述"
+          label=""
+          rows="2"
+          autoSize
+          type="textarea"
+          maxlength="500"
+          show-word-limit
+          :rules="[{ required: true, message: t('feedback.settext') + t('feedback.title2') }]"
+          :placeholder="t('feedback.settext')"
+        />
+        <van-field name="uploader" label-align="top">
+          <template #input>
+            <div>
+              <div>
+                <van-uploader :before-read="beforeRead" :after-read="clzpAfterRead" :max-count="6" v-model="formData.solutionImgs" />
+              </div>
+              <div class="tips">{{ t('feedback.fileTips') }}</div>
+            </div>
+          </template>
+        </van-field>
+        <div class="myTitle required"><span class="number">03</span>{{ t('feedback.title3') }}</div>
+        <van-field
+          v-model="formData.industryOptionId"
+          is-link
+          readonly
+          name="picker"
+          label=""
+          :placeholder="t('feedback.setselcet')"
+          :rules="[{ required: true, message: t('feedback.setselcet') + t('feedback.title3') }]"
+          @click="showPicker.industryOptionId = true"
+        />
+        <van-popup v-model:show="showPicker.industryOptionId" position="bottom">
+          <van-picker
+            :columns="propsOptions.industryOptionId"
+            @confirm="(val) => onConfirm(val, 'industryOptionId')"
+            @cancel="showPicker.industryOptionId = false"
+          />
+        </van-popup>
+        <div class="myTitle required"><span class="number">04</span>{{ t('feedback.title4') }}</div>
+        <van-field
+          v-model="formData.hardwareOptionId"
+          is-link
+          readonly
+          name="picker"
+          label=""
+          :placeholder="t('feedback.setselcet')"
+          :rules="[{ required: true, message: t('feedback.setselcet') + t('feedback.title4') }]"
+          @click="showPicker.hardwareOptionId = true"
+        />
+        <van-popup v-model:show="showPicker.hardwareOptionId" position="bottom">
+          <van-picker
+            :columns="propsOptions.hardwareOptionId"
+            @confirm="(val) => onConfirm(val, 'hardwareOptionId')"
+            @cancel="showPicker.hardwareOptionId = false"
+          />
+        </van-popup>
+        <div class="myTitle required"><span class="number">05</span>{{ t('feedback.title5') }}</div>
+        <van-field
+          v-model="formData.softwareOptionId"
+          is-link
+          readonly
+          name="picker"
+          label=""
+          maxlength="30"
+          :placeholder="t('feedback.setselcet')"
+          :rules="[{ required: true, message: t('feedback.setselcet') + t('feedback.title5') }]"
+          @click="showPicker.softwareOptionId = true"
+        />
+        <van-popup v-model:show="showPicker.softwareOptionId" position="bottom">
+          <van-picker
+            :columns="propsOptions.softwareOptionId"
+            @confirm="(val) => onConfirm(val, 'softwareOptionId')"
+            @cancel="showPicker.softwareOptionId = false"
+          />
+        </van-popup>
+        <div class="myTitle"><span class="number">06</span>{{ t('feedback.title6') }}</div>
+        <van-field
+          v-model="formData.nickName"
+          label-align="top"
+          name="联系电话"
+          maxlength="15"
+          label=""
+          :placeholder="t('feedback.settext')"
+        />
+        <div class="myTitle"><span class="number">07</span>{{ t('feedback.title61') }}</div>
+        <van-field
+          v-model="formData.phone"
+          label-align="top"
+          name="联系电话"
+          maxlength="11"
+          label=""
+          :placeholder="t('feedback.settext')"
+        />
+        <div class="myTitle"><span class="number">08</span>{{ t('feedback.title7') }}</div>
+        <van-field
+          v-model="formData.country"
+          is-link
+          readonly
+          name="picker"
+          label=""
+          :placeholder="t('feedback.setselcet')"
+          @click="showPicker.country = true"
+        />
+        <van-popup v-model:show="showPicker.country" position="bottom">
+          <van-picker :columns="columnsCountry" @confirm="(val) => onConfirm(val, 'country')" @cancel="showPicker.country = false" />
+        </van-popup>
+        <div class="myTitle" v-if="formData.country == '中国'"
+          ><span class="number">{{ formData.country == '中国' ? '09' : '08' }}</span
+          >{{ t('feedback.title71') }}</div
+        >
+        <van-field
+          v-if="formData.country == '中国'"
+          v-model="formData.city"
+          is-link
+          readonly
+          name="area"
+          label=""
+          :placeholder="t('feedback.setselcet')"
+          @click="showPicker.city = true"
+        />
+        <van-popup v-model:show="showPicker.city" position="bottom">
+          <van-cascader
+            v-model="cascaderValue"
+            title="请选择所在地区"
+            :options="columnsCity"
+            @close="showPicker.city = false"
+            @finish="onFinish"
+          />
+        </van-popup>
+        <div class="myTitle"
+          ><span class="number">{{ formData.country == '中国' ? '10' : '09' }}</span
+          >{{ t('feedback.title8') }}</div
+        >
+        <van-field name="rate" label="">
+          <template #input>
+            <van-rate color="#FADB14" void-color="#D9D9D9" :allow-half="true" v-model="formData.score" />
+          </template>
+        </van-field>
+        <div class="myTitle"
+          ><span class="number">{{ formData.country == '中国' ? '11' : '10' }}</span
+          >{{ t('feedback.title9') }}</div
+        >
+        <van-field
+          v-model="formData.scoreReason"
+          label-align="top"
+          name="联系电话"
+          maxlength="30"
+          label=""
+          :placeholder="t('feedback.settext')"
+        />
+        <div style="margin: 16px; background-color: #f5f5f5">
+          <van-button style="height: 44px" block color="#00B3EC" type="primary" native-type="submit"> 提交 </van-button>
+        </div>
+      </van-cell-group>
+    </van-form>
+  </div>
+</template>
+
+<script setup>
+  import { ref } from 'vue';
+  import cityList from './area.json';
+  import countryList from './country.json';
+  import axios from 'axios';
+  const areaList = ref({});
+  const props = defineProps(['formData', 'columns']);
+  const emit = defineEmits(['submit']);
+  const propsOptions = props.columns;
+  const loading = ref(false);
+  const setObjId = ref({})
+  const formData = ref({
+    problemDesc: '',
+    problemDescImgs: [],
+    hardwareOptionId: null,
+    softwareOptionId: null,
+    industryOptionId: null,
+    solution: '',
+    solutionImgs: [],
+    nickName: '',
+    phone: '',
+    address: '',
+    score: 0,
+    scoreReason: '',
+  });
+  import { useI18n } from 'vue-i18n';
+  const { t } = useI18n();
+  const showPicker = ref({
+    country: false,
+    hardwareOptionId: false,
+    softwareOptionId: false,
+    city: false,
+  });
+  const columnsCountry = countryList.map((ele) => {
+    return { text: ele.chinese, value: ele.chinese };
+  });
+  const columnsCity = cityList.map((ele) => {
+    return {
+      text: ele.name,
+      value: ele.name,
+      children: ele.city.map((element) => {
+        return {
+          text: element.name,
+          value: element.name,
+        };
+      }),
+    };
+  });
+  // 全部选项选择完毕后,会触发 finish 事件
+  const onFinish = ({ selectedOptions }, b) => {
+    showPicker.value.city = false;
+    formData.value.city = selectedOptions.map((option) => option.text).join('/');
+  };
+  const onConfirm = ({ selectedOptions, selectedValues }, b) => {
+    formData.value[b] = selectedOptions[0].text;
+    setObjId.value[b] = selectedValues.join(',');
+    showPicker.value[b] = false;
+  };
+  const onSubmit = () => {
+    formData.value = {
+      ...formData.value,
+      ...setObjId.value,
+    }
+    emit('submit', formData.value);
+  };
+  const beforeUpload = (file) => {
+    console.log('beforeUpload', file);
+    const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png';
+    if (!isJpgOrPng) {
+      message.error('You can only upload JPG file!');
+    }
+    const isLt2M = file.size / 1024 / 1024 < 2;
+    if (!isLt2M) {
+      message.error('Image must smaller than 2MB!');
+    }
+    return isJpgOrPng && isLt2M;
+  }; //校验图片的格式
+  function beforeRead(file) {
+    if (!/(jpg|jpeg|png|JPG|PNG|mp4)/i.test(file.type)) {
+      showToast(t('feedback.upload'));
+      return false;
+    }
+    return true;
+  }
+  //照片上传事件方法
+  function clzpAfterRead(file) {
+    // 上传状态提示开启
+    file.status = 'uploading';
+    file.message = '上传中...';
+    loading.value = true;
+    // 创建一个空对象实例
+    let formData = new FormData();
+    // 调用append()方法添加数据
+    formData.append('file', file.file);
+    axios({
+      url: '/service/sale/upload/file',
+      method: 'POST',
+      data: formData,
+      headers: {
+        'Content-Type': 'multipart/form-data',
+      },
+    })
+      .then((res) => {
+        loading.value = false;
+        let { data } = res;
+        if (data.code == 200) {
+          // 上传状态提示关闭
+          file.url = data.data;
+          file.file = '';
+          file.content = '';
+          file.status = 'done';
+          showToast('上传成功!');
+        }
+        console.log('formData.faultImg', formData, file);
+      })
+      .catch(() => {
+        loading.value = false;
+      });
+  }
+</script>
+
+<style lang="scss" scoped>
+  .mcSubmit {
+    .tips {
+      font-size: 14px;
+      font-family: Source Han Sans CN, Source Han Sans CN;
+      font-weight: 400;
+      color: #cccccc;
+      line-height: 16px;
+      margin-top: 15px;
+    }
+    .required {
+      &::before {
+        display: inline-block;
+        margin-inline-end: 4px;
+        color: #ff4d4f;
+        font-size: 14px;
+        font-family: SimSun, sans-serif;
+        line-height: 1;
+        content: '*';
+      }
+    }
+    .myTitle {
+      position: relative;
+      margin-top: 14px;
+      padding: 0 var(--van-cell-horizontal-padding);
+      .number {
+        font-size: 16px;
+        font-family: Microsoft YaHei, Microsoft YaHei;
+        font-weight: bold;
+        color: #00b3ec;
+      }
+      span {
+        font-size: 16px;
+        font-family: Microsoft YaHei, Microsoft YaHei;
+        font-weight: 400;
+        color: #333333;
+        line-height: 19px;
+        margin-right: 7px;
+      }
+    }
+  }
+</style>

+ 242 - 0
src/views/feedback/pcSubmit.vue

@@ -0,0 +1,242 @@
+<template>
+  <div class="pcSubmit">
+    <a-form layout="vertical" :model="formData" ref="formRef" :rules="rules">
+      <a-form-item name="problemDesc">
+        <div class="myTitle required" slot="label"><span class="number">01</span>{{ t('feedback.title1') }}</div>
+        <a-textarea :autoSize="{ minRows: 3, maxRows: 6 }" v-model:value="formData.problemDesc" :placeholder="t('feedback.settext')" />
+      </a-form-item>
+      <a-form-item name="problemFile">
+        <a-upload
+          v-model:file-list="formData.problemDescImgs"
+          accept=".jpg,.png,.mp4"
+          action="/service/sale/upload/file"
+          list-type="picture-card"
+          :before-upload="beforeUpload"
+        >
+          <div>
+            <PlusOutlined />
+            <div style="margin-top: 8px">{{ t('feedback.upload') }}</div>
+          </div>
+        </a-upload>
+        <div class="tips">{{ t('feedback.fileTips') }}</div>
+      </a-form-item>
+      <a-form-item name="solution">
+        <div class="myTitle required" slot="label"><span class="number">02</span>{{ t('feedback.title2') }}</div>
+        <a-textarea :autoSize="{ minRows: 3, maxRows: 6 }" v-model:value="formData.solution" :placeholder="t('feedback.settext')" />
+      </a-form-item>
+      <a-form-item>
+        <a-upload v-model:file-list="formData.solutionImgs" action="/service/sale/upload/file" list-type="picture-card">
+          <div>
+            <PlusOutlined />
+            <div style="margin-top: 8px">{{ t('feedback.upload') }}</div>
+          </div>
+        </a-upload>
+        <div class="tips">{{ t('feedback.fileTips') }}</div>
+      </a-form-item>
+      <a-form-item name="industryOptionId">
+        <div class="myTitle required" slot="label"><span class="number">03</span>{{ t('feedback.title3') }}</div>
+        <a-select
+          key="industryOptionId"
+          v-model:value="formData.industryOptionId"
+          :placeholder="t('feedback.setselcet')"
+          :options="propsOptions.industryOptionId"
+        />
+      </a-form-item>
+      <a-form-item name="hardwareOptionId">
+        <div class="myTitle required" slot="label"><span class="number">04</span>{{ t('feedback.title4') }}</div>
+        <a-select
+          key="hardwareOptionId"
+          v-model:value="formData.hardwareOptionId"
+          :placeholder="t('feedback.setselcet')"
+          :options="propsOptions.hardwareOptionId"
+        />
+      </a-form-item>
+      <a-form-item name="softwareOptionId">
+        <div class="myTitle required" slot="label"><span class="number">05</span>{{ t('feedback.title5') }}</div>
+        <a-select
+          key="softwareOptionId"
+          v-model:value="formData.softwareOptionId"
+          :placeholder="t('feedback.setselcet')"
+          :options="propsOptions.softwareOptionId"
+        />
+      </a-form-item>
+      <a-form-item>
+        <div class="myTitle" slot="label"><span class="number">06</span>{{ t('feedback.title6') }}</div>
+        <a-input v-model:value="formData.nickName" :placeholder="t('feedback.settext')" />
+      </a-form-item>
+      <a-form-item>
+        <div class="myTitle" slot="label"><span class="number">07</span>{{ t('feedback.title61') }}</div>
+        <a-input v-model:value="formData.phone" :placeholder="t('feedback.settext')" />
+      </a-form-item>
+      <a-form-item>
+        <div class="myTitle" slot="label"><span class="number">08</span>{{ t('feedback.title7') }}</div>
+        <a-select
+          v-model:value="formData.country"
+          :filterOption="filterOption"
+          :options="countryOption"
+          showSearch
+          :placeholder="t('feedback.setselcet')"
+        />
+        <!-- <a-cascader v-model:value="formData.countries" :options="options" placeholder="Please select" /> -->
+      </a-form-item>
+      <a-form-item v-if="formData.country == '中国'">
+        <div class="myTitle" slot="label"
+          ><span class="number">{{ formData.country == '中国' ? '09' : '08' }}</span
+          >{{ t('feedback.title71') }}</div
+        >
+        <a-cascader v-model:value="formData.countries" :options="options" :placeholder="t('feedback.setselcet')" />
+      </a-form-item>
+      <a-form-item>
+        <div class="myTitle" slot="label"
+          ><span class="number">{{ formData.country == '中国' ? '10' : '09' }}</span
+          >{{ t('feedback.title8') }}</div
+        >
+        <van-rate color="#FADB14" void-color="#D9D9D9" :allow-half="true" v-model="formData.score" :placeholder="t('feedback.setselcet')" />
+      </a-form-item>
+      <a-form-item>
+        <div class="myTitle" slot="label"
+          ><span class="number">{{ formData.country == '中国' ? '11' : '10' }}</span
+          >{{ t('feedback.title9') }}</div
+        >
+        <a-input v-model:value="formData.scoreReason" :placeholder="t('feedback.settext')" />
+      </a-form-item>
+      <a-form-item style="text-align: center">
+        <a-button class="w-160px" style="background-color: #00b3ec" type="primary" size="large" @click="onSubmit">{{
+          t('feedback.Submit')
+        }}</a-button>
+      </a-form-item>
+    </a-form>
+  </div>
+</template>
+
+<script setup>
+  import { ref } from 'vue';
+  import { PlusOutlined } from '@ant-design/icons-vue';
+  import cityList from './area.json';
+  import countryList from './country.json';
+  const props = defineProps(['formData', 'columns']);
+  import { message, Upload } from 'ant-design-vue';
+  import { useI18n } from 'vue-i18n';
+  const { t } = useI18n();
+  const emit = defineEmits(['submit']);
+  // eslint-disable-next-line vue/no-setup-props-destructure
+  const formData = ref({
+    problemDesc: '',
+    problemDescImgs: [],
+    hardwareOptionId: null,
+    softwareOptionId: null,
+    industryOptionId: null,
+    solution: '',
+    solutionImgs: [],
+    nickName: '',
+    phone: '',
+    score: 0,
+    scoreReason: '',
+    country: '',
+    address: '',
+    countries: [],
+  });
+  console.log('formData', formData);
+  // eslint-disable-next-line vue/no-setup-props-destructure
+  const propsOptions = props.columns;
+  const formRef = ref(null);
+  const rules = {
+    problemDesc: [{ required: true, message: t('feedback.settext') + t('feedback.title1'), trigger: 'change' }],
+    solution: [{ required: true, message: t('feedback.settext') + t('feedback.title2'), trigger: 'change' }],
+    softwareOptionId: [{ required: true, message: t('feedback.setselcet') + t('feedback.title5'), trigger: 'change' }],
+    industryOptionId: [{ required: true, message: t('feedback.setselcet') + t('feedback.title3'), trigger: 'change' }],
+    hardwareOptionId: [{ required: true, message: t('feedback.setselcet') + t('feedback.title4'), trigger: 'change' }],
+  };
+  const countryOption = countryList.map((ele) => {
+    return { value: ele.chinese, label: ele.chinese, english: ele.english };
+  });
+  function filterOption(inputValue, option) {
+    return (
+      option.label.toLowerCase().indexOf(inputValue.toLowerCase()) >= 0 ||
+      option.english.toLowerCase().indexOf(inputValue.toLowerCase()) >= 0
+    );
+  }
+  const options = cityList.map((ele) => {
+    return {
+      value: ele.name,
+      label: ele.name,
+      children: ele.city.map((element) => {
+        return {
+          value: element.name,
+          label: element.name,
+        };
+      }),
+    };
+  });
+  const beforeUpload = (file) => {
+    console.log('beforeUpload', file);
+    const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png' || file.type === 'video/mp4';
+    if (!isJpgOrPng) {
+      message.error(t('feedback.fileTips'));
+      return false;
+    }
+    const isLt2M = file.size / 1024 / 1024 < 2;
+    const isLt50M = file.size / 1024 / 1024 < 50;
+    if (!isLt2M && (file.type === 'image/jpeg' || file.type === 'image/png')) {
+      message.error(t('feedback.fileTips'));
+      return false;
+    }
+    if (!isLt50M && file.type === 'video/mp4') {
+      message.error(t('feedback.fileTips'));
+      return Upload.LIST_IGNORE;
+    }
+    return file.type === 'video/mp4' ? isLt50M && isJpgOrPng : isJpgOrPng && isLt2M;
+  };
+  const onSubmit = () => {
+    console.log('values', formRef.value.validate());
+    formRef.value
+      .validate()
+      .then(() => {
+        emit('submit', formData.value);
+        console.log('values', formData);
+      })
+      .catch((error) => {
+        console.log('error', error);
+      });
+  };
+</script>
+
+<style lang="scss" scoped>
+  .myTitle {
+    position: relative;
+    margin-bottom: 14px;
+    .number {
+      font-size: 16px;
+      font-family: Microsoft YaHei, Microsoft YaHei;
+      font-weight: bold;
+      color: #00b3ec;
+    }
+    span {
+      font-size: 16px;
+      font-family: Microsoft YaHei, Microsoft YaHei;
+      font-weight: 400;
+      color: #333333;
+      line-height: 19px;
+      margin-right: 7px;
+    }
+  }
+  .tips {
+    font-size: 14px;
+    font-family: Microsoft YaHei, Microsoft YaHei;
+    font-weight: 400;
+    color: #cccccc;
+    line-height: 16px;
+    margin-top: 8px;
+  }
+  .required {
+    &::before {
+      display: inline-block;
+      margin-inline-end: 4px;
+      color: #ff4d4f;
+      font-size: 14px;
+      font-family: SimSun, sans-serif;
+      line-height: 1;
+      content: '*';
+    }
+  }
+</style>

+ 1 - 1
src/views/home/submit.vue

@@ -53,7 +53,7 @@
         name="故障描述"
         label="故障描述"
         rows="2"
-        autosize
+        autoSize
         type="textarea"
         maxlength="500"
         show-word-limit

+ 13 - 2
vite.config.ts

@@ -2,8 +2,10 @@
 import { resolve } from 'path';
 import { ConfigEnv, UserConfig } from 'vite';
 // import { wrapperEnv } from './build/utils';
+import WindiCSS from 'vite-plugin-windicss';
+import AutoImport from 'unplugin-auto-import/vite';
 import Components from 'unplugin-vue-components/vite';
-import { VantResolver } from 'unplugin-vue-components/resolvers';
+import { VantResolver, AntDesignVueResolver } from 'unplugin-vue-components/resolvers';
 import vue from '@vitejs/plugin-vue';
 const pathResolve = (dir: string) => {
   return resolve(process.cwd(), '.', dir);
@@ -48,8 +50,17 @@ export default function (_: ConfigEnv): UserConfig {
     },
     plugins: [
       vue(),
+      WindiCSS(),
+      // AutoImport({
+      //   resolvers: [AntDesignVueResolver()], // api
+      // }),
       Components({
-        resolvers: [VantResolver()],
+        resolvers: [
+          VantResolver(),
+          AntDesignVueResolver({
+            importStyle: false, // css in js
+          }),
+        ],
       }),
     ],
     build: {

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 1788 - 1147
yarn.lock