Browse Source

init

厅长信箱
wxc 2 years ago
commit
fdec69f7ca
  1. 7
      .gitignore
  2. 6
      README.md
  3. 17
      index.html
  4. 26
      package.json
  5. BIN
      public/favicon.png
  6. BIN
      public/imgs/bg_web.png
  7. BIN
      public/imgs/search.png
  8. BIN
      public/imgs/write.png
  9. 3
      public/lib/index.umd.js
  10. 1
      public/lib/processor.worker.js
  11. BIN
      public/logo.png
  12. 3
      src/App.vue
  13. 12
      src/api/auth.js
  14. 21
      src/api/mail.js
  15. 21
      src/api/mailDraft.js
  16. 5
      src/api/sms.js
  17. 212
      src/assets/style/style.scss
  18. 8
      src/assets/style/theme.scss
  19. 29
      src/components/Icon.vue
  20. 44
      src/components/ImgPreview.vue
  21. 74
      src/components/Loading.vue
  22. 14
      src/layout/Index.vue
  23. 18
      src/main.js
  24. 51
      src/permission.js
  25. 39
      src/router/index.js
  26. 51
      src/store/dept.js
  27. 15
      src/store/page.js
  28. 28
      src/store/user.js
  29. 169
      src/util/audio.js
  30. 39
      src/util/cookie.js
  31. 26
      src/util/file.js
  32. 76
      src/util/request.js
  33. 12
      src/util/utils.js
  34. 93
      src/util/validator.js
  35. 75
      src/views/Home.vue
  36. 78
      src/views/Write.vue
  37. 9
      src/views/error/404.vue
  38. 58
      vite.config.js

7
.gitignore vendored

@ -0,0 +1,7 @@
# 编辑器
.vscode
.idea
node_modules
.history

6
README.md

