♻️ refactor some code

This commit is contained in:
2025-02-05 18:57:58 +08:00
parent a2e80b9a91
commit b4ef5a4b51
21 changed files with 501 additions and 217 deletions

View File

@@ -1,3 +1,2 @@
node_modules node_modules
dist
*.log *.log

View File

@@ -1,12 +1,12 @@
# 设置基础镜像
FROM nginx:latest FROM nginx:latest
# 设置作者信息
LABEL maintainer="landaiqing <<landaiqing@126.com>>" LABEL maintainer="landaiqing <<landaiqing@126.com>>"
# 设置时区
ENV TimeZone=Asia/Shanghai ENV TimeZone=Asia/Shanghai
# 将dist文件中的内容复制到 /usr/share/nginx/html/ 这个目录下面
COPY dist/ /usr/share/nginx/html/ COPY dist/ /usr/share/nginx/html/
# 用本地的 default.conf 配置来替换nginx镜像里的默认配置
COPY default.conf /etc/nginx/conf.d/default.conf COPY default.conf /etc/nginx/conf.d/default.conf
# 暴露80端口
EXPOSE 80 EXPOSE 80

2
auto-import.d.ts vendored
View File

@@ -150,6 +150,7 @@ declare global {
const useCloned: typeof import('@vueuse/core')['useCloned'] const useCloned: typeof import('@vueuse/core')['useCloned']
const useColorMode: typeof import('@vueuse/core')['useColorMode'] const useColorMode: typeof import('@vueuse/core')['useColorMode']
const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog'] const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog']
const useCountdown: typeof import('@vueuse/core')['useCountdown']
const useCounter: typeof import('@vueuse/core')['useCounter'] const useCounter: typeof import('@vueuse/core')['useCounter']
const useCssModule: typeof import('vue')['useCssModule'] const useCssModule: typeof import('vue')['useCssModule']
const useCssVar: typeof import('@vueuse/core')['useCssVar'] const useCssVar: typeof import('@vueuse/core')['useCssVar']
@@ -460,6 +461,7 @@ declare module 'vue' {
readonly useCloned: UnwrapRef<typeof import('@vueuse/core')['useCloned']> readonly useCloned: UnwrapRef<typeof import('@vueuse/core')['useCloned']>
readonly useColorMode: UnwrapRef<typeof import('@vueuse/core')['useColorMode']> readonly useColorMode: UnwrapRef<typeof import('@vueuse/core')['useColorMode']>
readonly useConfirmDialog: UnwrapRef<typeof import('@vueuse/core')['useConfirmDialog']> readonly useConfirmDialog: UnwrapRef<typeof import('@vueuse/core')['useConfirmDialog']>
readonly useCountdown: UnwrapRef<typeof import('@vueuse/core')['useCountdown']>
readonly useCounter: UnwrapRef<typeof import('@vueuse/core')['useCounter']> readonly useCounter: UnwrapRef<typeof import('@vueuse/core')['useCounter']>
readonly useCssModule: UnwrapRef<typeof import('vue')['useCssModule']> readonly useCssModule: UnwrapRef<typeof import('vue')['useCssModule']>
readonly useCssVar: UnwrapRef<typeof import('@vueuse/core')['useCssVar']> readonly useCssVar: UnwrapRef<typeof import('@vueuse/core')['useCssVar']>

View File

@@ -1,17 +0,0 @@
FROM node:22.12.0 as build
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80

2
components.d.ts vendored
View File

@@ -58,6 +58,7 @@ declare module 'vue' {
BackgroundAnimation: typeof import('./src/components/BackgroundAnimation/BackgroundAnimation.vue')['default'] BackgroundAnimation: typeof import('./src/components/BackgroundAnimation/BackgroundAnimation.vue')['default']
BackTop: typeof import('./src/components/MyUI/BackTop/BackTop.vue')['default'] BackTop: typeof import('./src/components/MyUI/BackTop/BackTop.vue')['default']
Badge: typeof import('./src/components/MyUI/Badge/Badge.vue')['default'] Badge: typeof import('./src/components/MyUI/Badge/Badge.vue')['default']
BlockOutlined: typeof import('@ant-design/icons-vue')['BlockOutlined']
BoxDog: typeof import('./src/components/BoxDog/BoxDog.vue')['default'] BoxDog: typeof import('./src/components/BoxDog/BoxDog.vue')['default']
Breadcrumb: typeof import('./src/components/MyUI/Breadcrumb/Breadcrumb.vue')['default'] Breadcrumb: typeof import('./src/components/MyUI/Breadcrumb/Breadcrumb.vue')['default']
Button: typeof import('./src/components/MyUI/Button/Button.vue')['default'] Button: typeof import('./src/components/MyUI/Button/Button.vue')['default']
@@ -88,6 +89,7 @@ declare module 'vue' {
DynamicTitle: typeof import('./src/components/DynamicTitle/DynamicTitle.vue')['default'] DynamicTitle: typeof import('./src/components/DynamicTitle/DynamicTitle.vue')['default']
Ellipsis: typeof import('./src/components/MyUI/Ellipsis/Ellipsis.vue')['default'] Ellipsis: typeof import('./src/components/MyUI/Ellipsis/Ellipsis.vue')['default']
Empty: typeof import('./src/components/MyUI/Empty/Empty.vue')['default'] Empty: typeof import('./src/components/MyUI/Empty/Empty.vue')['default']
EyeInvisibleOutlined: typeof import('@ant-design/icons-vue')['EyeInvisibleOutlined']
EyeOutlined: typeof import('@ant-design/icons-vue')['EyeOutlined'] EyeOutlined: typeof import('@ant-design/icons-vue')['EyeOutlined']
FileImageOutlined: typeof import('@ant-design/icons-vue')['FileImageOutlined'] FileImageOutlined: typeof import('@ant-design/icons-vue')['FileImageOutlined']
Flex: typeof import('./src/components/MyUI/Flex/Flex.vue')['default'] Flex: typeof import('./src/components/MyUI/Flex/Flex.vue')['default']

View File

@@ -11,20 +11,35 @@ server {
proxy_set_header Upgrade $http_upgrade; proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade"; proxy_set_header Connection "upgrade";
access_log /var/log/nginx/host.access.log main; access_log /var/log/nginx/host.access.log main;
error_log /var/log/nginx/error.log error; error_log /var/log/nginx/error.log error;
location / { location / {
root /usr/share/nginx/html; root /usr/share/nginx/html;
try_files $uri $uri/ /index.html; try_files $uri $uri/ /index.html;
index index.html index.htm; index index.html index.htm;
} }
location ~* \.(js|css)$ { location ~* \.(js|css)$ {
gzip_static on; gzip_static on;
expires max; expires max;
add_header Cache-Control "public"; add_header Cache-Control "public";
root /usr/share/nginx/html; root /usr/share/nginx/html;
} }
location /sys/ {
rewrite ^/sys/(.*)$ /$1 break;
proxy_pass http://127.0.0.1:80;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 3600s; # 设置为1小时
proxy_send_timeout 3600s; # 设置为1小时
send_timeout 3600s; # 设置为1小时
keepalive_timeout 3600s; # 设置为1小时
}
error_page 500 502 503 504 /50x.html; error_page 500 502 503 504 /50x.html;
location = /50x.html { location = /50x.html {

18
nginx.conf Normal file
View File

@@ -0,0 +1,18 @@
worker_processes 1;
events {
worker_connections 1024;
}
http {
server {
listen 8080;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
}
}

View File

@@ -14,6 +14,7 @@
"@ant-design/icons-vue": "^7.0.1", "@ant-design/icons-vue": "^7.0.1",
"@intlify/eslint-plugin-vue-i18n": "^3.2.0", "@intlify/eslint-plugin-vue-i18n": "^3.2.0",
"@mediapipe/face_detection": "^0.4.1646425229", "@mediapipe/face_detection": "^0.4.1646425229",
"@mediapipe/face_mesh": "^0.4.1633559619",
"@tensorflow-models/coco-ssd": "^2.2.3", "@tensorflow-models/coco-ssd": "^2.2.3",
"@tensorflow-models/face-detection": "^1.0.3", "@tensorflow-models/face-detection": "^1.0.3",
"@tensorflow-models/face-landmarks-detection": "^1.0.6", "@tensorflow-models/face-landmarks-detection": "^1.0.6",
@@ -26,17 +27,15 @@
"@tensorflow/tfjs-backend-webgpu": "^4.22.0", "@tensorflow/tfjs-backend-webgpu": "^4.22.0",
"@tensorflow/tfjs-converter": "^4.22.0", "@tensorflow/tfjs-converter": "^4.22.0",
"@tensorflow/tfjs-core": "^4.22.0", "@tensorflow/tfjs-core": "^4.22.0",
"@tensorflow/tfjs-node": "^4.22.0",
"@tensorflow/tfjs-node-gpu": "^4.22.0",
"@types/animejs": "^3.1.12", "@types/animejs": "^3.1.12",
"@types/crypto-js": "^4.2.2", "@types/crypto-js": "^4.2.2",
"@types/json-stringify-safe": "^5.0.3", "@types/json-stringify-safe": "^5.0.3",
"@types/node": "^22.10.7", "@types/node": "^22.13.1",
"@types/nprogress": "^0.2.3", "@types/nprogress": "^0.2.3",
"@vladmandic/face-api": "^1.7.14", "@vladmandic/face-api": "^1.7.14",
"@vuepic/vue-datepicker": "^11.0.1", "@vuepic/vue-datepicker": "^11.0.1",
"@vueuse/core": "^12.4.0", "@vueuse/core": "^12.5.0",
"@vueuse/integrations": "^12.4.0", "@vueuse/integrations": "^12.5.0",
"alova": "^3.2.8", "alova": "^3.2.8",
"animejs": "^3.2.2", "animejs": "^3.2.2",
"ant-design-vue": "^4.2.6", "ant-design-vue": "^4.2.6",
@@ -45,7 +44,7 @@
"buffer": "^6.0.3", "buffer": "^6.0.3",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"echarts": "^5.6.0", "echarts": "^5.6.0",
"eslint": "9.18.0", "eslint": "9.19.0",
"exifr": "^7.1.3", "exifr": "^7.1.3",
"go-captcha-vue": "^2.0.5", "go-captcha-vue": "^2.0.5",
"gsap": "^3.12.7", "gsap": "^3.12.7",
@@ -58,36 +57,37 @@
"pinia": "^2.3.1", "pinia": "^2.3.1",
"pinia-plugin-persistedstate-2": "^2.0.28", "pinia-plugin-persistedstate-2": "^2.0.28",
"qrcode": "^1", "qrcode": "^1",
"rimraf": "^6.0.1",
"seedrandom": "^3.0.5", "seedrandom": "^3.0.5",
"swiper": "^11.2.1", "swiper": "^11.2.2",
"unplugin-auto-import": "^19.0.0", "unplugin-auto-import": "^19.0.0",
"vite-plugin-compression": "^0.5.1", "vite-plugin-compression": "^0.5.1",
"vite-plugin-html": "^3.2.2", "vite-plugin-html": "^3.2.2",
"vite-plugin-node-polyfills": "^0.23.0", "vite-plugin-node-polyfills": "^0.23.0",
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-dompurify-html": "^5.2.0", "vue-dompurify-html": "^5.2.0",
"vue-i18n": "^11.0.1", "vue-i18n": "^11.1.0",
"vue-router": "^4.5.0", "vue-router": "^4.5.0",
"vue-waterfall-plugin-next": "^2.6.5", "vue-waterfall-plugin-next": "^2.6.5",
"ws": "^8.18.0" "ws": "^8.18.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.18.0", "@eslint/js": "^9.19.0",
"@vitejs/plugin-vue": "^5.2.1", "@vitejs/plugin-vue": "^5.2.1",
"eslint-plugin-vue": "^9.32.0", "eslint-plugin-vue": "^9.32.0",
"globals": "^15.14.0", "globals": "^15.14.0",
"sass": "^1.83.4", "sass": "^1.83.4",
"typescript": "^5.7.3", "typescript": "^5.7.3",
"typescript-eslint": "^8.20.0", "typescript-eslint": "^8.23.0",
"unplugin-vue-components": "^28.0.0", "unplugin-vue-components": "^28.0.0",
"vite": "^6.0.9", "vite": "^6.0.11",
"vite-plugin-bundle-obfuscator": "1.4.0", "vite-plugin-bundle-obfuscator": "1.4.1",
"vite-plugin-chunk-split": "^0.5.0", "vite-plugin-chunk-split": "^0.5.0",
"vue-tsc": "2.2.0" "vue-tsc": "2.2.0"
}, },
"overrides": { "overrides": {
"vite-plugin-chunk-split": { "vite-plugin-chunk-split": {
"vite": "^6.0.9" "vite": "^6.0.11"
} }
} }
} }

View File

@@ -8,3 +8,55 @@ export const uploadFile = (formData) => {
} }
}); });
}; };
/**
* 获取用户人脸样本列表
* @param type
*/
export const getFaceSamplesList = (type: number) => {
return service.Post('/api/auth/storage/face/sample/list', {
type: type,
}, {
cacheFor: {
expire: 60 * 60 * 24 * 7,
mode: "restore",
},
meta: {
ignoreToken: false,
signature: false,
}
});
};
/**
* 修改人脸样本名称
* @param id
* @param face_name
*/
export const modifyFaceSampleName = (id: number, face_name: string) => {
return service.Post('/api/auth/storage/face/sample/modify/name', {
id: id,
face_name: face_name,
}, {
meta: {
ignoreToken: false,
signature: false,
}
});
};
/**
* 批量修改人脸样本类型
* @param ids
* @param face_type
*/
export const modifyFaceTypeBatch = (ids: number[], face_type: number) => {
return service.Post('/api/auth/storage/face/sample/modify/type', {
ids: ids,
face_type: face_type,
}, {
meta: {
ignoreToken: false,
signature: false,
}
});
};

