boyuehasfj-vue3-html

This commit is contained in:
luoyu 2025-06-02 21:36:36 +08:00
parent 320265cc07
commit 2bf74587de
17 changed files with 3127 additions and 796 deletions

BIN
.env

Binary file not shown.

6
env.d.ts vendored
View File

@ -1 +1,7 @@
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}

View File

@ -14,6 +14,7 @@
},
"dependencies": {
"axios": "^1.9.0",
"element-plus": "^2.7.7",
"pinia": "^3.0.1",
"qrcode": "^1.5.4",
"vue": "^3.5.13",

172
pnpm-lock.yaml generated
View File

@ -11,6 +11,9 @@ importers:
axios:
specifier: ^1.9.0
version: 1.9.0
element-plus:
specifier: ^2.7.7
version: 2.10.3(vue@3.5.16(typescript@5.8.3))
pinia:
specifier: ^3.0.1
version: 3.0.2(typescript@5.8.3)(vue@3.5.16(typescript@5.8.3))
@ -218,6 +221,15 @@ packages:
resolution: {integrity: sha512-Y1GkI4ktrtvmawoSq+4FCVHNryea6uR+qUQy0AGxLSsjCX0nVmkYQMBLHDkXZuo5hGx7eYdnIaslsdBFm7zbUw==}
engines: {node: '>=6.9.0'}
'@ctrl/tinycolor@3.6.1':
resolution: {integrity: sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==}
engines: {node: '>=10'}
'@element-plus/icons-vue@2.3.1':
resolution: {integrity: sha512-XxVUZv48RZAd87ucGS48jPf6pKu0yV5UCg9f4FFwtrYxXOwWuVJo6wOvSLKEoMQKjv8GsX/mhP6UsC1lRwbUWg==}
peerDependencies:
vue: ^3.2.0
'@esbuild/aix-ppc64@0.21.5':
resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==}
engines: {node: '>=12'}
@ -394,6 +406,15 @@ packages:
resolution: {integrity: sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@floating-ui/core@1.7.2':
resolution: {integrity: sha512-wNB5ooIKHQc+Kui96jE/n69rHFWAVoxn5CAzL1Xdd8FG03cgY3MLO+GF9U3W737fYDSgPWA6MReKhBQBop6Pcw==}
'@floating-ui/dom@1.7.2':
resolution: {integrity: sha512-7cfaOQuCS27HD7DX+6ib2OrnW+b4ZBwDNnCcT0uTyidcmyWb03FnQqJybDBoCnpdxwBSfA94UAYlRCt7mV+TbA==}
'@floating-ui/utils@0.2.10':
resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==}
'@humanfs/core@0.19.1':
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
engines: {node: '>=18.18.0'}
@ -578,6 +599,9 @@ packages:
resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==}
engines: {node: '>=18'}
'@sxzz/popperjs-es@2.11.7':
resolution: {integrity: sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==}
'@tsconfig/node22@22.0.2':
resolution: {integrity: sha512-Kmwj4u8sDRDrMYRoN9FDEcXD8UpBSaPQQ24Gz+Gamqfm7xxn+GBR7ge/Z7pK8OXNGyUzbSwJj+TH6B+DS/epyA==}
@ -587,12 +611,21 @@ packages:
'@types/json-schema@7.0.15':
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
'@types/lodash-es@4.17.12':
resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==}
'@types/lodash@4.17.20':
resolution: {integrity: sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==}
'@types/node@22.15.29':
resolution: {integrity: sha512-LNdjOkUDlU1RZb8e1kOIUpN1qQUlzGkEtbVNo53vbrwDg5om6oduhm4SiUaPW5ASTXhAiP0jInWG8Qx9fVlOeQ==}
'@types/qrcode@1.5.5':
resolution: {integrity: sha512-CdfBi/e3Qk+3Z/fXYShipBT13OJ2fDO2Q2w5CIP5anLTLIndQG9z6P1cnm+8zCWSpm5dnxMFd/uREtb0EXuQzg==}
'@types/web-bluetooth@0.0.16':
resolution: {integrity: sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==}
'@typescript-eslint/eslint-plugin@8.33.0':
resolution: {integrity: sha512-CACyQuqSHt7ma3Ns601xykeBK/rDeZa3w6IS6UtMQbixO5DWy+8TilKkviGDH6jtWCo8FGRKEK5cLLkPvEammQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@ -767,6 +800,15 @@ packages:
vue:
optional: true
'@vueuse/core@9.13.0':
resolution: {integrity: sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==}
'@vueuse/metadata@9.13.0':
resolution: {integrity: sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==}
'@vueuse/shared@9.13.0':
resolution: {integrity: sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==}
acorn-jsx@5.3.2:
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
peerDependencies:
@ -798,6 +840,9 @@ packages:
argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
async-validator@4.2.5:
resolution: {integrity: sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==}
asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
@ -887,6 +932,9 @@ packages:
csstype@3.1.3:
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
dayjs@1.11.13:
resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==}
de-indent@1.0.2:
resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==}
@ -932,6 +980,11 @@ packages:
electron-to-chromium@1.5.161:
resolution: {integrity: sha512-hwtetwfKNZo/UlwHIVBlKZVdy7o8bIZxxKs0Mv/ROPiQQQmDgdm5a+KvKtBsxM8ZjFzTaCeLoodZ8jiBE3o9rA==}
element-plus@2.10.3:
resolution: {integrity: sha512-OLpf0iekuvWJrz1+H9ybvem6TYTKSNk6L1QDA3tYq2YWbogKXJnWpHG1UAGKR1B7gx+vUH7M15VIH3EijE9Kgw==}
peerDependencies:
vue: ^3.2.0
emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
@ -967,6 +1020,9 @@ packages:
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
engines: {node: '>=6'}
escape-html@1.0.3:
resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
escape-string-regexp@4.0.0:
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
engines: {node: '>=10'}
@ -1315,6 +1371,16 @@ packages:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
engines: {node: '>=10'}
lodash-es@4.17.21:
resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
lodash-unified@1.0.3:
resolution: {integrity: sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==}
peerDependencies:
'@types/lodash-es': '*'
lodash: '*'
lodash-es: '*'
lodash.merge@4.6.2:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
@ -1331,6 +1397,9 @@ packages:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'}
memoize-one@6.0.0:
resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==}
memorystream@0.3.1:
resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==}
engines: {node: '>= 0.10.0'}
@ -1387,6 +1456,9 @@ packages:
node-releases@2.0.19:
resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==}
normalize-wheel-es@1.2.0:
resolution: {integrity: sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==}
npm-normalize-package-bin@4.0.0:
resolution: {integrity: sha512-TZKxPvItzai9kN9H/TkmCtx/ZN/hvr3vUycjlfmH0ootY9yFBzNOpiXAdIn1Iteqsvk4lQn6B5PTrt+n6h8k/w==}
engines: {node: ^18.17.0 || >=20.5.0}
@ -1744,6 +1816,17 @@ packages:
vscode-uri@3.1.0:
resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==}
vue-demi@0.14.10:
resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==}
engines: {node: '>=12'}
hasBin: true
peerDependencies:
'@vue/composition-api': ^1.0.0-rc.1
vue: ^3.0.0-0 || ^2.6.0
peerDependenciesMeta:
'@vue/composition-api':
optional: true
vue-eslint-parser@10.1.3:
resolution: {integrity: sha512-dbCBnd2e02dYWsXoqX5yKUZlOt+ExIpq7hmHKPb5ZqKcjf++Eo0hMseFTZMLKThrUk61m+Uv6A2YSBve6ZvuDQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@ -2014,6 +2097,12 @@ snapshots:
'@babel/helper-string-parser': 7.27.1
'@babel/helper-validator-identifier': 7.27.1
'@ctrl/tinycolor@3.6.1': {}
'@element-plus/icons-vue@2.3.1(vue@3.5.16(typescript@5.8.3))':
dependencies:
vue: 3.5.16(typescript@5.8.3)
'@esbuild/aix-ppc64@0.21.5':
optional: true
@ -2127,6 +2216,17 @@ snapshots:
'@eslint/core': 0.14.0
levn: 0.4.1
'@floating-ui/core@1.7.2':
dependencies:
'@floating-ui/utils': 0.2.10
'@floating-ui/dom@1.7.2':
dependencies:
'@floating-ui/core': 1.7.2
'@floating-ui/utils': 0.2.10
'@floating-ui/utils@0.2.10': {}
'@humanfs/core@0.19.1': {}
'@humanfs/node@0.16.6':
@ -2245,12 +2345,20 @@ snapshots:
'@sindresorhus/merge-streams@4.0.0': {}
'@sxzz/popperjs-es@2.11.7': {}
'@tsconfig/node22@22.0.2': {}
'@types/estree@1.0.7': {}
'@types/json-schema@7.0.15': {}
'@types/lodash-es@4.17.12':
dependencies:
'@types/lodash': 4.17.20
'@types/lodash@4.17.20': {}
'@types/node@22.15.29':
dependencies:
undici-types: 6.21.0
@ -2259,6 +2367,8 @@ snapshots:
dependencies:
'@types/node': 22.15.29
'@types/web-bluetooth@0.0.16': {}
'@typescript-eslint/eslint-plugin@8.33.0(@typescript-eslint/parser@8.33.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)':
dependencies:
'@eslint-community/regexpp': 4.12.1
@ -2528,6 +2638,25 @@ snapshots:
typescript: 5.8.3
vue: 3.5.16(typescript@5.8.3)
'@vueuse/core@9.13.0(vue@3.5.16(typescript@5.8.3))':
dependencies:
'@types/web-bluetooth': 0.0.16
'@vueuse/metadata': 9.13.0
'@vueuse/shared': 9.13.0(vue@3.5.16(typescript@5.8.3))
vue-demi: 0.14.10(vue@3.5.16(typescript@5.8.3))
transitivePeerDependencies:
- '@vue/composition-api'
- vue
'@vueuse/metadata@9.13.0': {}
'@vueuse/shared@9.13.0(vue@3.5.16(typescript@5.8.3))':
dependencies:
vue-demi: 0.14.10(vue@3.5.16(typescript@5.8.3))
transitivePeerDependencies:
- '@vue/composition-api'
- vue
acorn-jsx@5.3.2(acorn@8.14.1):
dependencies:
acorn: 8.14.1
@ -2553,6 +2682,8 @@ snapshots:
argparse@2.0.1: {}
async-validator@4.2.5: {}
asynckit@0.4.0: {}
axios@1.9.0:
@ -2643,6 +2774,8 @@ snapshots:
csstype@3.1.3: {}
dayjs@1.11.13: {}
de-indent@1.0.2: {}
debug@4.4.1:
@ -2674,6 +2807,27 @@ snapshots:
electron-to-chromium@1.5.161: {}
element-plus@2.10.3(vue@3.5.16(typescript@5.8.3)):
dependencies:
'@ctrl/tinycolor': 3.6.1
'@element-plus/icons-vue': 2.3.1(vue@3.5.16(typescript@5.8.3))
'@floating-ui/dom': 1.7.2
'@popperjs/core': '@sxzz/popperjs-es@2.11.7'
'@types/lodash': 4.17.20
'@types/lodash-es': 4.17.12
'@vueuse/core': 9.13.0(vue@3.5.16(typescript@5.8.3))
async-validator: 4.2.5
dayjs: 1.11.13
escape-html: 1.0.3
lodash: 4.17.21
lodash-es: 4.17.21
lodash-unified: 1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.17.21)(lodash@4.17.21)
memoize-one: 6.0.0
normalize-wheel-es: 1.2.0
vue: 3.5.16(typescript@5.8.3)
transitivePeerDependencies:
- '@vue/composition-api'
emoji-regex@8.0.0: {}
entities@4.5.0: {}
@ -2723,6 +2877,8 @@ snapshots:
escalade@3.2.0: {}
escape-html@1.0.3: {}
escape-string-regexp@4.0.0: {}
eslint-config-prettier@10.1.5(eslint@9.28.0(jiti@2.4.2)):
@ -3055,6 +3211,14 @@ snapshots:
dependencies:
p-locate: 5.0.0
lodash-es@4.17.21: {}
lodash-unified@1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.17.21)(lodash@4.17.21):
dependencies:
'@types/lodash-es': 4.17.12
lodash: 4.17.21
lodash-es: 4.17.21
lodash.merge@4.6.2: {}
lodash@4.17.21: {}
@ -3069,6 +3233,8 @@ snapshots:
math-intrinsics@1.1.0: {}
memoize-one@6.0.0: {}
memorystream@0.3.1: {}
merge2@1.4.1: {}
@ -3108,6 +3274,8 @@ snapshots:
node-releases@2.0.19: {}
normalize-wheel-es@1.2.0: {}
npm-normalize-package-bin@4.0.0: {}
npm-run-all2@7.0.2:
@ -3439,6 +3607,10 @@ snapshots:
vscode-uri@3.1.0: {}
vue-demi@0.14.10(vue@3.5.16(typescript@5.8.3)):
dependencies:
vue: 3.5.16(typescript@5.8.3)
vue-eslint-parser@10.1.3(eslint@9.28.0(jiti@2.4.2)):
dependencies:
debug: 4.4.1