@ -0,0 +1,6 @@
# 局长信箱 群众端(PC)
## 技术栈
- Vue 3 + Vite + (Element Plus)[https://element-plus.gitee.io/zh-CN/component/form.html]
- sass
- pinia

17
index.html

@ -0,0 +1,17 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/*" href="/favicon.png" />
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>局长信箱 即接即办</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
<!-- 科大讯飞语音 -->
<script src="/lib/index.umd.js"></script>
<script src="/lib/processor.worker.js"></script>
</body>
</html>

26
package.json

@ -0,0 +1,26 @@
{
"name": "mailbox-outer-pc-vue",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"crypto-js": "^4.2.0",
"pinia": "^2.1.7",
"element-plus": "^2.2.9",
"vue": "^3.3.11",
"vue-router": "^4.2.5"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.5.2",
"sass": "^1.69.7",
"unplugin-auto-import": "^0.17.3",
"unplugin-vue-components": "^0.26.0",
"vite": "^5.0.8",
"vite-svg-loader": "^5.1.0"
}
}

BIN
public/favicon.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

BIN
public/imgs/bg_web.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 755 KiB

BIN
public/imgs/search.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

BIN
public/imgs/write.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

3
public/lib/index.umd.js

@ -0,0 +1,3 @@
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).RecorderManager=t()}(this,(function(){"use strict";function e(e,t,r,o){return new(r||(r=Promise))((function(n,a){function i(e){try{u(o.next(e))}catch(e){a(e)}}function s(e){try{u(o.throw(e))}catch(e){a(e)}}function u(e){var t;e.done?n(e.value):(t=e.value,t instanceof r?t:new r((function(e){e(t)}))).then(i,s)}u((o=o.apply(e,t||[])).next())}))}function t(e,t){var r,o,n,a,i={label:0,sent:function(){if(1&n[0])throw n[1];return n[1]},trys:[],ops:[]};return a={next:s(0),throw:s(1),return:s(2)},"function"==typeof Symbol&&(a[Symbol.iterator]=function(){return this}),a;function s(s){return function(u){return function(s){if(r)throw new TypeError("Generator is already executing.");for(;a&&(a=0,s[0]&&(i=0)),i;)try{if(r=1,o&&(n=2&s[0]?o.return:s[0]?o.throw||((n=o.return)&&n.call(o),0):o.next)&&!(n=n.call(o,s[1])).done)return n;switch(o=0,n&&(s=[2&s[0],n.value]),s[0]){case 0:case 1:n=s;break;case 4:return i.label++,{value:s[1],done:!1};case 5:i.label++,o=s[1],s=[0];continue;case 7:s=i.ops.pop(),i.trys.pop();continue;default:if(!(n=i.trys,(n=n.length>0&&n[n.length-1])||6!==s[0]&&2!==s[0])){i=0;continue}if(3===s[0]&&(!n||s[1]>n[0]&&s[1]<n[3])){i.label=s[1];break}if(6===s[0]&&i.label<n[1]){i.label=n[1],n=s;break}if(n&&i.label<n[2]){i.label=n[2],i.ops.push(s);break}n[2]&&i.ops.pop(),i.trys.pop();continue}s=t.call(e,i)}catch(e){s=[6,e],o=0}finally{r=n=0}if(5&s[0])throw s[1];return{value:s[0]?s[1]:void 0,done:!0}}([s,u])}}}function r(){var e,t=navigator,r=t.getUserMedia||t.webkitGetUserMedia||t.mozGetUserMedia;return(null===(e=t.mediaDevices)||void 0===e?void 0:e.getUserMedia)?t.mediaDevices.getUserMedia({audio:!0,video:!1}):r?new Promise((function(e,t){r.call(navigator,{audio:!0,video:!1},(function(t){e(t)}),(function(e){t(e)}))})):Promise.reject(new Error("不支持录音"))}var o;function n(r,n){return e(this,void 0,void 0,(function(){var e;return t(this,(function(t){switch(t.label){case 0:return[3,2];case 1:return t.sent(),[2,new AudioWorkletNode(r,"processor-worklet")];case 2:return(e=o)?[3,4]:[4,new Worker("".concat(n,"/processor.worker.js"))];case 3:e=t.sent(),t.label=4;case 4:return[2,{port:o=e}]}}))}))}return function(){function o(e){this.processorPath=e,this.audioBuffers=[]}return o.prototype.start=function(o){var a,i=o.sampleRate,s=o.frameSize,u=o.arrayBufferType;return e(this,void 0,void 0,(function(){var e,o,c,l,f,d,p;return t(this,(function(t){switch(t.label){case 0:return t.trys.push([0,3,,4]),(e=this).audioBuffers=[],[4,r()];case 1:return o=t.sent(),this.audioTracks=o.getAudioTracks(),c=function(e,t){var r;try{(r=new(window.AudioContext||window.webkitAudioContext)({sampleRate:t})).createMediaStreamSource(e)}catch(t){null==r||r.close(),(r=new(window.AudioContext||window.webkitAudioContext)).createMediaStreamSource(e)}return r}(o,i),this.audioContext=c,l=c.createMediaStreamSource(o),[4,n(c,this.processorPath)];case 2:return f=t.sent(),this.audioWorklet=f,f.port.postMessage({type:"init",data:{frameSize:s,toSampleRate:i||c.sampleRate,fromSampleRate:c.sampleRate,arrayBufferType:u||"short16"}}),f.port.onmessage=function(t){var r=t.data,o=r.frameBuffer,n=r.isLastFrame;if(s&&e.onFrameRecorded)if(null==o?void 0:o.byteLength)for(var a=0;a<o.byteLength;)e.onFrameRecorded({isLastFrame:n&&a+s>=o.byteLength,frameBuffer:t.data.frameBuffer.slice(a,a+s)}),a+=s;else e.onFrameRecorded(t.data);e.onStop&&(o&&e.audioBuffers.push(o),n&&e.onStop(e.audioBuffers))},(d=c.createScriptProcessor(0,1,1)).onaudioprocess=function(e){f.port.postMessage({type:"message",data:e.inputBuffer.getChannelData(0)})},l.connect(d),d.connect(c.destination),c.resume(),null===(a=this.onStart)||void 0===a||a.call(this),[3,4];case 3:return p=t.sent(),console.error(p),[3,4];case 4:return[2]}}))}))},o.prototype.stop=function(){var e,t,r,o;null===(e=this.audioWorklet)||void 0===e||e.port.postMessage({type:"stop"}),null===(t=this.audioTracks)||void 0===t||t[0].stop(),"running"===(null===(r=this.audioContext)||void 0===r?void 0:r.state)&&(null===(o=this.audioContext)||void 0===o||o.close())},o}()}));

1
public/lib/processor.worker.js

@ -0,0 +1 @@
!function(){"use strict";function t(t){return function(t){if(Array.isArray(t))return e(t)}(t)||function(t){if("undefined"!=typeof Symbol&&null!=t[Symbol.iterator]||null!=t["@@iterator"])return Array.from(t)}(t)||function(t,r){if(!t)return;if("string"==typeof t)return e(t,r);var i=Object.prototype.toString.call(t).slice(8,-1);"Object"===i&&t.constructor&&(i=t.constructor.name);if("Map"===i||"Set"===i)return Array.from(t);if("Arguments"===i||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(i))return e(t,r)}(t)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function e(t,e){(null==e||e>t.length)&&(e=t.length);for(var r=0,i=new Array(e);r<e;r++)i[r]=t[r];return i}function r(t,e,r,i){this.fromSampleRate=t,this.toSampleRate=e,this.channels=0|r,this.noReturn=!!i,this.initialize()}r.prototype.initialize=function(){if(!(this.fromSampleRate>0&&this.toSampleRate>0&&this.channels>0))throw new Error("Invalid settings specified for the resampler.");this.fromSampleRate==this.toSampleRate?(this.resampler=this.bypassResampler,this.ratioWeight=1):(this.fromSampleRate<this.toSampleRate?(this.lastWeight=1,this.resampler=this.compileLinearInterpolation):(this.tailExists=!1,this.lastWeight=0,this.resampler=this.compileMultiTap),this.ratioWeight=this.fromSampleRate/this.toSampleRate)},r.prototype.compileLinearInterpolation=function(t){var e=t.length;this.initializeBuffers(e);var r,i,s=this.outputBufferSize,a=this.ratioWeight,f=this.lastWeight,n=0,o=0,h=0,l=this.outputBuffer;if(e%this.channels==0){if(e>0){for(;f<1;f+=a)for(n=1-(o=f%1),r=0;r<this.channels;++r)l[h++]=this.lastOutput[r]*n+t[r]*o;for(f--,e-=this.channels,i=Math.floor(f)*this.channels;h<s&&i<e;){for(n=1-(o=f%1),r=0;r<this.channels;++r)l[h++]=t[i+r]*n+t[i+this.channels+r]*o;f+=a,i=Math.floor(f)*this.channels}for(r=0;r<this.channels;++r)this.lastOutput[r]=t[i++];return this.lastWeight=f%1,this.bufferSlice(h)}return this.noReturn?0:[]}throw new Error("Buffer was of incorrect sample length.")},r.prototype.compileMultiTap=function(t){var e=[],r=t.length;this.initializeBuffers(r);var i=this.outputBufferSize;if(r%this.channels==0){if(r>0){for(var s=this.ratioWeight,a=0,f=0;f<this.channels;++f)e[f]=0;var n=0,o=0,h=!this.tailExists;this.tailExists=!1;var l=this.outputBuffer,u=0,p=0;do{if(h)for(a=s,f=0;f<this.channels;++f)e[f]=0;else{for(a=this.lastWeight,f=0;f<this.channels;++f)e[f]+=this.lastOutput[f];h=!0}for(;a>0&&n<r;){if(!(a>=(o=1+n-p))){for(f=0;f<this.channels;++f)e[f]+=t[n+f]*a;p+=a,a=0;break}for(f=0;f<this.channels;++f)e[f]+=t[n++]*o;p=n,a-=o}if(0!=a){for(this.lastWeight=a,f=0;f<this.channels;++f)this.lastOutput[f]=e[f];this.tailExists=!0;break}for(f=0;f<this.channels;++f)l[u++]=e[f]/s}while(n<r&&u<i);return this.bufferSlice(u)}return this.noReturn?0:[]}throw new Error("Buffer was of incorrect sample length.")},r.prototype.bypassResampler=function(t){return this.noReturn?(this.outputBuffer=t,t.length):t},r.prototype.bufferSlice=function(t){if(this.noReturn)return t;try{return this.outputBuffer.subarray(0,t)}catch(e){try{return this.outputBuffer.length=t,this.outputBuffer}catch(e){return this.outputBuffer.slice(0,t)}}},r.prototype.initializeBuffers=function(t){this.outputBufferSize=Math.ceil(t*this.toSampleRate/this.fromSampleRate);try{this.outputBuffer=new Float32Array(this.outputBufferSize),this.lastOutput=new Float32Array(this.channels)}catch(t){this.outputBuffer=[],this.lastOutput=[]}},self.transData=function(t){return"short16"===self.arrayBufferType&&(t=function(t){for(var e=new ArrayBuffer(2*t.length),r=new DataView(e),i=0,s=0;s<t.length;s+=1,i+=2){var a=Math.max(-1,Math.min(1,t[s]));r.setInt16(i,a<0?32768*a:32767*a,!0)}return r.buffer}(t=self.resampler.resampler(t))),t},self.onmessage=function(e){var i=e.data,s=i.type,a=i.data;if("init"===s){var f=a.frameSize,n=a.toSampleRate,o=a.fromSampleRate,h=a.arrayBufferType;return self.frameSize=f*Math.floor(o/n),self.resampler=new r(o,n,1),self.frameBuffer=[],void(self.arrayBufferType=h)}if("stop"===s&&(self.postMessage({frameBuffer:self.transData(self.frameBuffer),isLastFrame:!0}),self.frameBuffer=[]),"message"===s){var l,u=a;if(self.frameSize)return(l=self.frameBuffer).push.apply(l,t(u)),self.frameBuffer.length>=self.frameSize&&(self.postMessage({frameBuffer:self.transData(this.frameBuffer),isLastFrame:!1}),self.frameBuffer=[]),!0;u&&self.postMessage({frameBuffer:self.transData(u),isLastFrame:!1})}}}();

BIN
public/logo.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

3
src/App.vue

@ -0,0 +1,3 @@
<template>
<router-view />
</template>

12
src/api/auth.js

@ -0,0 +1,12 @@
import { get, post } from '@/util/request'
// 授权登录
export function authOpenid(openid) {
return post('/auth/openid?openid=' + openid)
}
// 获取用户信息
export function userInfo() {
return get('/auth/user')
}

21
src/api/mail.js

@ -0,0 +1,21 @@
import { get, post, put } from '@/util/request'
// 新增信件
export function addMail(data) {
return post('/mail', data)
}
// 信件列表 分页
export function listMail(params) {
return get(`/mail?size=${params.size}&current=${params.current}`)
}
// 获取信件详情
export function getMail(id) {
return get('/mail/' + id)
}
// 信件评论
export function updateEvaluate(data) {
return put('/mail/evaluate', data)
}

21
src/api/mailDraft.js

@ -0,0 +1,21 @@
import { get, post, del } from '@/util/request'
// 保存草稿
export function saveDraft(data) {
return post('/mail/draft', data)
}
// 获取草稿详情
export function getDraft(id) {
return get('/mail/draft/' + id)
}
// 信件草稿列表 分页
export function listDraft(params) {
return get(`/mail/draft?size=${params.size}&current=${params.current}`)
}
// 删除信件
export function delDraft(id) {
return del('/mail/draft/' + id)
}

5
src/api/sms.js

@ -0,0 +1,5 @@
import { post } from '@/util/request'
export function send(phone) {
return post('/sms/send?phone=' + phone)
}

212
src/assets/style/style.scss

@ -0,0 +1,212 @@
body {
font-size: 14px;
font-family: PingFang-SC-Heavy;
margin: 0;
--header-height: 8.377vh;
background-color: var(--background-color);
}
#app {
max-width: 1200px;
margin: auto;
}
p {
margin: 0.5em 0;
}
img {
max-width: 100%;
}
svg {
width: 1em;
}
svg+span {
margin-left: .5em;
}
.none {
display: none;
}
.flex {
display: flex;
}
.flex-inline {
display: inline-flex;
}
.flex.v-center,
.flex-inline.v-center {
align-items: center;
}
.flex.center,
.flex-inline.center {
justify-content: center;
}
.flex.between,
.flex-inline.between {
justify-content: space-between;
}
.flex.end,
.flex-inline.end {
justify-content: flex-end;
}
.flex.wrap,
.flex-inline.wrap {
flex-wrap: wrap;
}
.flex.max-content,
.flex-inline.max-content {
width: max-content;
}
.flex.column, .flex-inline.column {
flex-direction: column;
}
.flex.gap,
.flex-inline.gap {
gap: 0 8px;
}
.flex.gap-10 {
gap: 0 10px;
}
.flex.gap-16 {
gap: 0 16px;
}
.text-center {
text-align: center;
}
.text-nowrap {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.text-wrap {
white-space: pre-wrap;
}
.container {
padding: 18px 36px;
}
.pointer:hover {
cursor: pointer;
}
.relative {
position: relative;
}
.ml-4 {
margin-left: 4px;
}
.ml-8 {
margin-left: 8px;
}
.ml-10 {
margin-left: 10px;
}
.mr-4 {
margin-right: 4px;
}
.mr-8 {
margin-right: 8px;
}
.mr-10 {
margin-right: 10px;
}
.mr-20 {
margin-right: 20px;
}
.mt-8 {
margin-top: 8px;
}
.mt-10 {
margin-top: 10px;
}
.mt-20 {
margin-top: 20px;
}
.mb-8 {
margin-bottom: 8px;
}
.mb-10 {
margin-bottom: 10px;
}
.mb-20 {
margin-bottom: 20px;
}
.mb-40 {
margin-bottom: 40px;
}
.card {
background-color: #fff;
margin-bottom: 10px;
padding-top: 6px;
h2 {
color: #666;
font-size: 12px;
font-weight: normal;
margin: var(--van-cell-group-inset-padding);
padding: var(--van-cell-vertical-padding) 0;
}
header {
margin: var(--van-cell-group-inset-padding);
margin-top: 16px;
font-size: 17px;
font-weight: bold;
}
.content {
box-shadow: inset 0px -1px 0px 0px rgba(227, 227, 227, 1);
color: #666;
margin-top: 10px;
padding: var(--van-cell-group-inset-padding);
padding-bottom: 30px;
font-size: 12px;
font-weight: 400;
}
footer {
box-shadow: inset 0px -1px 0px 0px rgba(227, 227, 227, 1);
padding: var(--van-cell-group-inset-padding);
}
}
.wrapper {
height: 100vh;
background-color: var(--background-color);
overflow: auto;
}

8
src/assets/style/theme.scss

@ -0,0 +1,8 @@
// @/styles/element/index.scss
@forward "element-plus/theme-chalk/src/common/var.scss" with (
$colors: (
"primary": (
"base": #162582,
)
)
);

29
src/components/Icon.vue

@ -0,0 +1,29 @@
<template>
<div :style="{ fontSize: size ? `${size}px` : '' }">
<IconSvg />
</div>
</template>
<script setup>
const props = defineProps({
name: {
type: String,
require: true,
},
size: {
type: Number,
},
});
let IconSvg = ref(h("template"));
import(`@/assets/icons/${props.name}.svg`).then((data) => {
IconSvg.value = data.render();
});
</script>
<style lang="scss" scoped>
:deep() {
svg {
display: block;
}
}
</style>

44
src/components/ImgPreview.vue

@ -0,0 +1,44 @@
<template>
<div v-if="show" @click="hide" class="img-preview-container flex v-center center">
<img :src="filepath" alt="" />
</div>
</template>
<script setup>
import { watch } from "vue"
const props = defineProps({
filepath: {
type: String,
default: ''
},
show: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['update:show'])
const show = ref(false)
watch(() => props.show, (val) => {
show.value = val
})
function hide() {
emit('update:show', false)
}
</script>
<style lang="scss" scoped>
.img-preview-container {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #000;
z-index: 2;
img {
max-width: 100%;
}
}
</style>

74
src/components/Loading.vue

@ -0,0 +1,74 @@
<template>
<div class="loading-wrapper flex v-center center" v-if="loading">
<div class="loading flex center v-center column">
<div class="loader-box" v-if="icon === 'loading'">
<div class="loader"></div>
</div>
<div v-else-if="icon === 'img'" class="mb-10">
<Icon name="img" :size="84" />
</div>
<div>{{ message }}</div>
</div>
</div>
</template>
<script setup>
defineProps({
loading: {
type: Boolean,
default: true
},
message: {
type: String,
default: '加载中...'
},
icon : {
type: String,
default: 'loading'
}
})
</script>
<style lang="scss" scoped>
.loading-wrapper {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 999;
.loading {
background: rgba(0,0,0,0.6);
width: 61vw;
max-width: 600px;
height: 30vh;
border-radius: 6px;
color: #fff;
.loader-box {
width: 32px;
height: 32px;
transform: translate(50%, 50%);
margin-bottom: 28px;
}
}
}
/* HTML: <div class="loader"></div> */
.loader {
--d:22px;
width: 4px;
height: 4px;
border-radius: 50%;
color: #fff;
box-shadow:
calc(1*var(--d)) calc(0*var(--d)) 0 0,
calc(0.707*var(--d)) calc(0.707*var(--d)) 0 1px,
calc(0*var(--d)) calc(1*var(--d)) 0 2px,
calc(-0.707*var(--d)) calc(0.707*var(--d)) 0 3px,
calc(-1*var(--d)) calc(0*var(--d)) 0 4px,
calc(-0.707*var(--d)) calc(-0.707*var(--d))0 5px,
calc(0*var(--d)) calc(-1*var(--d)) 0 6px;
animation: l27 1s infinite steps(8);
}
@keyframes l27 {
100% {transform: rotate(1turn)}
}
</style>

14
src/layout/Index.vue

@ -0,0 +1,14 @@
<template>
<div class="wrapper">
<header>
<img src="/imgs/bg_web.png" alt="" />
</header>
<main>
<router-view />
</main>
</div>
</template>
<script setup>
</script>
<style lang="scss" scoped>
</style>

18
src/main.js

@ -0,0 +1,18 @@
import { createApp } from 'vue'
import router from './router/index'
import { createPinia } from 'pinia'
import App from './App.vue'
import IconComponent from '@/components/Icon.vue'
import LoadingComponent from '@/components/Loading.vue'
import './permission' // 权限控制
import './assets/style/style.scss'
const app = createApp(App)
.use(router)
.use(createPinia())
.component('Icon', IconComponent)
.component('Loading', LoadingComponent);
app.mount('#app')

51
src/permission.js

@ -0,0 +1,51 @@
import router from './router'
import UserStore from "@/store/user"
import { getToken } from '@/util/cookie'
// 白名单
const whiteList = ['/', '/mail/write'];
router.beforeEach((to, from, next) => {
const whiteFlag = whiteList.indexOf(to.path) !== -1;
const userStore = UserStore();
const token = getToken();
// 没有token && 并且不是白名单中的路由
if (whiteFlag) {
next();
return
}
if (!token) {
next('/');
return
}
// 已有用户信息
if (userStore.user.id) {
// 判断用户是否进行了人脸认证
if (userStore.user.idCard || to.path === '/realName/auth') {
// 判断是否是从信件页面回退到人脸认证界面的,如果是则返回首页
if (to.path === '/realName/auth' && from.path === '/mail') {
next('/');
return
}
next();
return
}
next('/realName/auth');
return
}
// 获取用户信息
userStore.getUser().then(() => {
// 判断用户是否进行了人脸认证
if (userStore.user.idCard || to.path === '/realName/auth') {
// 判断是否是从信件页面回退到人脸认证界面的,如果是则返回首页
if (to.path === '/realName/auth' && from.path === '/mail') {
next('/');
return
}
next();
return
}
next('/realName/auth');
}).catch((res) => {
next('/')
})
})

39
src/router/index.js

@ -0,0 +1,39 @@
import { createRouter, createWebHistory } from 'vue-router'
import Layout from '@/layout/Index.vue'
import Home from '@/views/Home.vue'
import Write from '@/views/Write.vue'
import NotFound from '@/views/error/404.vue'
const routes = [
{
path: '/layout',
component: Layout,
children: [
{
path: '/',
component: Home,
},
{
path: '/mail/write',
component: Write,
}
]
},
{
path: '/:catchAll(.*)',
name: 'not-found',
component: NotFound,
meta: {
title: '404'
}
}
];
const router = createRouter({
history: createWebHistory(),
routes
});
export default router;

51
src/store/dept.js

@ -0,0 +1,51 @@
export const depts = [
{
text: "芙蓉分局",
value: 6,
},
{
text: "天心分局",
value: 29,
},
{
text: "岳麓分局",
value: 34,
},
{
text: "开福分局",
value: 20,
},
{
text: "雨花分局",
value: 33,
},
{
text: "高新分局",
value: 7,
},
{
text: "望城分局",
value: 31,
},
{
text: "长沙县局",
value: 35,
},
{
text: "浏阳市局",
value: 22,
},
{
text: "宁乡市局",
value: 24,
},
{
text: "交警支队",
value: 14,
},
{
text: "其他单位",
value: 0,
},
]

15
src/store/page.js

@ -0,0 +1,15 @@
import { defineStore } from 'pinia'
const PageStore = defineStore(
'page',
{
state: () => ({
mailTabActive: '',
mailDraftId: '',
myMailRefresh: false,
myMailDraftRefresh: false
})
}
)
export default PageStore

28
src/store/user.js

@ -0,0 +1,28 @@
import { defineStore } from 'pinia'
import { userInfo } from "@/api/auth";
import { delToken } from '@/util/cookie'
const UserStore = defineStore(
'user',
{
state: () => ({
user: {}
}),
actions: {
getUser() {
return new Promise((resolve, reject) => {
userInfo().then((data) => {
this.user = data;
resolve(data);
}).catch(res => {
delToken()
reject(res)
})
})
}
}
}
)
export default UserStore

169
src/util/audio.js

@ -0,0 +1,169 @@
import CryptoJS from 'crypto-js';
export function startRecorder(stopCallback, speechText) {
console.log("开始录音");
const recorder = new RecorderManager("lib");
let iatWS;
connectWebSocket();
recorder.onStart = () => {
// 开始
};
let resultText = "";
let resultTextTemp = "";
let result = "";
let flag = false;
recorder.onFrameRecorded = ({ isLastFrame, frameBuffer }) => {
if (iatWS.readyState === iatWS.OPEN) {
iatWS.send(
JSON.stringify({
data: {
status: isLastFrame ? 2 : 1,
format: "audio/L16;rate=16000",
encoding: "raw",
audio: toBase64(frameBuffer),
},
})
);
if (isLastFrame) {
}
}
};
recorder.onStop = () => {
console.log('onStop')
flag = true
};
function renderResult(resultData) {
// 识别结束
let jsonData = JSON.parse(resultData);
console.log('jsonData', jsonData)
if (jsonData.data && jsonData.data.result) {
let data = jsonData.data.result;
let str = "";
let ws = data.ws;
for (let i = 0; i < ws.length; i++) {
str = str + ws[i].cw[0].w;
console.log('str', str)
speechText.value = str
}
// 开启wpgs会有此字段(前提:在控制台开通动态修正功能)
// 取值为 "apd"时表示该片结果是追加到前面的最终结果;取值为"rpl" 时表示替换前面的部分结果,替换范围为rg字段
if (data.pgs) {
if (data.pgs === "apd") {
// 将resultTextTemp同步给resultText
resultText = resultTextTemp;
console.log('resultText', resultText)
}
// 将结果存储在resultTextTemp中
resultTextTemp = resultText + str;
} else {
resultText = resultText + str;
}
result += resultTextTemp || resultText || ""
resultTextTemp = ""
resultText = "";
console.log('result', result)
speechText.value = result
if (flag) {
// 回调
stopCallback(result)
console.log("录音结束");
}
}
// if (jsonData.code === 0 && jsonData.data.status === 2) {
// iatWS.close();
// }
// if (jsonData.code !== 0) {
// iatWS.close();
// console.error(jsonData);
// }
}
function connectWebSocket() {
const websocketUrl = getWebSocketUrl();
if ("WebSocket" in window) {
iatWS = new WebSocket(websocketUrl);
} else if ("MozWebSocket" in window) {
iatWS = new MozWebSocket(websocketUrl);
} else {
console.error("浏览器不支持WebSocket");
return;
}
iatWS.onopen = (e) => {
// 开始录音
recorder.start({
sampleRate: 16000,
frameSize: 1280,
});
var params = {
common: {
app_id: "3e5c642a",
},
business: {
language: "zh_cn",
domain: "iat",
accent: "mandarin",
vad_eos: 5000,
dwa: "wpgs",
},
data: {
status: 0,
format: "audio/L16;rate=16000",
encoding: "raw",
},
};
iatWS.send(JSON.stringify(params));
};
iatWS.onmessage = (e) => {
console.log('ws onmessage')
renderResult(e.data);
};
iatWS.onerror = (e) => {
console.log('ws 错误')
recorder.stop();
};
iatWS.onclose = (e) => {
console.log('ws 关闭')
};
}
/**
* 获取websocket url
* 该接口需要后端提供这里为了方便前端处理
*/
function getWebSocketUrl() {
// 请求地址根据语种不同变化
var url = "wss://iat-api.xfyun.cn/v2/iat";
var host = "iat-api.xfyun.cn";
var apiKey = "b4d5965f32492c53e74740f5056d9a21";
var apiSecret = "Nzc5ZDIzM2QxZGMxYWFhODk0NWIxZjg4";
var date = new Date().toGMTString();
var algorithm = "hmac-sha256";
var headers = "host date request-line";
var signatureOrigin = `host: ${host}\ndate: ${date}\nGET /v2/iat HTTP/1.1`;
var signatureSha = CryptoJS.HmacSHA256(signatureOrigin, apiSecret);
var signature = CryptoJS.enc.Base64.stringify(signatureSha);
var authorizationOrigin = `api_key="${apiKey}", algorithm="${algorithm}", headers="${headers}", signature="${signature}"`;
var authorization = btoa(authorizationOrigin);
url = `${url}?authorization=${authorization}&date=${date}&host=${host}`;
return url;
}
function toBase64(buffer) {
var binary = "";
var bytes = new Uint8Array(buffer);
var len = bytes.byteLength;
for (var i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
return window.btoa(binary);
}
return recorder;
}

39
src/util/cookie.js

@ -0,0 +1,39 @@
function setCookieByHour(cookieName, cookieValue, expirationDays) {
var d = new Date();
d.setTime(d.getTime() + (expirationDays * 60 * 60 * 1000));
var expires = "expires=" + d.toUTCString();
document.cookie = cookieName + "=" + cookieValue + ";" + expires + ";path=/";
}
export function getCookie(cookieName) {
var name = cookieName + "=";
var decodedCookie = decodeURIComponent(document.cookie);
var cookieArray = decodedCookie.split(';');
for (var i = 0; i < cookieArray.length; i++) {
var cookie = cookieArray[i].trim();
if (cookie.indexOf(name) === 0) {
return cookie.substring(name.length, cookie.length);
}
}
return "";
}
function deleteCookie(cookieName) {
// 设置cookie的过期时间为过去的日期
document.cookie = cookieName + "=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
}
const TOKEN_COOKIE_NAME = "token";
export function setToken(key) {
setCookieByHour(TOKEN_COOKIE_NAME, key, 1)
}
export function getToken() {
return getCookie(TOKEN_COOKIE_NAME)
}
export function delToken() {
return deleteCookie(TOKEN_COOKIE_NAME)
}

26
src/util/file.js

@ -0,0 +1,26 @@
export function getFileType(originFilename) {
const suffix =
originFilename.indexOf(".") > -1
? originFilename
.substring(originFilename.lastIndexOf(".") + 1)
.toLocaleLowerCase()
: "";
let filetype = "";
if (
suffix === "jpg" ||
suffix === "png" ||
suffix === "jpeg" ||
suffix === "bmp" ||
suffix === "gif"
) {
filetype = "img";
} else if (suffix === "mp3" || suffix === "wav") {
filetype = "mp3";
} else if (suffix === "mp4") {
filetype = "mp4";
} else {
filetype = "file";
}
return filetype;
}

76
src/util/request.js

@ -0,0 +1,76 @@
const basePath = '/api'
import { getToken } from '@/util/cookie'
export function get(url) {
return request(url, {
method: 'GET',
headers: {
"Content-Type": 'application/json',
"Authorization": getToken()
}
})
}
export function post(url, data) {
return request(url, {
method: 'POST',
body: JSON.stringify(data),
headers: {
"Content-Type": 'application/json',
"Authorization": getToken()
}
})
}
export function put(url, data) {
return request(url, {
method: 'PUT',
body: JSON.stringify(data),
headers: {
"Content-Type": 'application/json',
"Authorization": getToken()
}
})
}
export function del(url) {
return request(url, {
method: 'DELETE',
headers: {
"Content-Type": 'application/json',
"Authorization": getToken()
}
})
}
export function upload(data) {
return post('/file/upload/base64', data)
}
function request(url, options) {
return new Promise((resolve, reject) => {
fetch(`${basePath}${url}`, {
method: options.method,
body: options.body,
headers: options.headers
}).then(response => {
if (response.status === 413) {
return;
}
return response.json();
}).then(res => {
if (res.code === 200) {
resolve(res.data)
} else {
let message = res.msg;
if (res.code === 401) {
message = "未授权登陆"
} else {
}
reject(res)
}
})
})
}

12
src/util/utils.js

@ -0,0 +1,12 @@
export function getSex(idCard) {
let res = /^(\d{6})(\d{4})(\d{2})(\d{2})(\d{3})([0-9]|X)$/;
if (idCard && res.test(idCard)) {
let genderCode = idCard.charAt(16);
if (parseInt(genderCode) % 2 == 0) {
return 'F';
}
return 'M';
}
return '';
}

93
src/util/validator.js

@ -0,0 +1,93 @@
/**
* 验证身份证号码
* @param { String } code 身份证号码
*/
export function validatorIdCard(code) {
// 身份证号前两位代表区域
const city = {
11: '北京',
12: '天津',
13: '河北',
14: '山西',
15: '内蒙古',
21: '辽宁',
22: '吉林',
23: '黑龙江 ',
31: '上海',
32: '江苏',
33: '浙江',
34: '安徽',
35: '福建',
36: '江西',
37: '山东',
41: '河南',
42: '湖北 ',
43: '湖南',
44: '广东',
45: '广西',
46: '海南',
50: '重庆',
51: '四川',
52: '贵州',
53: '云南',
54: '西藏 ',
61: '陕西',
62: '甘肃',
63: '青海',
64: '宁夏',
65: '新疆',
71: '台湾',
81: '香港',
82: '澳门',
91: '国外 ',
};
const idCardReg = /^[1-9]\d{5}(19|20)?\d{2}(0[1-9]|1[012])(0[1-9]|[12]\d|3[01])\d{3}(\d|X)$/i; // 身份证格式正则表达式
// 如果身份证不满足格式正则表达式
if (!code) {
return '请输入身份证号码'
}
if (!code.match(idCardReg)) {
return '请输入正确的身份证号码';
}
if (!city[code.substr(0, 2)]) {
// 区域数组中不包含需验证的身份证前两位
return '请输入正确的身份证号码';
}
if (code.length === 18) {
// 18位身份证需要验证最后一位校验位
code = code.split('');
// ∑(ai×Wi)(mod 11)
// 加权因子
const factor = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
// 校验位
const parity = [1, 0, 'X', 9, 8, 7, 6, 5, 4, 3, 2];
let sum = 0;
let ai = 0;
let wi = 0;
for (let i = 0; i < 17; i++) {
ai = parseInt(code[i]);
wi = factor[i];
sum += ai * wi; // 开始计算并相加
}
const last = parity[sum % 11]; // 求余
if (last.toString() !== code[17]) {
return '请输入正确的身份证号码';
}
}
return true
}
/**
* 校验手机号
* @param {*} phonenumber
* @returns
*/
export function validatorPhone(phonenumber) {
if (!phonenumber) {
return '请输入手机号码'
}
if (!/^1[3456789]\d{9}/.test(phonenumber)) {
return '请输入手机号码'
}
return true
}

75
src/views/Home.vue

@ -0,0 +1,75 @@
<template>
<div class="flex gap-10 center mt-20">
<a
class="flex v-center center gap-16"
@click="router.push('/mail/write')"
>
<div style="width: 48px">
<img src="/imgs/write.png" alt="" />
</div>
<span>我要写信</span>
</a>
<a
class="flex v-center center gap-16"
@click="router.push('/mail?active=my')"
>
<div style="width: 48px">
<img src="/imgs/search.png" alt="" />
</div>
<span>回复查询</span>
</a>
</div>
<Loading :loading="loading" />
</template>
<script setup>
import { useRoute, useRouter } from "vue-router";
import { authOpenid } from "@/api/auth";
import UserStore from "@/store/user";
import { setToken, getToken } from "@/util/cookie";
const router = useRouter();
const route = useRoute();
const userStore = UserStore();
const loading = ref(false);
if (!getToken()) {
if (route.query.openid) {
//
authOpenid(route.query.openid).then((data) => {
setToken(data.token);
userStore.user = data.user;
loading.value = false;
wxStore.initSign();
});
}
} else {
loading.value = false;
wxStore.initSign();
}
</script>
<style lang="scss" scoped>
.wrapper {
background-color: #fff;
}
header {
img {
width: 100%;
}
}
a {
width: 45.2%;
height: 84px;
text-decoration: none;
border: 1px solid var(--primary-color);
color: var(--primary-color);
font-weight: bold;
font-size: 18px;
&:hover {
cursor: pointer;
}
}
</style>

78
src/views/Write.vue

@ -0,0 +1,78 @@
<template>
<el-form :model="form" label-width="120px">
<el-form-item label="Activity name">
<el-input v-model="form.name" />
</el-form-item>
<el-form-item label="Activity zone">
<el-select v-model="form.region" placeholder="please select your zone">
<el-option label="Zone one" value="shanghai" />
<el-option label="Zone two" value="beijing" />
</el-select>
</el-form-item>
<el-form-item label="Activity time">
<el-col :span="11">
<el-date-picker
v-model="form.date1"
type="date"
placeholder="Pick a date"
style="width: 100%"
/>
</el-col>
<el-col :span="2" class="text-center">
<span class="text-gray-500">-</span>
</el-col>
<el-col :span="11">
<el-time-picker
v-model="form.date2"
placeholder="Pick a time"
style="width: 100%"
/>
</el-col>
</el-form-item>
<el-form-item label="Instant delivery">
<el-switch v-model="form.delivery" />
</el-form-item>
<el-form-item label="Activity type">
<el-checkbox-group v-model="form.type">
<el-checkbox label="Online activities" name="type" />
<el-checkbox label="Promotion activities" name="type" />
<el-checkbox label="Offline activities" name="type" />
<el-checkbox label="Simple brand exposure" name="type" />
</el-checkbox-group>
</el-form-item>
<el-form-item label="Resources">
<el-radio-group v-model="form.resource">
<el-radio label="Sponsor" />
<el-radio label="Venue" />
</el-radio-group>
</el-form-item>
<el-form-item label="Activity form">
<el-input v-model="form.desc" type="textarea" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSubmit">Create</el-button>
<el-button>Cancel</el-button>
</el-form-item>
</el-form>
</template>
<script lang="ts" setup>
import { reactive } from 'vue'
// do not use same name with ref
const form = reactive({
name: '',
region: '',
date1: '',
date2: '',
delivery: false,
type: [],
resource: '',
desc: '',
})
const onSubmit = () => {
console.log('submit!')
}
</script>

9
src/views/error/404.vue

@ -0,0 +1,9 @@
<template>
<h1 class="text-center">404</h1>
</template>
<script setup>
</script>
<style lang="scss" scoped>
</style>

58
vite.config.js

@ -0,0 +1,58 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite'
import svgLoader from 'vite-svg-loader'
import path from 'path'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
svgLoader(),
AutoImport({
imports: [
'vue', 'vue-router'
],
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver({
importStyle: "sass"
})],
}),
],
resolve: {
// https://cn.vitejs.dev/config/#resolve-alias
alias: {
// 设置路径
'~': path.resolve(__dirname, './'),
// 设置别名
'@': path.resolve(__dirname, './src/')
},
// https://cn.vitejs.dev/config/#resolve-extensions
extensions: ['.js']
},
css: {
preprocessorOptions: {
scss: {
additionalData: `@use "src/assets/style/theme.scss" as *;`
},
},
},
server: {
host: '0.0.0.0',
port: 5174,
proxy: {
'/api': {
// https://mailbox.biutag.com/api
// http://127.0.0.1:8080
target: 'https://mailbox.biutag.com/api',
changeOrigin: true,
rewrite: (p) => p.replace(/^\/api/, '')
}
}
}
})
Loading…
Cancel
Save