View File

@@ -40,35 +40,32 @@
<AUpload <AUpload
:accept="'image/jpg, image/png, image/jpeg'" :accept="'image/jpg, image/png, image/jpeg'"
name="images" name="images"
:max-count="3" :max-count="1"
:multiple="true" :multiple="true"
method="post" method="post"
:directory="false" :directory="false"
:show-upload-list="false" :show-upload-list="false"
:custom-request="comment.customUploadRequest" :custom-request="comment.customUploadRequest"
:before-upload="comment.beforeUpload" :before-upload="comment.beforeUpload"
:disabled="comment.imageList.length >= 3 || comment.uploadLoading" :disabled="comment.uploadLoading"
> >
<ABadge :count="comment.imageList.length">
<AButton type="text" size="small" :icon="h(PictureOutlined)" <AButton type="text" size="small" :icon="h(PictureOutlined)"
class="comment-action-icon" :loading="comment.uploadLoading"> class="comment-action-icon" :loading="comment.uploadLoading">
{{ t('comment.picture') }} {{ t('comment.picture') }}
</AButton> </AButton>
</ABadge>
</AUpload> </AUpload>
<template v-if="comment.imageList.length > 0"> <template v-if="comment.imageList">
<AImagePreviewGroup> <ABadge style="margin-left: 10px;">
<ABadge style="margin-left: 10px;" v-for="(item, index) in comment.imageList" :key="index"> <template #count>
<template #count> <CloseCircleOutlined @click="comment.removeBase64Image()" style="color: #f5222d"/>
<CloseCircleOutlined @click="comment.removeBase64Image(index)" style="color: #f5222d"/> </template>
<AAvatar shape="square" size="small">
<template #icon>
<AImage :width="24" :height="24" :src="comment.imageList"/>
</template> </template>
<AAvatar shape="square" size="small"> </AAvatar>
<template #icon> </ABadge>
<AImage v-if="item" :width="24" :height="24" :src="item"/>
</template>
</AAvatar>
</ABadge>
</AImagePreviewGroup>
</template> </template>
</AFlex> </AFlex>
</AFlex> </AFlex>
@@ -153,10 +150,6 @@ async function commentSubmit(point: any) {
message.error(t('comment.commentContentNotEmpty')); message.error(t('comment.commentContentNotEmpty'));
return; return;
} }
if (comment.imageList.length > 3) {
message.error(t('comment.maxImageCount'));
return;
}
const content = commentContent.value.replace(/\r\n/g, '<br/>').replace(/\n/g, '<br/>').replace(/\s/g, ' '); const content = commentContent.value.replace(/\r\n/g, '<br/>').replace(/\n/g, '<br/>').replace(/\s/g, ' ');
const regex = /\[((1[0-6][0-6]|[1-9]?[0-9])\.gif)]/g; // 匹配 [1.gif] 的字符串 const regex = /\[((1[0-6][0-6]|[1-9]?[0-9])\.gif)]/g; // 匹配 [1.gif] 的字符串
const contentWithEmoji = content.replace(regex, (_match, p1) => { const contentWithEmoji = content.replace(regex, (_match, p1) => {
@@ -216,10 +209,6 @@ async function showSlideCaptcha() {
if (commentContent.value.trim() === "") { if (commentContent.value.trim() === "") {
return; return;
} }
if (comment.imageList.length > 3) {
message.warning(t('comment.maxImageCount'));
return;
}
const res = await comment.getSlideCaptchaData(); const res = await comment.getSlideCaptchaData();
if (res) { if (res) {
showSubmitCaptcha.value = true; showSubmitCaptcha.value = true;

View File

@@ -68,8 +68,7 @@
<AFlex :vertical="false" align="center" class="reply-images" v-if="item.images"> <AFlex :vertical="false" align="center" class="reply-images" v-if="item.images">
<AImagePreviewGroup> <AImagePreviewGroup>
<ASpace direction="horizontal"> <ASpace direction="horizontal">
<AImage :width="80" :height="80" v-for="(image, index) in item.images" :key="index" <AImage :width="80" :height="80" :src="item.images">
:src="image">
<template #previewMask> <template #previewMask>
<EyeOutlined style="font-size: 20px;"/> <EyeOutlined style="font-size: 20px;"/>
</template> </template>

View File

@@ -56,24 +56,24 @@
:show-upload-list="false" :show-upload-list="false"
:custom-request="comment.customUploadRequest" :custom-request="comment.customUploadRequest"
:before-upload="comment.beforeUpload" :before-upload="comment.beforeUpload"
:disabled="comment.imageList.length >= 3 || comment.uploadLoading" :disabled="comment.uploadLoading"
> >
<ABadge :count="comment.imageList.length"> <ABadge>
<AButton type="text" size="small" :icon="h(PictureOutlined)" <AButton type="text" size="small" :icon="h(PictureOutlined)"
class="comment-action-icon-reply" :loading="comment.uploadLoading"> class="comment-action-icon-reply" :loading="comment.uploadLoading">
{{ t('comment.picture') }} {{ t('comment.picture') }}
</AButton> </AButton>
</ABadge> </ABadge>
</AUpload> </AUpload>
<template v-if="comment.imageList.length > 0"> <template v-if="comment.imageList">
<AImagePreviewGroup> <AImagePreviewGroup>
<ABadge style="margin-left: 10px;" v-for="(item, index) in comment.imageList" :key="index"> <ABadge style="margin-left: 10px;">
<template #count> <template #count>
<CloseCircleOutlined @click="comment.removeBase64Image(index)" style="color: #f5222d"/> <CloseCircleOutlined @click="comment.removeBase64Image()" style="color: #f5222d"/>
</template> </template>
<AAvatar shape="square" size="small"> <AAvatar shape="square" size="small">
<template #icon> <template #icon>
<AImage v-if="item" :width="24" :height="24" :src="item"/> <AImage :width="24" :height="24" :src="comment.imageList"/>
</template> </template>
</AAvatar> </AAvatar>
</ABadge> </ABadge>
@@ -157,10 +157,6 @@ async function replySubmit(point: any) {
if (replyContent.value.trim() === "") { if (replyContent.value.trim() === "") {
return; return;
} }
if (comment.imageList.length > 3) {
message.warning(t('comment.maxImageCount'));
return;
}
const content = replyContent.value.replace(/\r\n/g, '<br/>').replace(/\n/g, '<br/>').replace(/\s/g, ' '); const content = replyContent.value.replace(/\r\n/g, '<br/>').replace(/\n/g, '<br/>').replace(/\s/g, ' ');
const regex = /\[((1[0-6][0-6]|[1-9]?[0-9])\.gif)\]/g; // 匹配 [1.gif] 的字符串 const regex = /\[((1[0-6][0-6]|[1-9]?[0-9])\.gif)\]/g; // 匹配 [1.gif] 的字符串
const contentWithEmoji = content.replace(regex, (_match, p1) => { const contentWithEmoji = content.replace(regex, (_match, p1) => {
@@ -234,10 +230,6 @@ async function showSlideCaptcha() {
message.warning(t('comment.commentContentNotEmpty')); message.warning(t('comment.commentContentNotEmpty'));
return; return;
} }
if (comment.imageList.length > 3) {
message.warning(t('comment.maxImageCount'));
return;
}
const res = await comment.getSlideCaptchaData(); const res = await comment.getSlideCaptchaData();
if (res) { if (res) {
showSubmitCaptcha.value = true; showSubmitCaptcha.value = true;

View File

@@ -51,8 +51,7 @@
<AImagePreviewGroup> <AImagePreviewGroup>
<AImagePreviewGroup> <AImagePreviewGroup>
<ASpace direction="horizontal"> <ASpace direction="horizontal">
<AImage :width="80" :height="80" v-for="(image, index) in child.images" :key="index" <AImage :width="80" :height="80" :src="child.images">
:src="image">
<template #previewMask> <template #previewMask>
<EyeOutlined style="font-size: 20px;"/> <EyeOutlined style="font-size: 20px;"/>
</template> </template>

View File

@@ -59,26 +59,25 @@
:show-upload-list="false" :show-upload-list="false"
:custom-request="comment.customUploadRequest" :custom-request="comment.customUploadRequest"
:before-upload="comment.beforeUpload" :before-upload="comment.beforeUpload"
:disabled="comment.imageList.length >= 3 || comment.uploadLoading" :disabled="comment.uploadLoading"
> >
<ABadge :count="comment.imageList.length"> <ABadge>
<AButton type="text" size="small" :icon="h(PictureOutlined)" <AButton type="text" size="small" :icon="h(PictureOutlined)"
class="comment-action-icon-reply-child" :loading="comment.uploadLoading"> class="comment-action-icon-reply-child" :loading="comment.uploadLoading">
{{ t('comment.picture') }} {{ t('comment.picture') }}
</AButton> </AButton>
</ABadge> </ABadge>
</AUpload> </AUpload>
<template v-if="comment.imageList.length > 0"> <template v-if="comment.imageList">
<AImagePreviewGroup> <AImagePreviewGroup>
<ABadge style="margin-left: 10px;" v-for="(item, index) in comment.imageList" <ABadge style="margin-left: 10px;">
:key="index">
<template #count> <template #count>
<CloseCircleOutlined @click="comment.removeBase64Image(index)" <CloseCircleOutlined @click="comment.removeBase64Image()"
style="color: #f5222d"/> style="color: #f5222d"/>
</template> </template>
<AAvatar shape="square" size="small"> <AAvatar shape="square" size="small">
<template #icon> <template #icon>
<AImage v-if="item" :width="24" :height="24" :src="item"/> <AImage :width="24" :height="24" :src="comment.imageList"/>
</template> </template>
</AAvatar> </AAvatar>
</ABadge> </ABadge>
@@ -168,10 +167,6 @@ async function replyReplySubmit(point: any) {
if (replyReplyContent.value.trim() === "") { if (replyReplyContent.value.trim() === "") {
return; return;
} }
if (comment.imageList.length > 3) {
message.warning(t('comment.maxImageCount'));
return;
}
const content = replyReplyContent.value.replace(/\r\n/g, '<br/>').replace(/\n/g, '<br/>').replace(/\s/g, ' '); const content = replyReplyContent.value.replace(/\r\n/g, '<br/>').replace(/\n/g, '<br/>').replace(/\s/g, ' ');
const regex = /\[((1[0-6][0-6]|[1-9]?[0-9])\.gif)]/g; // 匹配 [1.gif] 的字符串 const regex = /\[((1[0-6][0-6]|[1-9]?[0-9])\.gif)]/g; // 匹配 [1.gif] 的字符串
const contentWithEmoji = content.replace(regex, (_match, p1) => { const contentWithEmoji = content.replace(regex, (_match, p1) => {
@@ -240,10 +235,6 @@ async function showSlideCaptcha() {
message.warning(t('comment.commentContentNotEmpty')); message.warning(t('comment.commentContentNotEmpty'));
return; return;
} }
if (comment.imageList.length > 3) {
message.warning(t('comment.maxImageCount'));
return;
}
const res = await comment.getSlideCaptchaData(); const res = await comment.getSlideCaptchaData();
if (res) { if (res) {
showSubmitCaptcha.value = true; showSubmitCaptcha.value = true;

View File

@@ -5,7 +5,7 @@
@click="handleClick" @click="handleClick"
:style="cardStyle"> :style="cardStyle">
<div class="hover-circle" @click.stop="toggleSelection" <div class="hover-circle" @click.stop="toggleSelection"
v-if="showHoverCircle" v-if="showHoverCircle && !isSelected"
:style="{ width: iconSize + 'px', height: iconSize + 'px' }"> :style="{ width: iconSize + 'px', height: iconSize + 'px' }">
<img :src="greyComplete" alt="Hover" class="hover-icon" <img :src="greyComplete" alt="Hover" class="hover-icon"
:style="{ width: iconSize + 'px', height: iconSize + 'px' }"/> :style="{ width: iconSize + 'px', height: iconSize + 'px' }"/>

View File

@@ -2,6 +2,7 @@
import clickOutside from '@/directives/v-click-outside.ts'; import clickOutside from '@/directives/v-click-outside.ts';
import lazyLoad from "@/directives/v-lazy-load.ts"; import lazyLoad from "@/directives/v-lazy-load.ts";
import focus from "@/directives/v-focus.ts";
/** /**
* Register all directives * Register all directives
@@ -10,4 +11,5 @@ import lazyLoad from "@/directives/v-lazy-load.ts";
export const registerDirectives = (app: any) => { export const registerDirectives = (app: any) => {
app.directive('click-outside', clickOutside); app.directive('click-outside', clickOutside);
app.directive('lazy.ts-load', lazyLoad); app.directive('lazy.ts-load', lazyLoad);
app.directive('v-focus', focus);
}; };

View File

@@ -0,0 +1,6 @@
export default {
mounted(el) {
// 当元素被挂载到 DOM 时,自动聚焦
el.focus();
},
};

View File

@@ -29,8 +29,7 @@ export const useCommentStore = defineStore(
thumbX: 0, thumbX: 0,
thumbY: 0 thumbY: 0
}); });
const fileList = ref<any[]>([]); const imageList = ref<string>("");
const imageList = ref<any[]>([]);
const uploadLoading = ref<boolean>(false); const uploadLoading = ref<boolean>(false);
const emojiList = ref<any[]>(QQ_EMOJI); const emojiList = ref<any[]>(QQ_EMOJI);
const commentId = ref<number | null>(null); const commentId = ref<number | null>(null);
@@ -199,8 +198,7 @@ export const useCommentStore = defineStore(
* 清空文件列表 * 清空文件列表
*/ */
async function clearFileList() { async function clearFileList() {
fileList.value = []; imageList.value = "";
imageList.value = [];
} }
@@ -221,25 +219,21 @@ export const useCommentStore = defineStore(
const reader = new FileReader(); const reader = new FileReader();
reader.readAsDataURL(file); // 文件转换 reader.readAsDataURL(file); // 文件转换
reader.onloadend = async function () { reader.onloadend = async function () {
if (fileList.value.length < 3) { const img: HTMLImageElement = document.createElement('img');
const img: HTMLImageElement = document.createElement('img'); img.src = reader.result as string;
img.src = reader.result as string; img.onload = async () => {
img.onload = async () => { // 图片 NSFW 检测
// 图片 NSFW 检测 const nsfw: NSFWJS = await initNSFWJs();
const nsfw: NSFWJS = await initNSFWJs(); const isNSFW: boolean = await predictNSFW(nsfw, img);
const isNSFW: boolean = await predictNSFW(nsfw, img); if (isNSFW) {
if (isNSFW) { message.error(i18n.global.t('comment.illegalImage'));
message.error(i18n.global.t('comment.illegalImage')); imageList.value = "";
fileList.value.pop();
uploadLoading.value = false;
return false;
}
fileList.value.push(img.src);
uploadLoading.value = false; uploadLoading.value = false;
}; return false;
} else { }
return false; imageList.value = img.src;
} uploadLoading.value = false;
};
}; };
return true; return true;
@@ -249,16 +243,14 @@ export const useCommentStore = defineStore(
* 自定义上传图片请求 * 自定义上传图片请求
*/ */
async function customUploadRequest() { async function customUploadRequest() {
imageList.value = fileList.value;
} }
/** /**
* 移除图片 * 移除图片
* @param index
*/ */
async function removeBase64Image(index: number) { async function removeBase64Image() {
fileList.value.splice(index, 1); imageList.value = "";
imageList.value.splice(index, 1);
} }
/** /**
@@ -316,7 +308,6 @@ export const useCommentStore = defineStore(
replyLoading, replyLoading,
slideCaptchaData, slideCaptchaData,
commentMap, commentMap,
fileList,
imageList, imageList,
uploadLoading, uploadLoading,
emojiList, emojiList,

View File

@@ -23,14 +23,14 @@ interface CommentContent {
avatar: string; avatar: string;
nickname: string; nickname: string;
level?: number; level?: number;
images: string[]; images: string;
is_liked: boolean; is_liked: boolean;
} }
export interface ReplyCommentParams { export interface ReplyCommentParams {
topic_id: string, topic_id: string,
content: string, content: string,
images: string[], images: string,
author: string, author: string,
reply_id: number, reply_id: number,
reply_user: string, reply_user: string,

View File

@@ -1,71 +1,217 @@
<template> <template>
<div class="people-album"> <div class="people-album">
<div class="people-album-header"> <div class="people-album-header">
<ADropdown> <ADropdown trigger="click">
<AButton type="text" size="large" class="people-album-button"> <AButton type="text" size="large" class="people-album-button">
人物 {{ selecetedKey === '0' ? '人 物' : '已隐藏' }}
<DownOutlined class="people-album-icon"/> <DownOutlined class="people-album-icon"/>
</AButton> </AButton>
<template #overlay> <template #overlay>
<AMenu> <AMenu selectable :selectedKeys="[selecetedKey]" @select="handleSelect">
<AMenuItem> </AMenuItem> <AMenuItem key="0"> </AMenuItem>
<AMenuItem>已隐藏</AMenuItem> <AMenuItem key="1">已隐藏</AMenuItem>
</AMenu> </AMenu>
</template> </template>
</ADropdown> </ADropdown>
<span class="people-album-count"><span style="color: #0e87cc">{{ faceList.length }}</span></span>
</div> </div>
<div class="people-album-content"> <transition name="fade">
<div class="people-album-item" @mouseover="showButton = true" @mouseleave="showButton = false"> <div class="people-album-toolbar" v-show="selected.length !== 0">
<div class="people-album-item-avatar"> <div class="people-album-toolbar-left">
<AAvatar :size="86" shape="circle" src="/test/4.png"/> <AButton type="text" shape="circle" size="large" class="people-album-toolbar-btn" @click="cancelSelectPeople">
</div> <template #icon>
<div class="people-album-item-name"> <CloseOutlined class="people-album-toolbar-icon"/>
<AButton @click="showAddNameInput" class="people-album-add-name" v-show="showButton && !showInput" type="link"
size="small">
添加名字
</AButton>
<AInput v-show="showInput" @blur="hideAddNameInput" size="small" class="people-album-add-input">
<template #suffix>
<AButton type="link" size="small">完成</AButton>
</template> </template>
</AInput> </AButton>
<span style="font-size: 16px;font-weight: bold">
已选择 {{ selected.length }} 个人物
</span>
<AButton type="text" shape="default" class="people-album-toolbar-btn" size="middle" @click="selectAllPeople">
全选
</AButton>
</div> </div>
</div> <div class="people-album-toolbar-right">
<div class="people-album-item" @mouseover="showButton = true" @mouseleave="showButton = false"> <AButton type="text" shape="default" size="middle" class="people-album-toolbar-btn"
<div class="people-album-item-avatar"> :disabled="selected.length !== 2" v-if="selecetedKey === '0'">
<AAvatar :size="86" shape="circle" src="/test/4.png"/> <template #icon>
</div> <BlockOutlined class="people-album-toolbar-icon"/>
<div class="people-album-item-name">
<AButton @click="showAddNameInput" class="people-album-add-name" v-show="showButton && !showInput" type="link"
size="small">
添加名字
</AButton>
<AInput v-show="showInput" @blur="hideAddNameInput" size="small" class="people-album-add-input">
<template #suffix>
<AButton type="link" size="small">完成</AButton>
</template> </template>
</AInput> 合并人物
</AButton>
<AButton type="text" shape="default" size="middle" class="people-album-toolbar-btn" @click="hiddenFace">
<template #icon>
<EyeInvisibleOutlined class="people-album-toolbar-icon"/>
</template>
{{ selecetedKey === '0' ? '隐藏人物' : '取消隐藏' }}
</AButton>
</div> </div>
</div> </div>
</transition>
<div class="people-album-container">
<ASpin :spinning="loading" size="large" wrapperClassName="people-album-container">
<div class="people-album-content">
<CheckCard
v-for="(item, index) in faceList"
:key="index"
class="photo-item"
margin="0"
border-radius="0"
v-model="selected"
:showHoverCircle="true"
:background-color="'transparent'"
:iconSize="20"
:showSelectedEffect="false"
:value="item.id">
<div class="people-album-item"
:class="{ 'selected-item': selected.includes(item.id) }"
@mouseover="item.showButton = true"
@mouseleave="item.showButton = false">
<div class="people-album-item-avatar">
<AAvatar :size="86" shape="circle" :src="item.face_image"/>
</div>
<div class="people-album-item-name" v-show="!item.face_name">
<AButton @click="showAddNameInput(index)" class="people-album-add-name"
v-show="item.showButton && !item.showInput"
type="link"
size="small">
添加名字
</AButton>
<AInput ref="addNameInput" v-model:value="addNameInputValue" v-show="item.showInput"
@blur="hideAddNameInput(index)" size="small"
:maxlength="10"
:placeholder="item.face_name"
class="people-album-add-input">
<template #suffix>
<AButton type="link" style="font-size: 12px;" size="small" @mousedown.prevent
@click="modifyFaceName(item.id,index)">完成
</AButton>
</template>
</AInput>
</div>
<div class="people-album-item-name" v-show="item.face_name">
<AButton @click="showAddNameInput(index)" class="people-album-add-name" v-show="!item.showInput"
type="link"
size="small">
{{ item.face_name }}
</AButton>
<AInput ref="addNameInput" v-model:value="addNameInputValue" autofocus v-show="item.showInput"
@blur="hideAddNameInput(index)" size="small"
:maxlength="10"
:placeholder="item.face_name"
class="people-album-add-input">
<template #suffix>
<AButton type="link" style="font-size: 12px;" size="small" @mousedown.prevent
@click="modifyFaceName(item.id,index)">完成
</AButton>
</template>
</AInput>
</div>
</div>
</CheckCard>
</div>
</ASpin>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
const showButton = ref(false); import {getFaceSamplesList, modifyFaceSampleName, modifyFaceTypeBatch} from "@/api/storage";
const showInput = ref(false);
function showAddNameInput() { const faceList = ref<any[]>([]);
showInput.value = true; const addNameInput = ref<any>(null);
showButton.value = false; const addNameInputValue = ref<string>('');
const selecetedKey = ref<string>('0');
const loading = ref<boolean>(false);
const selected = ref<any[]>([]);
/**
* 获取人脸列表
*/
async function getFaceList(type: number = 0) {
loading.value = true;
faceList.value = [];
const res: any = await getFaceSamplesList(type);
if (res && res.code === 200 && res.data.faces) {
faceList.value = res.data.faces.map(face => ({
...face,
showButton: false,
showInput: false,
}));
}
loading.value = false;
} }
function hideAddNameInput() { function showAddNameInput(index: number) {
showInput.value = false; if (faceList.value[index]) {
showButton.value = false; faceList.value[index].showInput = true;
faceList.value[index].showButton = false;
}
} }
function hideAddNameInput(index: number) {
if (faceList.value[index]) {
faceList.value[index].showInput = false;
faceList.value[index].showButton = false;
}
}
/**
* 修改人脸名称
* @param id
* @param index
*/
async function modifyFaceName(id: number, index: number) {
if (!addNameInputValue.value.trim()) return;
const res: any = await modifyFaceSampleName(id, addNameInputValue.value);
if (res && res.code === 200) {
faceList.value[index].face_name = res.data.face_name;
addNameInputValue.value = '';
hideAddNameInput(index);
}
}
/**
* 选择分类
* @param key
*/
function handleSelect({key}) {
selecetedKey.value = key;
getFaceList(parseInt(key));
}
/**
* 全选
*/
function selectAllPeople() {
selected.value = faceList.value.map((item) => item.id);
}
/**
* 取消选择
*/
function cancelSelectPeople() {
selected.value = [];
}
/**
* 隐藏人物
*/
async function hiddenFace() {
if (selected.value.length === 0) return;
const res: any = await modifyFaceTypeBatch(selected.value, selecetedKey.value === '0' ? 1 : 0);
if (res && res.code === 200) {
await getFaceList();
selected.value = [];
}
}
onMounted(() => {
getFaceList();
});
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@@ -103,68 +249,162 @@ function hideAddNameInput() {
} }
} }
.people-album-count {
width: 100%;
font-size: 13px;
color: rgba(129, 129, 138, 0.99);
}
} }
.people-album-content {
.people-album-toolbar {
position: fixed;
width: calc(100% - 220px);
height: 70px;
top: 70px;
z-index: 3;
display: flex;
box-sizing: border-box;
justify-content: space-between;
align-items: center;
background-image: linear-gradient(45deg, #5789ff, #5c7bff 100%);
color: #fff;
box-shadow: 0 3px 10px 0 rgba(0, 0, 0, .06);
padding: 0 20px;
.people-album-toolbar-left {
width: 50%;
height: 100%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
gap: 20px;
}
.people-album-toolbar-right {
height: 100%;
width: 50%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-end;
gap: 30px;
}
.people-album-toolbar-icon {
font-size: 20px;
font-weight: bold;
color: #fff;
}
.people-album-toolbar-btn {
font-size: 16px;
font-weight: bold;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
}
}
.people-album-container {
width: 100%; width: 100%;
height: 100%; height: 100%;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: flex-start; justify-content: flex-start;
align-items: flex-start; align-content: flex-start;
padding-top: 20px; flex-wrap: wrap;
padding-left: 20px;
gap: 20px;
.people-album-item { .people-album-content {
width: 130px; width: 100%;
height: 160px; height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: row;
align-items: center; justify-content: flex-start;
justify-content: center; align-content: flex-start;
border-radius: 10px; flex-wrap: wrap;
transition: all 0.3s ease-in-out; padding-top: 20px;
position: relative; padding-left: 20px;
cursor: pointer; padding-right: 20px;
gap: 20px;
.people-album-item-avatar {
width: 100%;
height: 75%;
display: flex;
align-items: center;
justify-content: center;
}
.people-album-item-name { .people-album-item {
width: 100%; width: 130px;
height: 25%; height: 160px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: flex-start; justify-content: center;
border-radius: 10px;
transition: all 0.3s ease;
position: relative;
cursor: pointer;
.people-album-add-input { .people-album-item-avatar {
width: 80%; width: 100%;
height: 75%;
display: flex;
align-items: center;
justify-content: center;
}
.people-album-item-name {
width: 100%;
height: 25%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
.people-album-add-input {
width: 80%;
}
}
.people-album-add-name {
color: rgba(110, 110, 113, 0.99);
font-size: 13px;
}
.people-album-add-name:hover {
color: #0e87cc;
} }
} }
.people-album-item:hover,
.people-album-add-name { .people-album-item.selected-item {
color: rgba(126, 126, 135, 0.99); background-color: rgba(248, 248, 248, 0.74);
font-size: 12px; opacity: 1;
} transform: scale(1.05);
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
.people-album-add-name:hover {
color: #0e87cc;
} }
} }
.people-album-item:hover {
background-color: rgba(248, 248, 248, 0.74);
opacity: 1;
transform: scale(1.05);
}
} }
} }
.fade-enter-active, .fade-leave-active {
transition: all 0.5s ease;
}
.fade-enter-from, .fade-leave-to { /* .slide-fade-leave-active 在离开之前 */
transform: translateY(-20px);
opacity: 0;
}
.fade-enter-from {
transform: translateY(-30px);
opacity: 0;
}
.fade-enter-to {
transform: translateY(0);
opacity: 1;
}
</style> </style>

View File

@@ -77,7 +77,11 @@
<script setup lang="ts"> <script setup lang="ts">
import more from "@/assets/svgs/more.svg"; import more from "@/assets/svgs/more.svg";
const isHovered = ref<boolean>(false); const isHovered = ref<boolean>(false);
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.phoalbum { .phoalbum {