View File

@ -3,9 +3,11 @@ import { RouterView } from 'vue-router'
</script>
<template>
<keep-alive>
<RouterView />
</keep-alive>
<router-view v-slot="{ Component }">
<keep-alive>
<component :is="Component" />
</keep-alive>
</router-view>
</template>
<style>

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { ref, onMounted, watch } from 'vue';
import QRCode from 'qrcode';
const props = defineProps({
@ -33,11 +33,16 @@ const getDisplayUrl = (url: string): string => {
}
};
onMounted(async () => {
const generateQRCode = async (url: string) => {
if (!url) {
console.warn('QRCodeDisplay: URL 为空,无法生成二维码。');
qrcodeImgUrl.value = ''; //
return;
}
try {
isLoading.value = true;
// 使 toDataURL URL
qrcodeImgUrl.value = await QRCode.toDataURL(props.url, {
console.log('QRCodeDisplay: 尝试生成二维码URL:', url);
qrcodeImgUrl.value = await QRCode.toDataURL(url, {
width: 180,
margin: 1,
errorCorrectionLevel: 'H',
@ -46,15 +51,25 @@ onMounted(async () => {
light: '#ffffff'
}
});
console.log('QRCodeDisplay: 二维码生成成功URL数据:', qrcodeImgUrl.value.substring(0, 50) + '...');
} catch (error) {
console.error('生成二维码失败:', error);
// 使
qrcodeImgUrl.value = `https://api.qrserver.com/v1/create-qr-code/?size=180x180&data=${encodeURIComponent(props.url)}`;
console.error('QRCodeDisplay: 生成二维码失败:', error);
qrcodeImgUrl.value = `https://api.qrserver.com/v1/create-qr-code/?size=180x180&data=${encodeURIComponent(url)}`;
console.log('QRCodeDisplay: 二维码生成失败使用备用URL:', qrcodeImgUrl.value.substring(0, 50) + '...');
} finally {
isLoading.value = false;
}
};
onMounted(() => {
generateQRCode(props.url);
});
// props.url
watch(() => props.url, (newUrl) => {
generateQRCode(newUrl);
}, { immediate: true }); // immediate: true
const onDownload = () => {
try {
if (!qrcodeImgUrl.value) {
@ -82,7 +97,8 @@ const onDownload = () => {
<div class="spinner"></div>
</div>
<div v-else class="qrcode">
<img :src="qrcodeImgUrl" :alt="title" />
<img v-if="qrcodeImgUrl" :src="qrcodeImgUrl" :alt="title" />
<p v-else class="qr-code-fallback">二维码加载失败</p>
</div>
</div>
<div class="qrcode-info">
@ -113,6 +129,12 @@ const onDownload = () => {
overflow: hidden;
}
.qr-code-fallback {
text-align: center;
color: #999;
font-size: 0.9em;
}
.qrcode img {
max-width: 100%;
height: auto;

View File

@ -76,6 +76,9 @@ onUnmounted(() => {
<li class="nav-item">
<router-link to="/hasfjform" class="nav-link" @click="closeMenu">表单下载</router-link>
</li>
<li class="nav-item">
<router-link to="/feedback-query" class="nav-link" @click="closeMenu">留言查询</router-link>
</li>
</ul>
</div>
</div>

View File

@ -60,6 +60,12 @@ const router = createRouter({
name: 'searchResults',
component: () => import('../views/SearchResultsView.vue'),
},
// 留言查询页面
{
path: '/feedback-query',
name: 'feedbackQuery',
component: () => import('../views/FeedbackQueryView.vue'),
},
],
})

View File

@ -2,7 +2,7 @@ import axios from 'axios'
// 创建axios实例
const apiClient = axios.create({
baseURL: '', // 使用相对路径将通过Vite代理
baseURL: 'http://222.184.49.22:19696/', // 直接设置后端服务地址
timeout: 15000, // 请求超时时间
headers: {
'Content-Type': 'application/json',
@ -239,7 +239,7 @@ async function requestWithCorrectType(type: string, formatId: string) {
// 如果上面的方法失败,尝试其他备用方法
console.log(`尝试使用备用API路径获取详情`);
const backupUrl = `/api/hasfj/hasfjpages/list`;
const backupUrl = `/hasfj/hasfjpages/list`;
const backupParams = { pageType: type, pageNum: 1, pageSize: 10 };
const backupResponse = await apiClient.get(backupUrl, { params: backupParams });
@ -915,126 +915,98 @@ export const getBaseUrl = (): string => {
// 更新页面浏览量
export const updateViewCount = async (type: string, formatId: string) => {
try {
console.log(`开始更新${type}浏览量formatId:`, formatId);
// 前端直接调用页面查询接口来更新浏览量避开updateViewCount接口问题
// 从README-浏览量更新.md可知后端可能没有实现正确的updateViewCount接口
try {
console.log(`尝试使用getInfoByFormatId接口获取详情并更新浏览量`);
// 使用format/{formatId}接口,这个接口会自动更新浏览量
const response = await apiClient.get(`/hasfj/hasfjpages/format/${formatId}`);
console.log(`获取页面详情响应:`, response);
if (response.status === 200 && response.data) {
// 如果请求成功,从响应中提取浏览量数据
const viewCount =
(response.data.data && typeof response.data.data.viewCount === 'number') ? response.data.data.viewCount :
(response.data.viewCount !== undefined) ? response.data.viewCount : null;
if (viewCount !== null) {
return {
code: 200,
msg: '更新成功',
data: {
viewCount: viewCount
}
};
} else {
// 如果没有获得浏览量数据,但请求成功,依然返回成功
return {
code: 200,
msg: '更新成功',
};
}
}
} catch (error) {
console.warn(`使用页面详情接口更新浏览量失败:`, error);
console.log(`更新${type}浏览次数formatId:`, formatId);
const url = `/hasfj/hasfjpages/updateViewCount`; // 假设后端有此接口
const data = { pageType: type, formatId: formatId };
const response = await apiClient.post(url, data);
if (response.status === 200) {
console.log(`浏览次数更新成功:`, response.data);
return response.data;
} else {
console.error(`浏览次数更新失败:`, response);
throw new Error(`浏览次数更新失败,状态码: ${response.status}`);
}
// 退化方案:使用页面列表接口查询页面,然后记录浏览量
try {
console.log(`尝试使用页面列表接口查询页面`);
const listResponse = await apiClient.get(`/hasfj/hasfjpages/list`, {
params: {
pageType: type,
formatId: formatId,
pageNum: 1,
pageSize: 10
}
});
console.log(`页面列表响应:`, listResponse);
if (listResponse.status === 200 && listResponse.data && listResponse.data.rows) {
// 从列表结果中查找匹配的记录
const rows = listResponse.data.rows;
let targetPage = null;
if (Array.isArray(rows)) {
targetPage = rows.find((item: any) =>
item.formatId === formatId && (!item.pageType || item.pageType === type));
}
if (targetPage) {
// 找到匹配的页面,获取其浏览量
const viewCount = typeof targetPage.viewCount === 'number' ? targetPage.viewCount : null;
if (viewCount !== null) {
return {
code: 200,
msg: '更新成功',
data: {
viewCount: viewCount
}
};
}
}
}
} catch (error) {
console.warn(`使用页面列表接口查询失败:`, error);
}
// 最后的方案使用get接口不更新浏览量仅获取当前页面数据
try {
console.log(`尝试使用get接口获取页面数据`);
const getResponse = await apiClient.get(`/api/hasfj/page`, {
params: {
type: type,
formatId: formatId
}
});
if (getResponse.status === 200 && getResponse.data) {
// 从响应中提取浏览量
const viewCount =
(getResponse.data.data && typeof getResponse.data.data.viewCount === 'number') ? getResponse.data.data.viewCount :
(getResponse.data.viewCount !== undefined) ? getResponse.data.viewCount : null;
if (viewCount !== null) {
return {
code: 200,
msg: '更新成功',
data: {
viewCount: viewCount
}
};
}
}
} catch (error) {
console.warn(`使用get接口获取页面数据失败:`, error);
}
// 所有方法都失败,返回一个通用成功响应
return {
code: 200,
msg: '操作完成',
};
} catch (error) {
console.error(`更新浏览量失败:`, error);
// 即使失败也返回成功,以免影响用户体验
return {
code: 200,
msg: '操作完成'
};
console.error(`更新浏览次数异常:`, error);
throw error;
}
}
};
/**
*
* @param {Object} feedbackData -
*/
export const submitFeedback = async (feedbackData: any) => {
try {
console.log('提交用户留言:', feedbackData);
const response = await apiClient.post('/hasfj/feedback', feedbackData);
if (response.status === 200 && response.data.code === 200) {
console.log('用户留言提交成功:', response.data);
return response.data;
} else {
console.error('用户留言提交失败:', response);
throw new Error(response.data.msg || '留言提交失败');
}
} catch (error) {
console.error('用户留言提交异常:', error);
throw error;
}
};
/**
*
* @param {Object} queryParams - (feedbackNo, phoneNumber, idCardNumber, userName)
*/
export const queryFeedback = async (queryParams: any) => {
try {
console.log('查询用户留言:', queryParams);
const response = await apiClient.get('/hasfj/feedback/query', { params: queryParams });
if (response.status === 200 && response.data.code === 200) {
console.log('用户留言查询成功:', response.data);
return response.data;
} else {
console.error('用户留言查询失败:', response);
throw new Error(response.data.msg || '留言查询失败');
}
} catch (error) {
console.error('用户留言查询异常:', error);
throw error;
}
};
// 新增用户留言
export const addFeedback = async (data: any) => {
try {
const response = await apiClient.post('/hasfj/feedback/user/submit', data);
return response.data;
} catch (error) {
console.error('提交留言失败:', error);
throw error;
}
};
// 查询用户留言
export const listFeedback = async (query: any) => {
try {
const response = await apiClient.get('/user/query', { params: query });
return response.data;
} catch (error) {
console.error('列表留言失败:', error);
throw error;
}
};
/**
*
* @param {string} feedbackNo -
*/
export const queryFeedbackDetail = async (queryParams: any) => {
try {
// 使用新的 /user/queryDetail 接口,并传递包含所有查询参数的对象
const response = await apiClient.get('/hasfj/feedback/user/queryDetail', { params: queryParams });
return response.data;
} catch (error) {
console.error('查询留言详情失败:', error);
throw error;
}
};

View File

@ -5,111 +5,111 @@ import TheNavbar from '../components/TheNavbar.vue'
import Pagination from '../components/Pagination.vue'
interface PageItem {
id: number;
formatId: string;
title: string;
content?: string;
pageType?: string;
pageUrl?: string;
createTime: string;
updateTime?: string;
status?: number;
sortOrder?: number;
viewCount: number;
author?: string;
keywords?: string;
description?: string;
attachmentUrl?: string;
id: number
formatId: string
title: string
content?: string
pageType?: string
pageUrl?: string
createTime: string
updateTime?: string
status?: number
sortOrder?: number
viewCount: number
author?: string
keywords?: string
description?: string
attachmentUrl?: string
}
const caseList = ref<PageItem[]>([]);
const loading = ref(true);
const error = ref('');
const total = ref(0);
const currentPage = ref(1);
const pageSize = ref(12); // 12
const caseList = ref<PageItem[]>([])
const loading = ref(true)
const error = ref('')
const total = ref(0)
const currentPage = ref(1)
const pageSize = ref(12) // 12
const fetchCases = async () => {
try {
loading.value = true;
error.value = '';
const result = await getPageList('case', currentPage.value, pageSize.value);
loading.value = true
error.value = ''
const result = await getPageList('case', currentPage.value, pageSize.value)
if (result.code === 200) {
caseList.value = result.rows || [];
total.value = result.total || caseList.value.length;
caseList.value = result.rows || []
total.value = result.total || caseList.value.length
//
if (caseList.value.length === 0 && currentPage.value > 1) {
currentPage.value = currentPage.value - 1;
await fetchCases();
currentPage.value = currentPage.value - 1
await fetchCases()
}
} else {
error.value = result.msg || '获取数据失败';
caseList.value = [];
error.value = result.msg || '获取数据失败'
caseList.value = []
}
} catch (e) {
console.error(e);
error.value = '获取数据失败,请稍后重试';
caseList.value = [];
console.error(e)
error.value = '获取数据失败,请稍后重试'
caseList.value = []
} finally {
loading.value = false;
loading.value = false
}
};
}
//
watch([currentPage, pageSize], () => {
fetchCases();
});
fetchCases()
})
//
const handlePageChange = (page: number, size: number) => {
currentPage.value = page;
pageSize.value = size;
};
currentPage.value = page
pageSize.value = size
}
//
const formatDate = (dateStr: string | null | undefined): string => {
if (!dateStr) return '未知日期';
if (!dateStr) return '未知日期'
try {
const date = new Date(dateStr);
const date = new Date(dateStr)
if (isNaN(date.getTime())) {
return '未知日期';
return '未知日期'
}
return date.toLocaleDateString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
});
return date.toLocaleDateString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
})
} catch (e) {
console.error('日期格式化错误:', e);
return '未知日期';
console.error('日期格式化错误:', e)
return '未知日期'
}
}
onMounted(() => {
fetchCases();
});
fetchCases()
})
//
onActivated(() => {
console.log('典型案例列表页面被激活,重新获取数据');
fetchCases();
});
console.log('典型案例列表页面被激活,重新获取数据')
fetchCases()
})
</script>
<template>
<div class="case-list">
<TheNavbar />
<div class="page-header">
<div class="container">
<h1>典型案例</h1>
<p class="subtitle">查看劳动纠纷的典型处理案例</p>
</div>
</div>
<div class="container">
<div class="content-wrapper">
<div class="cases-section">
@ -117,19 +117,19 @@ onActivated(() => {
<div class="spinner"></div>
<p>加载中...</p>
</div>
<div v-else-if="error" class="error">
<h3>内容加载失败</h3>
<p>{{ error }}</p>
<button @click="fetchCases" class="btn-retry">重试</button>
</div>
<div v-else-if="caseList.length === 0" class="empty">
<h3>暂无内容</h3>
<p>当前没有典型案例相关内容</p>
<router-link to="/" class="btn-home">返回首页</router-link>
</div>
<div v-else class="list-content">
<div class="case-grid">
<div v-for="item in caseList" :key="item.formatId" class="case-card">
@ -148,14 +148,14 @@ onActivated(() => {
</router-link>
</div>
</div>
<!-- 分页组件 -->
<Pagination
<Pagination
:total="total"
v-model:current-page="currentPage"
v-model:page-size="pageSize"
@change="handlePageChange"
:page-sizes="[8, 16, 24, 32]"
:page-sizes="[8, 16, 24, 32, 150]"
/>
</div>
</div>
@ -240,7 +240,9 @@ onActivated(() => {
width: 100%;
}
.loading, .error, .empty {
.loading,
.error,
.empty {
display: flex;
flex-direction: column;
align-items: center;
@ -260,10 +262,13 @@ onActivated(() => {
}
@keyframes spin {
to { transform: rotate(360deg); }
to {
transform: rotate(360deg);
}
}
.btn-retry, .btn-home {
.btn-retry,
.btn-home {
background-color: var(--color-primary);
color: white;
border: none;
@ -278,7 +283,8 @@ onActivated(() => {
display: inline-block;
}
.btn-retry:hover, .btn-home:hover {
.btn-retry:hover,
.btn-home:hover {
background-color: var(--color-primary-dark);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 123, 255, 0.3);
@ -356,13 +362,15 @@ onActivated(() => {
gap: 0.5rem;
}
.case-date, .case-views {
.case-date,
.case-views {
display: flex;
align-items: center;
gap: 0.5rem;
}
.case-date::before, .case-views::before {
.case-date::before,
.case-views::before {
content: '';
display: inline-block;
width: 16px;
@ -409,16 +417,16 @@ onActivated(() => {
.page-header {
padding: 2.5rem 0;
}
.page-header h1 {
font-size: 2.2rem;
}
.case-grid {
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 1rem;
}
.cases-section {
padding: 1.5rem;
}
@ -428,26 +436,26 @@ onActivated(() => {
.page-header {
padding: 2rem 0;
}
.page-header h1 {
font-size: 1.8rem;
}
.subtitle {
font-size: 1rem;
}
.case-grid {
grid-template-columns: 1fr;
}
.cases-section {
padding: 1rem;
border-radius: 8px;
}
.case-link {
padding: 1.2rem;
}
}
</style>
</style>

View File

@ -1,7 +1,13 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ref, onMounted, computed, reactive } from 'vue'
import { useRoute } from 'vue-router'
import { getPageDetail, updateViewCount } from '../services/api'
import {
getPageDetail,
updateViewCount,
addFeedback,
listFeedback,
queryFeedbackDetail,
} from '../services/api'
import TheNavbar from '../components/TheNavbar.vue'
const route = useRoute()
@ -9,131 +15,281 @@ const loading = ref(true)
const error = ref(false)
const page = ref<any>(null)
const feedbackForm = reactive({
legalArticle: '',
issueDescription: '',
userName: '',
phoneNumber: '',
idCardNumber: '',
contactInfoType: 1, //
})
const feedbackSubmitSuccess = ref(false)
const feedbackNoReturned = ref('')
// ##
const processedContent = computed(() => {
if (!page.value || !page.value.content) return ''
// ##
const regex = /#([^#]+)#([\s\S]*?)(?=#[^#]+#|$)/g
let index = 0
// HTML使onclick
const result = page.value.content.replace(
regex,
(match: string, title: string, content: string) => {
//
title = title.trim()
content = content.trim()
const currentIndex = index++
return `
<div class="law-card" data-card-index="${currentIndex}">
<div class="law-card-header" data-card-index="${currentIndex}">
<span class="law-card-title">${title}</span>
<span class="law-card-toggle"></span>
</div>
<div class="law-card-content">
${content}
</div>
</div>
`
},
)
return result
})
//
const expandedCards = ref<Set<number>>(new Set())
const toggleCard = (index: number) => {
const card = document.querySelector(`.law-card[data-card-index="${index}"]`)
if (card) {
if (expandedCards.value.has(index)) {
expandedCards.value.delete(index)
card.classList.remove('expanded')
} else {
expandedCards.value.add(index)
card.classList.add('expanded')
}
}
}
onMounted(async () => {
try {
const formatId = route.query.Id as string
if (!formatId) {
console.error('缺少必要的formatId参数');
error.value = true;
loading.value = false;
return;
console.error('缺少必要的formatId参数')
error.value = true
loading.value = false
return
}
console.log('开始获取典型案例详情formatId:', formatId);
console.log('开始获取典型案例详情formatId:', formatId)
//
const result = await getPageDetail('case', formatId);
console.log('典型案例详情请求结果:', result);
const result = await getPageDetail('case', formatId)
console.log('典型案例详情请求结果:', result)
if (result && result.code === 200 && result.data) {
page.value = result.data;
console.log('成功获取典型案例详情:', page.value);
page.value = result.data
console.log('成功获取典型案例详情:', page.value)
//
try {
const viewResult = await updateViewCount('case', formatId) as any;
console.log('更新浏览量结果:', viewResult);
const viewResult = (await updateViewCount('case', formatId)) as any
console.log('更新浏览量结果:', viewResult)
//
if (viewResult && viewResult.code === 200) {
//
if (viewResult.data && typeof viewResult.data.viewCount === 'number') {
page.value.viewCount = viewResult.data.viewCount;
}
//
else if (typeof page.value.viewCount === 'number' || typeof page.value.viewCount === 'string') {
page.value.viewCount = Number(page.value.viewCount || 0) + 1;
} else {
page.value.viewCount = 1; //
page.value.viewCount = viewResult.data.viewCount
}
console.log('浏览量更新为:', page.value.viewCount);
//
else if (
typeof page.value.viewCount === 'number' ||
typeof page.value.viewCount === 'string'
) {
page.value.viewCount = Number(page.value.viewCount || 0) + 1
} else {
page.value.viewCount = 1 //
}
console.log('浏览量更新为:', page.value.viewCount)
} else {
console.warn('浏览量更新API返回错误:', viewResult?.msg || '未知错误');
console.warn('浏览量更新API返回错误:', viewResult?.msg || '未知错误')
}
} catch (e) {
console.error('更新浏览量失败:', e);
console.error('更新浏览量失败:', e)
//
if (typeof page.value.viewCount === 'number' || typeof page.value.viewCount === 'string') {
page.value.viewCount = Number(page.value.viewCount || 0) + 1;
console.log('API出错本地更新浏览量为:', page.value.viewCount);
page.value.viewCount = Number(page.value.viewCount || 0) + 1
console.log('API出错本地更新浏览量为:', page.value.viewCount)
}
}
//
if (page.value.pageType && page.value.pageType !== 'case') {
console.error(`错误: 请求的是典型案例(case),但返回的是${page.value.pageType}类型的内容`);
console.error(`错误: 请求的是典型案例(case),但返回的是${page.value.pageType}类型的内容`)
//
error.value = true;
loading.value = false;
return;
error.value = true
loading.value = false
return
}
//
if (page.value.title && /\\u|%/.test(page.value.title)) {
try {
page.value.title = decodeURIComponent(page.value.title);
page.value.title = decodeURIComponent(page.value.title)
} catch (e) {
console.error('标题解码失败:', e);
console.error('标题解码失败:', e)
}
}
// HTML
if (page.value.content) {
console.log('内容类型:', typeof page.value.content);
console.log('内容前50个字符:', page.value.content.substring(0, 50));
console.log('内容类型:', typeof page.value.content)
console.log('内容前50个字符:', page.value.content.substring(0, 50))
//
if (/\\u|%/.test(page.value.content)) {
try {
page.value.content = decodeURIComponent(page.value.content);
page.value.content = decodeURIComponent(page.value.content)
} catch (e) {
console.error('内容解码失败:', e);
console.error('内容解码失败:', e)
}
}
// HTML
if (typeof page.value.content !== 'string') {
try {
page.value.content = JSON.stringify(page.value.content);
page.value.content = JSON.stringify(page.value.content)
} catch (e) {
console.error('内容格式转换失败:', e);
console.error('内容格式转换失败:', e)
}
}
} else {
console.warn('页面内容为空');
page.value.content = '<p>暂无内容</p>';
console.warn('页面内容为空')
page.value.content = '<p>暂无内容</p>'
}
//
document.title = `${page.value.title || '典型案例详情'} - 典型案例`;
document.title = `${page.value.title || '典型案例详情'} - 典型案例`
} else {
console.error('获取典型案例详情失败:', result?.msg || '未知错误');
error.value = true;
console.error('获取典型案例详情失败:', result?.msg || '未知错误')
error.value = true
}
} catch (e) {
console.error('获取典型案例详情异常:', e);
error.value = true;
console.error('获取典型案例详情异常:', e)
error.value = true
} finally {
loading.value = false;
loading.value = false
}
})
//
const formatDate = (dateStr: string | null | undefined): string => {
if (!dateStr) return '未知日期';
if (!dateStr) return '未知日期'
try {
const date = new Date(dateStr);
const date = new Date(dateStr)
if (isNaN(date.getTime())) {
return '未知日期';
return '未知日期'
}
return date.toLocaleDateString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
});
day: '2-digit',
})
} catch (e) {
console.error('日期格式化错误:', e);
return '未知日期';
console.error('日期格式化错误:', e)
return '未知日期'
}
}
//
const initCollapsibleCards = () => {
console.log('初始化折叠卡片...')
// 使
document.querySelector('.body')?.addEventListener('click', (e) => {
const target = e.target as HTMLElement
const header = target.closest('.law-card-header')
if (header) {
const index = parseInt(header.getAttribute('data-card-index') || '-1')
if (index >= 0) {
toggleCard(index)
console.log('切换卡片状态:', index)
}
}
})
console.log('折叠卡片初始化完成')
}
//
onMounted(() => {
//
setTimeout(() => {
initCollapsibleCards()
}, 1000)
})
const submitFeedback = async () => {
try {
//
if (!feedbackForm.legalArticle.trim()) {
alert('有问题的案例不能为空。')
return
}
if (!feedbackForm.issueDescription.trim()) {
alert('问题描述不能为空。')
return
}
if (feedbackForm.contactInfoType === 1 && !feedbackForm.phoneNumber.trim()) {
alert('手机号不能为空。')
return
} else if (feedbackForm.contactInfoType === 2 && !feedbackForm.idCardNumber.trim()) {
alert('身份证号不能为空。')
return
} else if (feedbackForm.contactInfoType === 3 && !feedbackForm.userName.trim()) {
alert('姓名不能为空。')
return
}
const dataToSend = { ...feedbackForm }
//
if (feedbackForm.contactInfoType === 1) {
dataToSend.idCardNumber = ''
dataToSend.userName = ''
} else if (feedbackForm.contactInfoType === 2) {
dataToSend.phoneNumber = ''
dataToSend.userName = ''
} else if (feedbackForm.contactInfoType === 3) {
dataToSend.phoneNumber = ''
dataToSend.idCardNumber = ''
}
const res = await addFeedback(dataToSend)
if (res && res.code === 200) {
feedbackSubmitSuccess.value = true
feedbackNoReturned.value = res.data?.feedbackNo || '未知'
//
feedbackForm.legalArticle = ''
feedbackForm.issueDescription = ''
feedbackForm.userName = ''
feedbackForm.phoneNumber = ''
feedbackForm.idCardNumber = ''
feedbackForm.contactInfoType = 1
console.log('留言提交成功,查询编号:', feedbackNoReturned.value)
} else {
alert('提交留言失败:' + (res.msg || '未知错误'))
}
} catch (e) {
console.error('提交留言异常:', e)
alert('提交留言异常,请稍后再试')
}
}
</script>
@ -168,12 +324,99 @@ const formatDate = (dateStr: string | null | undefined): string => {
</div>
</div>
<div class="body" v-html="page.content"></div>
<div class="body" v-html="processedContent"></div>
<div class="footer">
<router-link to="/hasfjcase" class="btn-more">查看更多典型案例</router-link>
<router-link to="/" class="btn-home">返回首页</router-link>
</div>
<!-- 用户留言模块 -->
<div class="feedback-section">
<h3><i class="icon-chat"></i> 案例咨询与留言</h3>
<div v-if="!feedbackSubmitSuccess" class="feedback-form">
<p class="section-description">如果您对典型案例有任何疑问或建议欢迎在此留言</p>
<div class="form-group">
<label for="legalArticle">有问题的案例:<span class="required">*</span></label>
<input
type="text"
id="legalArticle"
v-model="feedbackForm.legalArticle"
placeholder="如2023年某某案"
/>
</div>
<div class="form-group">
<label for="issueDescription">问题描述: <span class="required">*</span></label>
<textarea
id="issueDescription"
v-model="feedbackForm.issueDescription"
rows="5"
placeholder="请详细描述您的问题字数在10-500字之间"
required
></textarea>
</div>
<div class="form-group">
<label for="contactInfoType">联系方式类型:</label>
<select id="contactInfoType" v-model="feedbackForm.contactInfoType">
<option :value="1">手机号</option>
<option :value="2">身份证号</option>
<option :value="3">姓名</option>
</select>
</div>
<div class="form-group">
<label for="phoneNumber"
>手机号:
<span class="required" v-show="feedbackForm.contactInfoType === 1">*</span></label
>
<input
type="text"
id="phoneNumber"
v-model="feedbackForm.phoneNumber"
placeholder="请输入手机号"
:required="feedbackForm.contactInfoType === 1"
/>
</div>
<div class="form-group">
<label for="idCardNumber"
>身份证号:
<span class="required" v-show="feedbackForm.contactInfoType === 2">*</span></label
>
<input
type="text"
id="idCardNumber"
v-model="feedbackForm.idCardNumber"
placeholder="请输入身份证号"
:required="feedbackForm.contactInfoType === 2"
/>
</div>
<div class="form-group">
<label for="userNameContact"
>姓名:
<span class="required" v-show="feedbackForm.contactInfoType === 3">*</span></label
>
<input
type="text"
id="userNameContact"
v-model="feedbackForm.userName"
placeholder="请输入您的姓名"
:required="feedbackForm.contactInfoType === 3"
/>
</div>
<button @click="submitFeedback" class="submit-btn">提交留言</button>
</div>
<div v-else class="feedback-success">
<h4>留言提交成功</h4>
<p>
您的留言查询编号是<span class="feedback-no">{{ feedbackNoReturned }}</span>
</p>
<p>请牢记此编号以便查询回复</p>
<button
@click="((feedbackSubmitSuccess = false), (feedbackNoReturned = ''))"
class="submit-btn"
>
继续留言
</button>
</div>
</div>
</div>
</div>
</div>
@ -210,7 +453,9 @@ const formatDate = (dateStr: string | null | undefined): string => {
}
@keyframes spin {
to { transform: rotate(360deg); }
to {
transform: rotate(360deg);
}
}
.error {
@ -350,18 +595,75 @@ const formatDate = (dateStr: string | null | undefined): string => {
flex-wrap: wrap;
}
.btn-home, .btn-more {
/* 添加法律卡片相关样式 */
:deep(.law-card) {
margin: 1.5rem 0;
border: 1px solid #e0e0e0;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
background-color: #f9f9f9;
}
:deep(.law-card-header) {
padding: 1rem 1.5rem;
background-color: #f0f0f0;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
transition: background-color 0.3s;
user-select: none; /* 防止选中文本 */
}
:deep(.law-card-header:hover) {
background-color: #e0e0e0;
}
:deep(.law-card-title) {
font-weight: bold;
color: #333;
font-size: 1.1rem;
}
:deep(.law-card-toggle) {
transition: transform 0.3s;
}
:deep(.law-card-content) {
padding: 0;
max-height: 0;
overflow: hidden;
transition:
max-height 0.5s ease,
padding 0.5s ease;
}
:deep(.law-card.expanded .law-card-content) {
padding: 1.5rem;
max-height: 2000px;
}
:deep(.law-card.expanded .law-card-toggle) {
transform: rotate(180deg);
}
.btn-home,
.btn-more {
display: inline-block;
padding: 0.6rem 1.5rem;
background-color: var(--color-primary);
color: white;
text-decoration: none;
border-radius: 4px;
transition: background-color 0.3s, transform 0.3s;
transition:
background-color 0.3s,
transform 0.3s;
font-size: 0.9rem;
}
.btn-home:hover, .btn-more:hover {
.btn-home:hover,
.btn-more:hover {
background-color: #0069d9;
transform: translateY(-2px);
}
@ -390,6 +692,10 @@ const formatDate = (dateStr: string | null | undefined): string => {
.content .body {
font-size: 0.95rem;
}
:deep(.law-card-title) {
font-size: 1rem;
}
}
@media (max-width: 576px) {
@ -426,10 +732,23 @@ const formatDate = (dateStr: string | null | undefined): string => {
padding-top: 1rem;
}
.btn-home, .btn-more {
.btn-home,
.btn-more {
padding: 0.5rem 1.2rem;
font-size: 0.85rem;
}
:deep(.law-card-header) {
padding: 0.8rem 1rem;
}
:deep(.law-card-title) {
font-size: 0.9rem;
}
:deep(.law-card.expanded .law-card-content) {
padding: 1rem;
}
}
@media (min-width: 1200px) {
@ -446,4 +765,249 @@ const formatDate = (dateStr: string | null | undefined): string => {
padding: 0;
}
}
.feedback-section {
background-color: #f9f9f9;
border-radius: 8px;
margin-top: 2rem;
padding: 2rem;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
}
.feedback-section h3 {
font-size: 1.5rem;
color: var(--color-primary);
margin-bottom: 1.5rem;
display: flex;
align-items: center;
}
.feedback-section .icon-chat::before {
content: '💬'; /* Chat bubble icon */
margin-right: 10px;
font-size: 1.8rem;
}
.feedback-section .icon-search::before {
content: '🔍'; /* Search icon */
margin-right: 10px;
font-size: 1.8rem;
}
.feedback-section .section-description {
color: var(--color-text-light);
margin-bottom: 1.5rem;
line-height: 1.6;
}
.feedback-form .form-group,
.feedback-query .form-group {
margin-bottom: 1rem;
}
.feedback-form label,
.feedback-query label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: var(--color-text);
}
.feedback-form input[type='text'],
.feedback-form textarea,
.feedback-form select,
.feedback-query input[type='text'],
.feedback-query select {
width: calc(100% - 20px);
padding: 0.8rem 10px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 1rem;
box-sizing: border-box;
}
.feedback-form textarea {
resize: vertical;
}
.feedback-form .required,
.feedback-query .required {
color: var(--color-danger);
margin-left: 5px;
}
.feedback-form .submit-btn,
.feedback-query .submit-btn {
display: inline-block;
padding: 0.8rem 2rem;
background-color: var(--color-primary);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
transition: background-color 0.3s ease;
margin-top: 1rem;
}
.feedback-form .submit-btn:hover,
.feedback-query .submit-btn:hover {
background-color: #0069d9;
}
.feedback-query .submit-btn:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
.feedback-success {
text-align: center;
padding: 2rem;
background-color: #e6ffe6;
border: 1px solid #aaffaa;
border-radius: 8px;
color: #336633;
}
.feedback-success h4 {
color: #28a745;
margin-bottom: 1rem;
font-size: 1.3rem;
}
.feedback-success .feedback-no {
font-weight: bold;
color: var(--color-primary);
font-size: 1.5rem;
}
.feedback-success button {
margin-top: 1.5rem;
padding: 0.7rem 1.5rem;
background-color: var(--color-success);
border: none;
border-radius: 4px;
color: white;
cursor: pointer;
transition: background-color 0.3s;
}
.feedback-success button:hover {
background-color: #218838;
}
.query-title {
margin-top: 3rem;
border-top: 1px dashed #eee;
padding-top: 2rem;
}
.query-result {
background-color: #e9f7fe;
border: 1px solid #b3e0ff;
border-radius: 8px;
padding: 1.5rem;
margin-top: 2rem;
}
.query-result h4 {
color: #007bff;
margin-bottom: 1rem;
font-size: 1.3rem;
padding-bottom: 0.8rem;
border-bottom: 1px dashed #cceeff;
}
.query-result .result-item {
margin-bottom: 0.8rem;
color: var(--color-text);
font-size: 0.95rem;
}
.query-result .result-item strong {
color: #333;
}
.query-result .admin-reply-content {
background-color: #ffffff;
border: 1px solid #e0e0e0;
border-radius: 4px;
padding: 1rem;
margin-top: 0.5rem;
line-height: 1.6;
color: #555;
}
.error-message {
color: var(--color-danger);
margin-top: 1rem;
font-size: 0.9rem;
}
.status-pending {
color: orange;
font-weight: bold;
}
.status-replied {
color: green;
font-weight: bold;
}
.status-closed {
color: gray;
font-weight: bold;
}
@media (max-width: 768px) {
.feedback-section {
padding: 1.5rem;
}
}
@media (max-width: 576px) {
.feedback-section {
padding: 1rem;
}
.feedback-section h3 {
font-size: 1.3rem;
}
.feedback-section .icon-chat::before,
.feedback-section .icon-search::before {
font-size: 1.5rem;
}
.feedback-form input[type='text'],
.feedback-form textarea,
.feedback-form select,
.feedback-query input[type='text'],
.feedback-query select {
padding: 0.6rem 8px;
font-size: 0.9rem;
}
.feedback-form .submit-btn,
.feedback-query .submit-btn {
padding: 0.6rem 1.5rem;
font-size: 0.9rem;
}
.feedback-success h4 {
font-size: 1.1rem;
}
.feedback-success .feedback-no {
font-size: 1.3rem;
}
.query-result h4 {
font-size: 1.1rem;
}
.query-result .result-item {
font-size: 0.85rem;
}
}
</style>

View File

@ -0,0 +1,515 @@
<script setup lang="ts">
import { reactive, ref } from 'vue'
import TheNavbar from '../components/TheNavbar.vue'
import { queryFeedbackDetail } from '../services/api'
import QRCodeDisplay from '../components/QRCodeDisplay.vue'
import { onMounted } from 'vue'
//
const feedbackQueryForm = reactive({
feedbackNo: '',
userName: '',
idCardNumber: '',
phoneNumber: '',
contactInfoType: 1, //
})
const feedbackQueryResult = ref<any>(null)
const queryErrorMessage = ref('')
const isQuerying = ref(false)
// QR Code
const currentUrl = ref('')
// URL
onMounted(() => {
currentUrl.value = window.location.href
})
//
const queryFeedback = async () => {
queryErrorMessage.value = ''
feedbackQueryResult.value = null
isQuerying.value = true
try {
//
if (!feedbackQueryForm.feedbackNo.trim()) {
queryErrorMessage.value = '留言编号不能为空。'
return
}
//
const hasContactInfo =
(feedbackQueryForm.contactInfoType === 1 && feedbackQueryForm.phoneNumber.trim()) ||
(feedbackQueryForm.contactInfoType === 2 && feedbackQueryForm.idCardNumber.trim()) ||
(feedbackQueryForm.contactInfoType === 3 && feedbackQueryForm.userName.trim())
if (!hasContactInfo) {
queryErrorMessage.value = '手机号、身份证号或姓名至少需要填写一项。'
return
}
const dataToSend = { ...feedbackQueryForm }
//
if (feedbackQueryForm.contactInfoType === 1) {
dataToSend.idCardNumber = ''
dataToSend.userName = ''
} else if (feedbackQueryForm.contactInfoType === 2) {
dataToSend.phoneNumber = ''
dataToSend.userName = ''
} else if (feedbackQueryForm.contactInfoType === 3) {
dataToSend.phoneNumber = ''
dataToSend.idCardNumber = ''
}
const res = await queryFeedbackDetail(dataToSend)
if (res && res.code === 200 && res.data) {
feedbackQueryResult.value = res.data
console.log('留言查询结果:', feedbackQueryResult.value)
} else {
queryErrorMessage.value = res.msg || '未找到匹配的留言或查询失败。'
}
} catch (e) {
console.error('查询留言异常:', e)
queryErrorMessage.value = '查询留言异常,请稍后再试。'
} finally {
isQuerying.value = false
}
}
//
const formatDate = (dateStr: string | null | undefined): string => {
if (!dateStr) return '未知日期'
try {
const date = new Date(dateStr)
if (isNaN(date.getTime())) {
return '未知日期'
}
return date.toLocaleDateString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
})
} catch (e) {
console.error('日期格式化错误:', e)
return '未知日期'
}
}
//
const getFeedbackStatus = (adminReplyContent: string | null | undefined): string => {
return adminReplyContent && adminReplyContent.trim() !== '' ? '已回复' : '待处理'
}
// P
const removePTags = (htmlString: string | null | undefined): string => {
if (!htmlString) return ''
// 使 <p> </p>
return htmlString.replace(/<p[^>]*>|<\/p>/gi, '')
}
</script>
<template>
<div class="feedback-query-container">
<TheNavbar />
<div class="content">
<div class="container query-layout">
<div class="query-card">
<h3><i class="icon-search"></i> 留言查询</h3>
<p class="section-description">请输入您的留言编号和联系方式以查询留言回复情况</p>
<div class="feedback-query-form">
<div class="form-group">
<label for="feedbackNo">留言编号:<span class="required">*</span></label>
<input
type="text"
id="feedbackNo"
v-model="feedbackQueryForm.feedbackNo"
placeholder="请输入留言编号"
/>
</div>
<div class="form-group">
<label for="contactInfoType">联系方式类型:</label>
<select id="contactInfoType" v-model="feedbackQueryForm.contactInfoType">
<option :value="1">手机号</option>
<option :value="2">身份证号</option>
<option :value="3">姓名</option>
</select>
</div>
<div class="form-group" v-if="feedbackQueryForm.contactInfoType === 1">
<label for="phoneNumber">手机号: <span class="required">*</span></label>
<input
type="text"
id="phoneNumber"
v-model="feedbackQueryForm.phoneNumber"
placeholder="请输入手机号"
/>
</div>
<div class="form-group" v-if="feedbackQueryForm.contactInfoType === 2">
<label for="idCardNumber">身份证号: <span class="required">*</span></label>
<input
type="text"
id="idCardNumber"
v-model="feedbackQueryForm.idCardNumber"
placeholder="请输入身份证号"
/>
</div>
<div class="form-group" v-if="feedbackQueryForm.contactInfoType === 3">
<label for="userName">姓名: <span class="required">*</span></label>
<input
type="text"
id="userName"
v-model="feedbackQueryForm.userName"
placeholder="请输入您的姓名"
/>
</div>
<button @click="queryFeedback" :disabled="isQuerying" class="submit-btn">
{{ isQuerying ? '查询中...' : '查询留言' }}
</button>
<p v-if="queryErrorMessage" class="error-message">{{ queryErrorMessage }}</p>
</div>
<div v-if="feedbackQueryResult" class="query-result-section">
<h4>留言回复详情</h4>
<div class="result-item">
<strong>留言编号:</strong> {{ feedbackQueryResult.feedbackNo }}
</div>
<div class="result-item">
<strong>留言标题:</strong> {{ feedbackQueryResult.legalArticle }}
</div>
<div class="result-item">
<strong>问题描述:</strong> {{ feedbackQueryResult.issueDescription }}
</div>
<div class="result-item">
<strong>留言时间:</strong> {{ formatDate(feedbackQueryResult.createTime) }}
</div>
<div class="result-item">
<strong>留言状态:</strong>
<span
:class="{
'status-pending':
getFeedbackStatus(feedbackQueryResult.adminReplyContent) === '待处理',
'status-replied':
getFeedbackStatus(feedbackQueryResult.adminReplyContent) === '已回复',
}"
>
{{ getFeedbackStatus(feedbackQueryResult.adminReplyContent) }}
</span>
</div>
<div v-if="feedbackQueryResult.adminReplyContent" class="result-item">
<strong>回复内容:</strong>
<div
class="admin-reply-content"
v-html="removePTags(feedbackQueryResult.adminReplyContent)"
></div>
</div>
<div v-if="feedbackQueryResult.adminReplyTime" class="result-item">
<strong>回复时间:</strong> {{ formatDate(feedbackQueryResult.adminReplyTime) }}
</div>
</div>
</div>
<div class="qr-code-side-card">
<h4>扫描二维码</h4>
<p class="section-description">在手机上扫描此二维码快速打开当前查询页面</p>
<QRCodeDisplay
v-if="currentUrl"
:url="currentUrl"
title="留言查询页面"
id="feedback-query-page-qr"
/>
<p v-else class="qr-code-loading-hint">正在生成二维码...</p>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.feedback-query-container {
min-height: 100vh;
background-color: var(--color-background);
display: flex;
flex-direction: column;
width: 100%;
position: relative;
}
.content {
padding: 2rem 0;
flex: 1;
background-color: var(--color-background);
width: 100%;
}
.container.query-layout {
display: flex;
gap: 2rem;
align-items: flex-start;
padding: 0 1rem;
}
.query-card {
flex: 2;
background-color: white;
border-radius: 8px;
box-shadow: var(--shadow-md);
padding: 2.5rem;
overflow: hidden;
margin: 0;
min-height: 400px;
}
.qr-code-side-card {
flex: 1;
background-color: white;
border-radius: 8px;
box-shadow: var(--shadow-md);
padding: 1.5rem;
margin-top: 0;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
min-width: 250px;
max-width: 400px;
}
.qr-code-side-card h4 {
font-size: 1.3rem;
color: var(--color-primary);
margin-bottom: 1rem;
padding-bottom: 0.8rem;
border-bottom: 1px dashed var(--color-border-light);
width: 100%;
}
.qr-code-side-card .section-description {
color: var(--color-text-light);
margin-bottom: 1.5rem;
line-height: 1.6;
text-align: left;
}
.qr-code-display-wrapper {
margin-top: 80px;
width: 180px;
height: 180px;
margin-bottom: 1rem;
display: flex;
justify-content: center;
align-items: center;
}
.qr-code-loading-hint {
color: var(--color-text-muted);
font-size: 0.9em;
text-align: center;
}
.query-card h3 {
font-size: 1.5rem;
color: var(--color-primary);
margin-bottom: 1.5rem;
display: flex;
align-items: center;
}
.query-card .icon-search::before {
content: '🔍';
margin-right: 10px;
font-size: 1.8rem;
}
.query-card .section-description {
color: var(--color-text-light);
margin-bottom: 1.5rem;
line-height: 1.6;
}
.feedback-query-form .form-group {
margin-bottom: 1.5rem;
}
.feedback-query-form label {
display: block;
margin-bottom: 0.6rem;
font-weight: 500;
color: var(--color-text);
}
.feedback-query-form input[type='text'],
.feedback-query-form select {
width: 100%;
padding: 0.8rem 12px;
border: 1px solid var(--color-border);
border-radius: 6px;
font-size: 1rem;
box-sizing: border-box;
transition:
border-color var(--transition-fast),
box-shadow var(--transition-fast);
}
.feedback-query-form input[type='text']:focus,
.feedback-query-form select:focus {
border-color: var(--color-primary);
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.25);
outline: none;
}
.feedback-query-form .required {
color: var(--color-danger);
margin-left: 5px;
}
.feedback-query-form .submit-btn {
display: inline-block;
padding: 0.8rem 2.5rem;
background-color: var(--color-primary);
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 1.05rem;
transition:
background-color 0.3s ease,
transform 0.2s ease,
box-shadow 0.3s ease;
margin-top: 1.5rem;
font-weight: 600;
box-shadow: var(--shadow-sm);
}
.feedback-query-form .submit-btn:hover {
background-color: var(--color-primary-dark);
transform: translateY(-1px);
box-shadow: var(--shadow-md);
}
.feedback-query-form .submit-btn:active {
transform: translateY(0);
box-shadow: var(--shadow-sm);
}
.feedback-query-form .submit-btn:disabled {
background-color: #cccccc;
cursor: not-allowed;
box-shadow: none;
}
.error-message {
color: var(--color-danger);
margin-top: 1rem;
font-size: 0.9rem;
background-color: #ffebe8;
border: 1px solid #ffccb8;
padding: 0.8rem;
border-radius: 4px;
}
.query-result-section {
background-color: #e9f7fe;
border: 1px solid #b3e0ff;
border-radius: 8px;
padding: 1.5rem;
margin-top: 2rem;
box-shadow: var(--shadow-sm);
}
.query-result-section h4 {
color: #007bff;
margin-bottom: 1rem;
font-size: 1.3rem;
padding-bottom: 0.8rem;
border-bottom: 1px dashed #cceeff;
}
.query-result-section .result-item {
margin-bottom: 0.8rem;
color: var(--color-text);
font-size: 0.95rem;
}
.query-result-section .result-item strong {
color: #333;
}
.query-result-section .admin-reply-content {
background-color: #ffffff;
border: 1px solid var(--color-border-light);
border-radius: 4px;
padding: 1rem;
margin-top: 0.5rem;
line-height: 1.6;
color: #555;
white-space: pre-wrap;
word-break: break-word;
}
.status-pending {
color: orange;
font-weight: bold;
}
.status-replied {
color: green;
font-weight: bold;
}
@media (max-width: 992px) {
.container.query-layout {
flex-direction: column;
align-items: center;
padding: 2rem 1rem;
}
.query-card,
.qr-code-side-card {
width: 100%;
max-width: 600px;
padding: 2rem;
}
.qr-code-side-card {
margin-top: 2rem;
}
}
@media (max-width: 576px) {
.query-card {
padding: 1.5rem;
}
.query-card h3 {
font-size: 1.3rem;
}
.query-card .icon-search::before {
font-size: 1.5rem;
}
.feedback-query-form input[type='text'],
.feedback-query-form select {
padding: 0.7rem 10px;
font-size: 0.9rem;
}
.feedback-query-form .submit-btn {
padding: 0.7rem 2rem;
font-size: 0.95rem;
}
.query-result-section h4 {
font-size: 1.1rem;
}
.query-result-section .result-item {
font-size: 0.85rem;
}
}
</style>

View File

@ -5,107 +5,107 @@ import TheNavbar from '../components/TheNavbar.vue'
import Pagination from '../components/Pagination.vue'
interface PageItem {
id: number;
formatId: string;
title: string;
pageUrl?: string;
createTime: string;
viewCount: number;
author?: string;
multiAttachments?: string;
attachmentUrl?: string;
id: number
formatId: string
title: string
pageUrl?: string
createTime: string
viewCount: number
author?: string
multiAttachments?: string
attachmentUrl?: string
}
const formList = ref<PageItem[]>([]);
const loading = ref(true);
const error = ref('');
const total = ref(0);
const currentPage = ref(1);
const pageSize = ref(15);
const formList = ref<PageItem[]>([])
const loading = ref(true)
const error = ref('')
const total = ref(0)
const currentPage = ref(1)
const pageSize = ref(15)
const fetchForms = async () => {
try {
loading.value = true;
error.value = '';
const result = await getPageList('form', currentPage.value, pageSize.value);
loading.value = true
error.value = ''
const result = await getPageList('form', currentPage.value, pageSize.value)
if (result.code === 200) {
formList.value = result.rows || [];
total.value = result.total || formList.value.length;
formList.value = result.rows || []
total.value = result.total || formList.value.length
//
if (formList.value.length === 0 && currentPage.value > 1) {
currentPage.value = currentPage.value - 1;
await fetchForms();
currentPage.value = currentPage.value - 1
await fetchForms()
}
} else {
error.value = result.msg || '获取数据失败';
formList.value = [];
error.value = result.msg || '获取数据失败'
formList.value = []
}
} catch (e) {
console.error(e);
error.value = '获取数据失败,请稍后重试';
formList.value = [];
console.error(e)
error.value = '获取数据失败,请稍后重试'
formList.value = []
} finally {
loading.value = false;
loading.value = false
}
};
}
//
watch([currentPage, pageSize], () => {
fetchForms();
});
fetchForms()
})
//
const handlePageChange = (page: number, size: number) => {
currentPage.value = page;
pageSize.value = size;
};
currentPage.value = page
pageSize.value = size
}
onMounted(() => {
fetchForms();
});
fetchForms()
})
//
onActivated(() => {
console.log('表单下载列表页面被激活,重新获取数据');
fetchForms();
});
console.log('表单下载列表页面被激活,重新获取数据')
fetchForms()
})
//
function hasAttachments(item: PageItem): boolean {
//
if (item.attachmentUrl) return true;
if (item.attachmentUrl) return true
//
if (!item.multiAttachments) return false;
if (!item.multiAttachments) return false
try {
const attachments = JSON.parse(item.multiAttachments);
return Array.isArray(attachments) && attachments.length > 0;
const attachments = JSON.parse(item.multiAttachments)
return Array.isArray(attachments) && attachments.length > 0
} catch (e) {
console.error('解析附件列表失败:', e);
return false;
console.error('解析附件列表失败:', e)
return false
}
}
//
const formatDate = (dateStr: string | null | undefined): string => {
if (!dateStr) return '未知日期';
if (!dateStr) return '未知日期'
try {
const date = new Date(dateStr);
const date = new Date(dateStr)
if (isNaN(date.getTime())) {
return '未知日期';
return '未知日期'
}
return date.toLocaleDateString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
});
return date.toLocaleDateString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
})
} catch (e) {
console.error('日期格式化错误:', e);
return '未知日期';
console.error('日期格式化错误:', e)
return '未知日期'
}
}
</script>
@ -113,14 +113,14 @@ const formatDate = (dateStr: string | null | undefined): string => {
<template>
<div class="form-list">
<TheNavbar />
<div class="page-header">
<div class="container">
<h1>表单下载</h1>
<p class="subtitle">下载各类表单模板便捷办理相关业务</p>
</div>
</div>
<div class="container">
<div class="content-wrapper">
<div class="forms-section">
@ -128,24 +128,24 @@ const formatDate = (dateStr: string | null | undefined): string => {
<div class="spinner"></div>
<p>加载中...</p>
</div>
<div v-else-if="error" class="error">
<h3>内容加载失败</h3>
<p>{{ error }}</p>
<button @click="fetchForms" class="btn-retry">重试</button>
</div>
<div v-else-if="formList.length === 0" class="empty">
<h3>暂无内容</h3>
<p>当前没有表单下载相关内容</p>
<router-link to="/" class="btn-home">返回首页</router-link>
</div>
<div v-else class="list-content">
<div class="form-grid">
<div v-for="item in formList" :key="item.formatId" class="form-card">
<router-link :to="`/table.html?Id=${item.formatId}`" class="form-link">
<div class="form-icon" :class="{'has-download': hasAttachments(item)}"></div>
<div class="form-icon" :class="{ 'has-download': hasAttachments(item) }"></div>
<div class="form-content">
<h3 class="form-title">{{ item.title }}</h3>
<div class="form-meta">
@ -160,14 +160,14 @@ const formatDate = (dateStr: string | null | undefined): string => {
</router-link>
</div>
</div>
<!-- 分页组件 -->
<Pagination
<Pagination
:total="total"
v-model:current-page="currentPage"
v-model:page-size="pageSize"
@change="handlePageChange"
:page-sizes="[8, 16, 24, 32]"
:page-sizes="[8, 16, 24, 32, 150]"
/>
</div>
</div>
@ -252,7 +252,9 @@ const formatDate = (dateStr: string | null | undefined): string => {
width: 100%;
}
.loading, .error, .empty {
.loading,
.error,
.empty {
display: flex;
flex-direction: column;
align-items: center;
@ -272,10 +274,13 @@ const formatDate = (dateStr: string | null | undefined): string => {
}
@keyframes spin {
to { transform: rotate(360deg); }
to {
transform: rotate(360deg);
}
}
.btn-retry, .btn-home {
.btn-retry,
.btn-home {
background-color: var(--color-primary);
color: white;
border: none;
@ -290,7 +295,8 @@ const formatDate = (dateStr: string | null | undefined): string => {
display: inline-block;
}
.btn-retry:hover, .btn-home:hover {
.btn-retry:hover,
.btn-home:hover {
background-color: var(--color-primary-dark);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 123, 255, 0.3);
@ -376,13 +382,15 @@ const formatDate = (dateStr: string | null | undefined): string => {
gap: 0.5rem;
}
.form-date, .form-views {
.form-date,
.form-views {
display: flex;
align-items: center;
gap: 0.5rem;
}
.form-date::before, .form-views::before {
.form-date::before,
.form-views::before {
content: '';
display: inline-block;
width: 16px;
@ -449,16 +457,16 @@ const formatDate = (dateStr: string | null | undefined): string => {
.page-header {
padding: 2.5rem 0;
}
.page-header h1 {
font-size: 2.2rem;
}
.form-grid {
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 1rem;
}
.forms-section {
padding: 1.5rem;
}
@ -468,26 +476,26 @@ const formatDate = (dateStr: string | null | undefined): string => {
.page-header {
padding: 2rem 0;
}
.page-header h1 {
font-size: 1.8rem;
}
.subtitle {
font-size: 1rem;
}
.form-grid {
grid-template-columns: 1fr;
}
.forms-section {
padding: 1rem;
border-radius: 8px;
}
.form-link {
padding: 1.2rem;
}
}
</style>
</style>

File diff suppressed because it is too large Load Diff

View File

@ -5,118 +5,118 @@ import TheNavbar from '../components/TheNavbar.vue'
import Pagination from '../components/Pagination.vue'
interface PageItem {
id: number;
formatId: string;
title: string;
content: string;
pageType: string;
pageUrl?: string;
createTime: string;
updateTime: string;
status: number;
sortOrder: number;
viewCount: number;
author?: string;
keywords?: string;
description?: string;
attachmentUrl?: string;
id: number
formatId: string
title: string
content: string
pageType: string
pageUrl?: string
createTime: string
updateTime: string
status: number
sortOrder: number
viewCount: number
author?: string
keywords?: string
description?: string
attachmentUrl?: string
}
interface PageData {
code: number;
rows: PageItem[];
total: number;
msg: string;
code: number
rows: PageItem[]
total: number
msg: string
}
const laws = ref<PageItem[]>([]);
const loading = ref(true);
const error = ref('');
const total = ref(0);
const currentPage = ref(1);
const pageSize = ref(15);
const laws = ref<PageItem[]>([])
const loading = ref(true)
const error = ref('')
const total = ref(0)
const currentPage = ref(1)
const pageSize = ref(15)
const fetchLaws = async () => {
try {
loading.value = true;
error.value = '';
const response = await getPageList('law', currentPage.value, pageSize.value);
loading.value = true
error.value = ''
const response = await getPageList('law', currentPage.value, pageSize.value)
if (response.code === 200) {
laws.value = response.rows || [];
total.value = response.total || laws.value.length;
laws.value = response.rows || []
total.value = response.total || laws.value.length
//
if (laws.value.length === 0 && currentPage.value > 1) {
currentPage.value = currentPage.value - 1;
await fetchLaws();
currentPage.value = currentPage.value - 1
await fetchLaws()
}
} else {
error.value = response.msg || '获取数据失败';
laws.value = [];
error.value = response.msg || '获取数据失败'
laws.value = []
}
} catch (err) {
console.error('获取法律规定列表失败:', err);
error.value = '获取数据失败,请稍后重试';
laws.value = [];
console.error('获取法律规定列表失败:', err)
error.value = '获取数据失败,请稍后重试'
laws.value = []
} finally {
loading.value = false;
loading.value = false
}
};
}
//
watch([currentPage, pageSize], () => {
fetchLaws();
});
fetchLaws()
})
//
const handlePageChange = (page: number, size: number) => {
currentPage.value = page;
pageSize.value = size;
};
currentPage.value = page
pageSize.value = size
}
//
const formatDate = (dateStr: string | null | undefined): string => {
if (!dateStr) return '未知日期';
if (!dateStr) return '未知日期'
try {
const date = new Date(dateStr);
const date = new Date(dateStr)
if (isNaN(date.getTime())) {
return '未知日期';
return '未知日期'
}
return date.toLocaleDateString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
});
return date.toLocaleDateString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
})
} catch (e) {
console.error('日期格式化错误:', e);
return '未知日期';
console.error('日期格式化错误:', e)
return '未知日期'
}
}
onMounted(() => {
fetchLaws();
});
fetchLaws()
})
//
onActivated(() => {
console.log('法律规定列表页面被激活,重新获取数据');
fetchLaws();
});
console.log('法律规定列表页面被激活,重新获取数据')
fetchLaws()
})
</script>
<template>
<div class="law-list-container">
<TheNavbar />
<div class="page-header">
<div class="container">
<h1>法律规定</h1>
<p class="subtitle">查看并了解最新的法律法规内容</p>
</div>
</div>
<div class="container">
<div class="content-wrapper">
<div class="law-list">
@ -124,19 +124,19 @@ onActivated(() => {
<div class="spinner"></div>
<p>加载中...</p>
</div>
<div v-else-if="error" class="error">
<h3>获取数据失败</h3>
<p>{{ error }}</p>
<button @click="fetchLaws" class="btn-retry">重试</button>
</div>
<div v-else-if="laws.length === 0" class="empty">
<h3>暂无内容</h3>
<p>当前没有法律规定相关内容</p>
<router-link to="/" class="btn-home">返回首页</router-link>
</div>
<div v-else class="list-content">
<div class="law-grid">
<div v-for="law in laws" :key="law.id" class="law-card">
@ -155,14 +155,14 @@ onActivated(() => {
</router-link>
</div>
</div>
<!-- 分页组件 -->
<Pagination
<Pagination
:total="total"
v-model:current-page="currentPage"
v-model:page-size="pageSize"
@change="handlePageChange"
:page-sizes="[8, 16, 24, 32]"
:page-sizes="[8, 16, 24, 32, 150]"
/>
</div>
</div>
@ -247,7 +247,9 @@ onActivated(() => {
width: 100%;
}
.loading, .error, .empty {
.loading,
.error,
.empty {
display: flex;
flex-direction: column;
align-items: center;
@ -267,10 +269,13 @@ onActivated(() => {
}
@keyframes spin {
to { transform: rotate(360deg); }
to {
transform: rotate(360deg);
}
}
.btn-retry, .btn-home {
.btn-retry,
.btn-home {
background-color: var(--color-primary);
color: white;
border: none;
@ -285,7 +290,8 @@ onActivated(() => {
display: inline-block;
}
.btn-retry:hover, .btn-home:hover {
.btn-retry:hover,
.btn-home:hover {
background-color: var(--color-primary-dark);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 123, 255, 0.3);
@ -363,13 +369,15 @@ onActivated(() => {
gap: 0.5rem;
}
.law-date, .law-views {
.law-date,
.law-views {
display: flex;
align-items: center;
gap: 0.5rem;
}
.law-date::before, .law-views::before {
.law-date::before,
.law-views::before {
content: '';
display: inline-block;
width: 16px;
@ -416,16 +424,16 @@ onActivated(() => {
.page-header {
padding: 2.5rem 0;
}
.page-header h1 {
font-size: 2.2rem;
}
.law-grid {
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 1rem;
}
.law-list {
padding: 1.5rem;
}
@ -435,26 +443,26 @@ onActivated(() => {
.page-header {
padding: 2rem 0;
}
.page-header h1 {
font-size: 1.8rem;
}
.subtitle {
font-size: 1rem;
}
.law-grid {
grid-template-columns: 1fr;
}
.law-list {
padding: 1rem;
border-radius: 8px;
}
.law-link {
padding: 1.2rem;
}
}
</style>
</style>

View File

@ -1,139 +1,310 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ref, onMounted, computed, reactive } from 'vue'
import { useRoute } from 'vue-router'
import { getPageDetail, updateViewCount } from '../services/api'
import {
getPageDetail,
updateViewCount,
addFeedback,
listFeedback,
queryFeedbackDetail,
} from '../services/api'
import TheNavbar from '../components/TheNavbar.vue'
const route = useRoute()
const loading = ref(true)
const error = ref(false)
const page = ref<any>(null)
const expandedCards = ref<Set<number>>(new Set())
const cardCount = ref(0)
const feedbackForm = reactive({
legalArticle: '',
issueDescription: '',
userName: '',
phoneNumber: '',
idCardNumber: '',
contactInfoType: 1, //
})
const feedbackSubmitSuccess = ref(false)
const feedbackNoReturned = ref('')
const submitFeedback = async () => {
try {
//
if (!feedbackForm.legalArticle.trim()) {
alert('有问题的法律条例或案例不能为空。')
return
}
if (!feedbackForm.issueDescription.trim()) {
alert('问题描述不能为空。')
return
}
if (feedbackForm.contactInfoType === 1 && !feedbackForm.phoneNumber.trim()) {
alert('手机号不能为空。')
return
} else if (feedbackForm.contactInfoType === 2 && !feedbackForm.idCardNumber.trim()) {
alert('身份证号不能为空。')
return
} else if (feedbackForm.contactInfoType === 3 && !feedbackForm.userName.trim()) {
alert('姓名不能为空。')
return
}
const dataToSend = { ...feedbackForm }
//
if (feedbackForm.contactInfoType === 1) {
dataToSend.idCardNumber = ''
dataToSend.userName = ''
} else if (feedbackForm.contactInfoType === 2) {
dataToSend.phoneNumber = ''
dataToSend.userName = ''
} else if (feedbackForm.contactInfoType === 3) {
dataToSend.phoneNumber = ''
dataToSend.idCardNumber = ''
}
const res = await addFeedback(dataToSend)
if (res && res.code === 200) {
feedbackSubmitSuccess.value = true
feedbackNoReturned.value = res.data?.feedbackNo || '未知'
//
feedbackForm.legalArticle = ''
feedbackForm.issueDescription = ''
feedbackForm.userName = ''
feedbackForm.phoneNumber = ''
feedbackForm.idCardNumber = ''
feedbackForm.contactInfoType = 1
console.log('留言提交成功,查询编号:', feedbackNoReturned.value)
} else {
alert('提交留言失败:' + (res.msg || '未知错误'))
}
} catch (e) {
console.error('提交留言异常:', e)
alert('提交留言异常,请稍后再试')
}
}
// ##
const processedContent = computed(() => {
if (!page.value || !page.value.content) return ''
// ##
const regex = /#([^#]+)#([\s\S]*?)(?=#[^#]+#|$)/g
let index = 0
// HTML使onclick
const result = page.value.content.replace(
regex,
(match: string, title: string, content: string) => {
//
title = title.trim()
content = content.trim()
const currentIndex = index++
// IDexpandedCards使
expandedCards.value.add(currentIndex)
return `
<div class="law-card expanded" data-card-index="${currentIndex}">
<div class="law-card-header" data-card-index="${currentIndex}">
<span class="law-card-title">${title}</span>
<span class="law-card-toggle"></span>
</div>
<div class="law-card-content">
${content}
</div>
</div>
`
},
)
// 使
cardCount.value = index
return result
})
//
const toggleCard = (index: number) => {
const card = document.querySelector(`.law-card[data-card-index="${index}"]`)
if (card) {
if (expandedCards.value.has(index)) {
expandedCards.value.delete(index)
card.classList.remove('expanded')
} else {
expandedCards.value.add(index)
card.classList.add('expanded')
}
}
}
onMounted(async () => {
try {
const formatId = route.query.Id as string
if (!formatId) {
console.error('缺少必要的formatId参数');
error.value = true;
loading.value = false;
return;
console.error('缺少必要的formatId参数')
error.value = true
loading.value = false
return
}
console.log('开始获取法律规定详情formatId:', formatId);
console.log('开始获取法律规定详情formatId:', formatId)
//
const result = await getPageDetail('law', formatId);
console.log('法律规定详情请求结果:', result);
const result = await getPageDetail('law', formatId)
console.log('法律规定详情请求结果:', result)
if (result && result.code === 200 && result.data) {
page.value = result.data;
console.log('成功获取法律规定详情:', page.value);
page.value = result.data
console.log('成功获取法律规定详情:', page.value)
//
try {
const viewResult = await updateViewCount('law', formatId) as any;
console.log('更新浏览量结果:', viewResult);
const viewResult = (await updateViewCount('law', formatId)) as any
console.log('更新浏览量结果:', viewResult)
//
if (viewResult && viewResult.code === 200) {
//
if (viewResult.data && typeof viewResult.data.viewCount === 'number') {
page.value.viewCount = viewResult.data.viewCount;
}
//
else if (typeof page.value.viewCount === 'number' || typeof page.value.viewCount === 'string') {
page.value.viewCount = Number(page.value.viewCount || 0) + 1;
} else {
page.value.viewCount = 1; //
page.value.viewCount = viewResult.data.viewCount
}
console.log('浏览量更新为:', page.value.viewCount);
//
else if (
typeof page.value.viewCount === 'number' ||
typeof page.value.viewCount === 'string'
) {
page.value.viewCount = Number(page.value.viewCount || 0) + 1
} else {
page.value.viewCount = 1 //
}
console.log('浏览量更新为:', page.value.viewCount)
} else {
console.warn('浏览量更新API返回错误:', viewResult?.msg || '未知错误');
console.warn('浏览量更新API返回错误:', viewResult?.msg || '未知错误')
}
} catch (e) {
console.error('更新浏览量失败:', e);
console.error('更新浏览量失败:', e)
//
if (typeof page.value.viewCount === 'number' || typeof page.value.viewCount === 'string') {
page.value.viewCount = Number(page.value.viewCount || 0) + 1;
console.log('API出错本地更新浏览量为:', page.value.viewCount);
page.value.viewCount = Number(page.value.viewCount || 0) + 1
console.log('API出错本地更新浏览量为:', page.value.viewCount)
}
}
//
if (page.value.pageType && page.value.pageType !== 'law') {
console.error(`错误: 请求的是法律规定(law),但返回的是${page.value.pageType}类型的内容`);
console.error(`错误: 请求的是法律规定(law),但返回的是${page.value.pageType}类型的内容`)
//
error.value = true;
loading.value = false;
return;
error.value = true
loading.value = false
return
}
//
if (page.value.title && /\\u|%/.test(page.value.title)) {
try {
page.value.title = decodeURIComponent(page.value.title);
page.value.title = decodeURIComponent(page.value.title)
} catch (e) {
console.error('标题解码失败:', e);
console.error('标题解码失败:', e)
}
}
// HTML
if (page.value.content) {
console.log('内容类型:', typeof page.value.content);
console.log('内容前50个字符:', page.value.content.substring(0, 50));
console.log('内容类型:', typeof page.value.content)
console.log('内容前50个字符:', page.value.content.substring(0, 50))
//
if (/\\u|%/.test(page.value.content)) {
try {
page.value.content = decodeURIComponent(page.value.content);
page.value.content = decodeURIComponent(page.value.content)
} catch (e) {
console.error('内容解码失败:', e);
console.error('内容解码失败:', e)
}
}
// HTML
if (typeof page.value.content !== 'string') {
try {
page.value.content = JSON.stringify(page.value.content);
page.value.content = JSON.stringify(page.value.content)
} catch (e) {
console.error('内容格式转换失败:', e);
console.error('内容格式转换失败:', e)
}
}
} else {
console.warn('页面内容为空');
page.value.content = '<p>暂无内容</p>';
console.warn('页面内容为空')
page.value.content = '<p>暂无内容</p>'
}
//
document.title = `${page.value.title || '法律规定详情'} - 法律规定`;
document.title = `${page.value.title || '法律规定详情'} - 法律规定`
} else {
console.error('获取法律规定详情失败:', result?.msg || '未知错误');
error.value = true;
console.error('获取法律规定详情失败:', result?.msg || '未知错误')
error.value = true
}
} catch (e) {
console.error('获取法律规定详情异常:', e);
error.value = true;
console.error('获取法律规定详情异常:', e)
error.value = true
} finally {
loading.value = false;
loading.value = false
}
})
//
const initCollapsibleCards = () => {
console.log('初始化折叠卡片...')
//
for (let i = 0; i < cardCount.value; i++) {
expandedCards.value.add(i)
const card = document.querySelector(`.law-card[data-card-index="${i}"]`)
if (card) {
card.classList.add('expanded')
}
}
// 使
document.querySelector('.body')?.addEventListener('click', (e) => {
const target = e.target as HTMLElement
const header = target.closest('.law-card-header')
if (header) {
const index = parseInt(header.getAttribute('data-card-index') || '-1')
if (index >= 0) {
toggleCard(index)
console.log('切换卡片状态:', index)
}
}
})
console.log('折叠卡片初始化完成')
}
onMounted(() => {
//
setTimeout(() => {
initCollapsibleCards()
}, 1000)
})
//
const formatDate = (dateStr: string | null | undefined): string => {
if (!dateStr) return '未知日期';
if (!dateStr) return '未知日期'
try {
const date = new Date(dateStr);
const date = new Date(dateStr)
if (isNaN(date.getTime())) {
return '未知日期';
return '未知日期'
}
return date.toLocaleDateString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
});
day: '2-digit',
})
} catch (e) {
console.error('日期格式化错误:', e);
return '未知日期';
console.error('日期格式化错误:', e)
return '未知日期'
}
}
</script>
@ -168,12 +339,100 @@ const formatDate = (dateStr: string | null | undefined): string => {
</div>
</div>
<div class="body" v-html="page.content"></div>
<div class="body" v-html="processedContent"></div>
<div class="footer">
<router-link to="/hasfjlaw" class="btn-more">查看更多法律规定</router-link>
<router-link to="/" class="btn-home">返回首页</router-link>
</div>
<!-- 用户留言模块 -->
<div class="feedback-section">
<h3><i class="icon-chat"></i> 法律咨询与留言</h3>
<div v-if="!feedbackSubmitSuccess" class="feedback-form">
<p class="section-description">如果您对法律规定有任何疑问或建议欢迎在此留言</p>
<div class="form-group">
<label for="legalArticle"
>有问题的法律条例或案例:<span class="required">*</span></label
>
<input
type="text"
id="legalArticle"
v-model="feedbackForm.legalArticle"
placeholder="如:公司法第十五条"
/>
</div>
<div class="form-group">
<label for="issueDescription">问题描述: <span class="required">*</span></label>
<textarea
id="issueDescription"
v-model="feedbackForm.issueDescription"
rows="5"
placeholder="请详细描述您的问题字数在10-500字之间"
required
></textarea>
</div>
<div class="form-group">
<label for="contactInfoType">联系方式类型:</label>
<select id="contactInfoType" v-model="feedbackForm.contactInfoType">
<option :value="1">手机号</option>
<option :value="2">身份证号</option>
<option :value="3">姓名</option>
</select>
</div>
<div class="form-group">
<label for="phoneNumber"
>手机号:
<span class="required" v-show="feedbackForm.contactInfoType === 1">*</span></label
>
<input
type="text"
id="phoneNumber"
v-model="feedbackForm.phoneNumber"
placeholder="请输入手机号"
:required="feedbackForm.contactInfoType === 1"
/>
</div>
<div class="form-group">
<label for="idCardNumber"
>身份证号:
<span class="required" v-show="feedbackForm.contactInfoType === 2">*</span></label
>
<input
type="text"
id="idCardNumber"
v-model="feedbackForm.idCardNumber"
placeholder="请输入身份证号"
:required="feedbackForm.contactInfoType === 2"
/>
</div>
<div class="form-group">
<label for="userNameContact"
>姓名:
<span class="required" v-show="feedbackForm.contactInfoType === 3">*</span></label
>
<input
type="text"
id="userNameContact"
v-model="feedbackForm.userName"
placeholder="请输入您的姓名"
:required="feedbackForm.contactInfoType === 3"
/>
</div>
<button @click="submitFeedback" class="submit-btn">提交留言</button>
</div>
<div v-else class="feedback-success">
<h4>留言提交成功</h4>
<p>
您的留言查询编号是<span class="feedback-no">{{ feedbackNoReturned }}</span>
</p>
<p>请牢记此编号以便查询回复</p>
<button
@click="((feedbackSubmitSuccess = false), (feedbackNoReturned = ''))"
class="submit-btn"
>
继续留言
</button>
</div>
</div>
</div>
</div>
</div>
@ -210,7 +469,9 @@ const formatDate = (dateStr: string | null | undefined): string => {
}
@keyframes spin {
to { transform: rotate(360deg); }
to {
transform: rotate(360deg);
}
}
.error {
@ -295,6 +556,11 @@ const formatDate = (dateStr: string | null | undefined): string => {
margin-right: 5px;
}
.icon-user::before {
content: '👤';
margin-right: 5px;
}
.content .body {
line-height: 1.8;
color: var(--color-text);
@ -340,6 +606,61 @@ const formatDate = (dateStr: string | null | undefined): string => {
background-color: #f8f9fa;
}
/* 添加法律卡片相关样式 */
:deep(.law-card) {
margin: 1.5rem 0;
border: 1px solid #e0e0e0;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
background-color: #f9f9f9;
}
:deep(.law-card-header) {
padding: 1rem 1.5rem;
background-color: #f0f0f0;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
transition: background-color 0.3s;
user-select: none; /* 防止选中文本 */
}
:deep(.law-card-header:hover) {
background-color: #e0e0e0;
}
:deep(.law-card-title) {
font-weight: bold;
color: #333;
font-size: 1.1rem;
}
:deep(.law-card-toggle) {
transition: transform 0.3s;
transform: rotate(180deg); /* 默认显示为向上箭头,表示展开状态 */
}
:deep(.law-card-content) {
padding: 1.5rem; /* 默认有内边距 */
max-height: 2000px; /* 默认展开高度 */
overflow: hidden;
transition:
max-height 0.5s ease,
padding 0.5s ease;
}
/* 非展开状态的样式 */
:deep(.law-card:not(.expanded) .law-card-content) {
padding: 0;
max-height: 0;
}
:deep(.law-card:not(.expanded) .law-card-toggle) {
transform: rotate(0deg);
}
.content .footer {
margin-top: 3rem;
padding-top: 1.5rem;
@ -350,18 +671,22 @@ const formatDate = (dateStr: string | null | undefined): string => {
flex-wrap: wrap;
}
.btn-home, .btn-more {
.btn-home,
.btn-more {
display: inline-block;
padding: 0.6rem 1.5rem;
background-color: var(--color-primary);
color: white;
text-decoration: none;
border-radius: 4px;
transition: background-color 0.3s, transform 0.3s;
transition:
background-color 0.3s,
transform 0.3s;
font-size: 0.9rem;
}
.btn-home:hover, .btn-more:hover {
.btn-home:hover,
.btn-more:hover {
background-color: #0069d9;
transform: translateY(-2px);
}
@ -390,6 +715,10 @@ const formatDate = (dateStr: string | null | undefined): string => {
.content .body {
font-size: 0.95rem;
}
:deep(.law-card-title) {
font-size: 1rem;
}
}
@media (max-width: 576px) {
@ -426,10 +755,23 @@ const formatDate = (dateStr: string | null | undefined): string => {
padding-top: 1rem;
}
.btn-home, .btn-more {
.btn-home,
.btn-more {
padding: 0.5rem 1.2rem;
font-size: 0.85rem;
}
:deep(.law-card-header) {
padding: 0.8rem 1rem;
}
:deep(.law-card-title) {
font-size: 0.9rem;
}
:deep(.law-card.expanded .law-card-content) {
padding: 1rem;
}
}
@media (min-width: 1200px) {
@ -446,4 +788,258 @@ const formatDate = (dateStr: string | null | undefined): string => {
padding: 0;
}
}
:deep(.law-card.expanded .law-card-content) {
padding: 1.5rem;
max-height: 2000px;
}
:deep(.law-card.expanded .law-card-toggle) {
transform: rotate(180deg);
}
.feedback-section {
background-color: #f9f9f9;
border-radius: 8px;
margin-top: 2rem;
padding: 2rem;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
}
.feedback-section h3 {
font-size: 1.5rem;
color: var(--color-primary);
margin-bottom: 1.5rem;
display: flex;
align-items: center;
}
.feedback-section .icon-chat::before {
content: '💬'; /* Chat bubble icon */
margin-right: 10px;
font-size: 1.8rem;
}
.feedback-section .icon-search::before {
content: '🔍'; /* Search icon */
margin-right: 10px;
font-size: 1.8rem;
}
.feedback-section .section-description {
color: var(--color-text-light);
margin-bottom: 1.5rem;
line-height: 1.6;
}
.feedback-form .form-group,
.feedback-query .form-group {
margin-bottom: 1rem;
}
.feedback-form label,
.feedback-query label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: var(--color-text);
}
.feedback-form input[type='text'],
.feedback-form textarea,
.feedback-form select,
.feedback-query input[type='text'],
.feedback-query select {
width: calc(100% - 20px);
padding: 0.8rem 10px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 1rem;
box-sizing: border-box;
}
.feedback-form textarea {
resize: vertical;
}
.feedback-form .required,
.feedback-query .required {
color: var(--color-danger);
margin-left: 5px;
}
.feedback-form .submit-btn,
.feedback-query .submit-btn {
display: inline-block;
padding: 0.8rem 2rem;
background-color: var(--color-primary);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
transition: background-color 0.3s ease;
margin-top: 1rem;
}
.feedback-form .submit-btn:hover,
.feedback-query .submit-btn:hover {
background-color: #0069d9;
}
.feedback-query .submit-btn:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
.feedback-success {
text-align: center;
padding: 2rem;
background-color: #e6ffe6;
border: 1px solid #aaffaa;
border-radius: 8px;
color: #336633;
}
.feedback-success h4 {
color: #28a745;
margin-bottom: 1rem;
font-size: 1.3rem;
}
.feedback-success .feedback-no {
font-weight: bold;
color: var(--color-primary);
font-size: 1.5rem;
}
.feedback-success button {
margin-top: 1.5rem;
padding: 0.7rem 1.5rem;
background-color: var(--color-success);
border: none;
border-radius: 4px;
color: white;
cursor: pointer;
transition: background-color 0.3s;
}
.feedback-success button:hover {
background-color: #218838;
}
.query-title {
margin-top: 3rem;
border-top: 1px dashed #eee;
padding-top: 2rem;
}
.query-result {
background-color: #e9f7fe;
border: 1px solid #b3e0ff;
border-radius: 8px;
padding: 1.5rem;
margin-top: 2rem;
}
.query-result h4 {
color: #007bff;
margin-bottom: 1rem;
font-size: 1.3rem;
padding-bottom: 0.8rem;
border-bottom: 1px dashed #cceeff;
}
.query-result .result-item {
margin-bottom: 0.8rem;
color: var(--color-text);
font-size: 0.95rem;
}
.query-result .result-item strong {
color: #333;
}
.query-result .admin-reply-content {
background-color: #ffffff;
border: 1px solid #e0e0e0;
border-radius: 4px;
padding: 1rem;
margin-top: 0.5rem;
line-height: 1.6;
color: #555;
}
.error-message {
color: var(--color-danger);
margin-top: 1rem;
font-size: 0.9rem;
}
.status-pending {
color: orange;
font-weight: bold;
}
.status-replied {
color: green;
font-weight: bold;
}
.status-closed {
color: gray;
font-weight: bold;
}
@media (max-width: 768px) {
.feedback-section {
padding: 1.5rem;
}
}
@media (max-width: 576px) {
.feedback-section {
padding: 1rem;
}
.feedback-section h3 {
font-size: 1.3rem;
}
.feedback-section .icon-chat::before,
.feedback-section .icon-search::before {
font-size: 1.5rem;
}
.feedback-form input[type='text'],
.feedback-form textarea,
.feedback-form select,
.feedback-query input[type='text'],
.feedback-query select {
padding: 0.6rem 8px;
font-size: 0.9rem;
}
.feedback-form .submit-btn,
.feedback-query .submit-btn {
padding: 0.6rem 1.5rem;
font-size: 0.9rem;
}
.feedback-success h4 {
font-size: 1.1rem;
}
.feedback-success .feedback-no {
font-size: 1.3rem;
}
.query-result h4 {
font-size: 1.1rem;
}
.query-result .result-item {
font-size: 0.85rem;
}
}
</style>

View File

@ -19,11 +19,13 @@ export default defineConfig({
return () => {
server.middlewares.use((req, res, next) => {
// 如果是前端路由路径直接返回index.html
if (req.url === '/hasfjlaw' || req.url === '/hasfjcase' || req.url === '/hasfjform' || req.url === '/qrcodes') {
const indexHtml = fs.readFileSync(
path.resolve(__dirname, 'index.html'),
'utf-8'
)
if (
req.url === '/hasfjlaw' ||
req.url === '/hasfjcase' ||
req.url === '/hasfjform' ||
req.url === '/qrcodes'
) {
const indexHtml = fs.readFileSync(path.resolve(__dirname, 'index.html'), 'utf-8')
res.statusCode = 200
res.setHeader('Content-Type', 'text/html')
res.end(indexHtml)
@ -32,12 +34,12 @@ export default defineConfig({
next()
})
}
}
}
},
},
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
server: {
@ -46,79 +48,86 @@ export default defineConfig({
proxy: {
// 配置跨域
'/api': {
// target: 'http://127.0.0.1:9799', // 后端服务地址
target: 'http://222.184.49.22:9799', // 后端服务地址
// target: 'http://127.0.0.1:19696', // 后端服务地址
target: 'http://222.184.49.22:19696', // 后端服务地址
changeOrigin: true, // 支持跨域
rewrite: (path) => path.replace(/^\/api/, ''), // 移除/api前缀
configure: (proxy, options) => {
// 添加代理错误处理
proxy.on('error', (err, req, res) => {
console.error('API代理错误:', err);
console.error('API代理错误:', err)
// 返回友好的错误响应
if (!res.headersSent) {
res.writeHead(500, {
'Content-Type': 'application/json'
});
'Content-Type': 'application/json',
})
res.end(JSON.stringify({
code: 500,
msg: '后端服务暂时不可用,请稍后重试',
data: null
}));
res.end(
JSON.stringify({
code: 500,
msg: '后端服务暂时不可用,请稍后重试',
data: null,
}),
)
}
});
}
})
},
},
// 添加司法局后台管理接口代理
'/hasfj': {
// target: 'http://127.0.0.1:9799', // 后端服务地址
target: 'http://222.184.49.22:9799', // 司法局后端服务
// target: 'http://127.0.0.1:19696', // 后端服务地址
target: 'http://222.184.49.22:19696', // 后端服务地址
changeOrigin: true,
rewrite: (path) => path, // 保持路径不变
configure: (proxy, options) => {
// 添加代理错误处理
proxy.on('error', (err, req, res) => {
console.error('后台管理接口代理错误:', err);
console.error('后台管理接口代理错误:', err)
// 返回友好的错误响应
if (!res.headersSent) {
res.writeHead(500, {
'Content-Type': 'application/json'
});
'Content-Type': 'application/json',
})
res.end(JSON.stringify({
code: 500,
msg: '后端服务暂时不可用,请稍后重试',
data: null
}));
res.end(
JSON.stringify({
code: 500,
msg: '后端服务暂时不可用,请稍后重试',
data: null,
}),
)
}
});
}
})
},
},
// 添加文件下载代理,专门处理静态资源文件请求
'/profile': {
target: 'http://222.184.49.22:9799', // 后端静态资源服务地址
// target: 'http://127.0.0.1:19696', // 后端服务地址
target: 'http://222.184.49.22:19696', // 后端静态资源服务地址
changeOrigin: true,
rewrite: (path) => path, // 保持路径不变
configure: (proxy, options) => {
proxy.on('error', (err, req, res) => {
console.error('文件下载代理错误:', err);
console.error('文件下载代理错误:', err)
if (!res.headersSent) {
res.writeHead(500, {
'Content-Type': 'application/json'
});
'Content-Type': 'application/json',
})
res.end(JSON.stringify({
code: 500,
msg: '文件下载失败,请稍后重试',
data: null
}));
res.end(
JSON.stringify({
code: 500,
msg: '文件下载失败,请稍后重试',
data: null,
}),
)
}
});
}
}
}
}
})
},
},
},
},
})