后台管理上限

This commit is contained in:
2026-04-03 16:04:02 +08:00
parent 6608b3045e
commit 7e20966f97
47 changed files with 3148 additions and 830 deletions

6
.env
View File

@@ -20,9 +20,9 @@ VITE_APP_DOCALERT_ENABLE=true
VITE_APP_BAIDU_CODE = a1ff8825baa73c3a78eb96aa40325abc
# 默认账户密码
VITE_APP_DEFAULT_LOGIN_TENANT = 芋道源码
VITE_APP_DEFAULT_LOGIN_USERNAME = admin
VITE_APP_DEFAULT_LOGIN_PASSWORD = admin123
# VITE_APP_DEFAULT_LOGIN_TENANT = keyLove
# VITE_APP_DEFAULT_LOGIN_USERNAME = admin
# VITE_APP_DEFAULT_LOGIN_PASSWORD = admin123
# API 加解密
VITE_APP_API_ENCRYPT_ENABLE = true

View File

@@ -5,6 +5,7 @@ VITE_DEV=true
# 请求路径
VITE_BASE_URL='http://192.168.2.22:48081'
# VITE_BASE_URL='https://admin-xtm9a.loveamorkey.com'
# 文件上传类型server - 后端上传, client - 前端直连上传,仅支持 S3 服务
VITE_UPLOAD_TYPE=server
@@ -28,7 +29,7 @@ VITE_BASE_PATH=/
VITE_MALL_H5_DOMAIN='http://localhost:3000'
# 验证码的开关
VITE_APP_CAPTCHA_ENABLE=false
VITE_APP_CAPTCHA_ENABLE=true
# GoView域名
VITE_GOVIEW_URL='http://127.0.0.1:3000'

View File

@@ -4,8 +4,8 @@ NODE_ENV=production
VITE_DEV=false
# 请求路径
VITE_BASE_URL='http://localhost:48080'
# VITE_BASE_URL='http://localhost:48080'
VITE_BASE_URL='https://admin-xtm9a.loveamorkey.com'
# 文件上传类型server - 后端上传, client - 前端直连上传仅支持S3服务
VITE_UPLOAD_TYPE=server

View File

@@ -1,16 +1,25 @@
import request from '@/config/axios'
import type { Dayjs } from 'dayjs';
/** AI陪聊角色国际化表用于存储不同语言下的角色名称、一句话描述和详细介绍信息 */
export interface AiCompanionI18n {
id: number; // 主键ID
companionId?: number; // 陪聊角色主表ID对应 keyboard_ai_companion.id
locale?: string; // 语言标识,如 zh-CN、en-US、ja-JP
name?: string; // 角色名称(多语言)
shortDesc: string; // 一句话人设描述(多语言)
introText: string; // 角色详细介绍文案(多语言)
createdAt?: string | Dayjs; // 创建时间
updatedAt?: string | Dayjs; // 更新时间
}
/** AI陪聊角色表用于定义恋爱/陪伴型虚拟角色的基础信息与人设信息 */
export interface AiCompanion {
id: number; // 陪聊角色唯一ID
name?: string; // 角色名称展示用Katie Leona
avatarUrl: string; // 角色头像URL用于列表页和聊天页
coverImageUrl: string; // 角色封面图URL用于角色详情页
gender: string; // 角色性别male / female / other
ageRange: string; // 角色年龄段描述20s、25-30
shortDesc: string; // 一句话人设描述,用于卡片或列表展示
introText: string; // 角色详细介绍文案,用于角色详情页
personalityTags: object; // 角色性格标签数组(如:温柔、黏人、治愈)
speakingStyle: string; // 角色说话风格(如:撒娇型、理性型、活泼型)
systemPrompt?: string; // AI系统Prompt定义角色核心人设仅供模型使用
@@ -60,5 +69,36 @@ export const AiCompanionApi = {
// 导出AI陪聊角色表用于定义恋爱/陪伴型虚拟角色的基础信息与人设 Excel
exportAiCompanion: async (params) => {
return await request.download({ url: `/keyboard/ai-companion/export-excel`, params })
},
// ==================== 子表AI陪聊角色国际化表用于存储不同语言下的角色名称、一句话描述和详细介绍 ====================
// 获得AI陪聊角色国际化表用于存储不同语言下的角色名称、一句话描述和详细介绍分页
getAiCompanionI18nPage: async (params) => {
return await request.get({ url: `/keyboard/ai-companion/ai-companion-i18n/page`, params })
},
// 新增AI陪聊角色国际化表用于存储不同语言下的角色名称、一句话描述和详细介绍
createAiCompanionI18n: async (data: AiCompanionI18n) => {
return await request.post({ url: `/keyboard/ai-companion/ai-companion-i18n/create`, data })
},
// 修改AI陪聊角色国际化表用于存储不同语言下的角色名称、一句话描述和详细介绍
updateAiCompanionI18n: async (data: AiCompanionI18n) => {
return await request.put({ url: `/keyboard/ai-companion/ai-companion-i18n/update`, data })
},
// 删除AI陪聊角色国际化表用于存储不同语言下的角色名称、一句话描述和详细介绍
deleteAiCompanionI18n: async (id: number) => {
return await request.delete({ url: `/keyboard/ai-companion/ai-companion-i18n/delete?id=` + id })
},
/** 批量删除AI陪聊角色国际化表用于存储不同语言下的角色名称、一句话描述和详细介绍 */
deleteAiCompanionI18nList: async (ids: number[]) => {
return await request.delete({ url: `/keyboard/ai-companion/ai-companion-i18n/delete-list?ids=${ids.join(',')}` })
},
// 获得AI陪聊角色国际化表用于存储不同语言下的角色名称、一句话描述和详细介绍
getAiCompanionI18n: async (id: number) => {
return await request.get({ url: `/keyboard/ai-companion/ai-companion-i18n/get?id=` + id })
}
}

View File

@@ -0,0 +1,61 @@
import request from '@/config/axios'
import type { Dayjs } from 'dayjs';
/** App 版本发布与更新检查表:区分 Android/iOS、渠道支持最低支持版本与强更策略。信息 */
export interface AppVersions {
id: number; // 主键自增版本记录ID。
appId?: string; // 应用标识支持多App/多包名场景单App可固定为 main。
platform?: string; // 平台android 或 ios用 CHECK 约束限制取值)。
channel?: string; // 渠道标识:如 official / huawei / xiaomi / testflight 等,用于区分不同分发包。
versionName?: string; // 展示用版本号(语义版本字符串),如 1.2.3。
versionCode?: number; // 比较用版本号整数递增Android 对应 versionCodeiOS 建议维护同样的递增值以便比较。
buildNumber: string; // iOS 可选构建号(例如 CFBundleVersion通常为字符串用于追溯构建或与CI编号对齐。
minSupportedCode?: number; // 最低支持版本号(整数):客户端 version_code 低于该值必须更新/可拒绝继续使用。
isForceUpdate?: boolean; // 是否强制更新:当客户端未达到最新版本且此字段为 true可要求强更即使 >= min_supported_code
isActive?: boolean; // 是否生效true 表示该版本记录可用于对外更新检查false 用于下架/撤回。
releaseNotes: string; // 更新说明(展示给用户的版本更新内容)。
downloadUrl: string; // 下载链接Android 可为 apk 直链/市场 schemeiOS 通常为 App Store 链接或统一跳转页。
storeUrl: string; // 应用市场/商店页面链接(可选,若 download_url 已覆盖可不填)。
metadata?: object; // 扩展元数据JSON如包大小、md5、签名信息、最低系统版本等。
releasedAt?: string | Dayjs; // 发布时间(对外宣布/上线时间),用于展示与排序。
createdAt?: string | Dayjs; // 记录创建时间。
updatedAt?: string | Dayjs; // 记录更新时间(建议配合触发器自动维护)。
}
// App 版本发布与更新检查表:区分 Android/iOS、渠道支持最低支持版本与强更策略。 API
export const AppVersionsApi = {
// 查询App 版本发布与更新检查表:区分 Android/iOS、渠道支持最低支持版本与强更策略。分页
getAppVersionsPage: async (params: any) => {
return await request.get({ url: `/keyboard/app-versions/page`, params })
},
// 查询App 版本发布与更新检查表:区分 Android/iOS、渠道支持最低支持版本与强更策略。详情
getAppVersions: async (id: number) => {
return await request.get({ url: `/keyboard/app-versions/get?id=` + id })
},
// 新增App 版本发布与更新检查表:区分 Android/iOS、渠道支持最低支持版本与强更策略。
createAppVersions: async (data: AppVersions) => {
return await request.post({ url: `/keyboard/app-versions/create`, data })
},
// 修改App 版本发布与更新检查表:区分 Android/iOS、渠道支持最低支持版本与强更策略。
updateAppVersions: async (data: AppVersions) => {
return await request.put({ url: `/keyboard/app-versions/update`, data })
},
// 删除App 版本发布与更新检查表:区分 Android/iOS、渠道支持最低支持版本与强更策略。
deleteAppVersions: async (id: number) => {
return await request.delete({ url: `/keyboard/app-versions/delete?id=` + id })
},
/** 批量删除App 版本发布与更新检查表:区分 Android/iOS、渠道支持最低支持版本与强更策略。 */
deleteAppVersionsList: async (ids: number[]) => {
return await request.delete({ url: `/keyboard/app-versions/delete-list?ids=${ids.join(',')}` })
},
// 导出App 版本发布与更新检查表:区分 Android/iOS、渠道支持最低支持版本与强更策略。 Excel
exportAppVersions: async (params) => {
return await request.download({ url: `/keyboard/app-versions/export-excel`, params })
}
}

View File

@@ -1,6 +1,17 @@
import request from '@/config/axios'
import type { Dayjs } from 'dayjs';
/** 键盘人设国际化内容信息 */
export interface CharacterI18n {
id: number; // 主键 Id
characterId?: number; // 角色主表id
locale?: string; // 语言标识,如 zh-CN/en-US/ja-JP
characterName: string; // 标题
characterBackground: string; // 背景描述
createdAt: string | Dayjs; // 创建时间
updatedAt: string | Dayjs; // 更新时间
}
/** 键盘人设信息 */
export interface Character {
id: number; // 主键 Id
@@ -51,5 +62,36 @@ export const CharacterApi = {
// 导出键盘人设 Excel
exportCharacter: async (params) => {
return await request.download({ url: `/keyboard/character/export-excel`, params })
},
// ==================== 子表(键盘人设国际化内容) ====================
// 获得键盘人设国际化内容分页
getCharacterI18nPage: async (params) => {
return await request.get({ url: `/keyboard/character/character-i18n/page`, params })
},
// 新增键盘人设国际化内容
createCharacterI18n: async (data: CharacterI18n) => {
return await request.post({ url: `/keyboard/character/character-i18n/create`, data })
},
// 修改键盘人设国际化内容
updateCharacterI18n: async (data: CharacterI18n) => {
return await request.put({ url: `/keyboard/character/character-i18n/update`, data })
},
// 删除键盘人设国际化内容
deleteCharacterI18n: async (id: number) => {
return await request.delete({ url: `/keyboard/character/character-i18n/delete?id=` + id })
},
/** 批量删除键盘人设国际化内容 */
deleteCharacterI18nList: async (ids: number[]) => {
return await request.delete({ url: `/keyboard/character/character-i18n/delete-list?ids=${ids.join(',')}` })
},
// 获得键盘人设国际化内容
getCharacterI18n: async (id: number) => {
return await request.get({ url: `/keyboard/character/character-i18n/get?id=` + id })
}
}

View File

@@ -0,0 +1,53 @@
import request from '@/config/axios'
import type { Dayjs } from 'dayjs';
/** 键盘人设国际化内容信息 */
export interface CharacterI18n {
id: number; // 主键 Id
characterId?: number; // 角色主表id
locale?: string; // 语言标识,如 zh-CN/en-US/ja-JP
characterName: string; // 标题
characterBackground: string; // 背景描述
prompt: string; // 人设提示词
emoji: string; // emoji 标签
createdAt: string | Dayjs; // 创建时间
updatedAt: string | Dayjs; // 更新时间
}
// 键盘人设国际化内容 API
export const CharacterI18nApi = {
// 查询键盘人设国际化内容分页
getCharacterI18nPage: async (params: any) => {
return await request.get({ url: `/keyboard/character-i18n/page`, params })
},
// 查询键盘人设国际化内容详情
getCharacterI18n: async (id: number) => {
return await request.get({ url: `/keyboard/character-i18n/get?id=` + id })
},
// 新增键盘人设国际化内容
createCharacterI18n: async (data: CharacterI18n) => {
return await request.post({ url: `/keyboard/character-i18n/create`, data })
},
// 修改键盘人设国际化内容
updateCharacterI18n: async (data: CharacterI18n) => {
return await request.put({ url: `/keyboard/character-i18n/update`, data })
},
// 删除键盘人设国际化内容
deleteCharacterI18n: async (id: number) => {
return await request.delete({ url: `/keyboard/character-i18n/delete?id=` + id })
},
/** 批量删除键盘人设国际化内容 */
deleteCharacterI18nList: async (ids: number[]) => {
return await request.delete({ url: `/keyboard/character-i18n/delete-list?ids=${ids.join(',')}` })
},
// 导出键盘人设国际化内容 Excel
exportCharacterI18n: async (params) => {
return await request.download({ url: `/keyboard/character-i18n/export-excel`, params })
}
}

View File

@@ -1,10 +1,19 @@
import request from '@/config/axios'
import type { Dayjs } from 'dayjs';
/** 人设标签国际化信息 */
export interface TagI18n {
id: number; // 主键 Id
tagId?: number; // 标签主表ID对应 keyboard_tag.id
locale?: string; // 语言标识,如 zh-CN、en-US、ja-JP
tagName?: string; // 标签名称(多语言)
createdAt: string | Dayjs; // 创建时间
updatedAt: string | Dayjs; // 更新时间
}
/** 人设标签信息 */
export interface Tag {
id: number; // 主键 Id
tagName: string; // 标签名
createdAt: string | Dayjs; // 创建时间
updatedAt: string | Dayjs; // 更新时间
}
@@ -44,5 +53,36 @@ export const TagApi = {
// 导出人设标签 Excel
exportTag: async (params) => {
return await request.download({ url: `/keyboard/tag/export-excel`, params })
},
// ==================== 子表(人设标签国际化) ====================
// 获得人设标签国际化分页
getTagI18nPage: async (params) => {
return await request.get({ url: `/keyboard/tag/tag-i18n/page`, params })
},
// 新增人设标签国际化
createTagI18n: async (data: TagI18n) => {
return await request.post({ url: `/keyboard/tag/tag-i18n/create`, data })
},
// 修改人设标签国际化
updateTagI18n: async (data: TagI18n) => {
return await request.put({ url: `/keyboard/tag/tag-i18n/update`, data })
},
// 删除人设标签国际化
deleteTagI18n: async (id: number) => {
return await request.delete({ url: `/keyboard/tag/tag-i18n/delete?id=` + id })
},
/** 批量删除人设标签国际化 */
deleteTagI18nList: async (ids: number[]) => {
return await request.delete({ url: `/keyboard/tag/tag-i18n/delete-list?ids=${ids.join(',')}` })
},
// 获得人设标签国际化
getTagI18n: async (id: number) => {
return await request.get({ url: `/keyboard/tag/tag-i18n/get?id=` + id })
}
}

View File

@@ -0,0 +1,49 @@
import request from '@/config/axios'
import type { Dayjs } from 'dayjs';
/** 用户注销提示信息信息 */
export interface WarningMessage {
id: number; // 主键
locale: string; // 地区
content: string; // 正文
updatedAt: string | Dayjs; // 更新时间
created: string | Dayjs; // 创建时间
}
// 用户注销提示信息 API
export const WarningMessageApi = {
// 查询用户注销提示信息分页
getWarningMessagePage: async (params: any) => {
return await request.get({ url: `/keyboard/warning-message/page`, params })
},
// 查询用户注销提示信息详情
getWarningMessage: async (id: number) => {
return await request.get({ url: `/keyboard/warning-message/get?id=` + id })
},
// 新增用户注销提示信息
createWarningMessage: async (data: WarningMessage) => {
return await request.post({ url: `/keyboard/warning-message/create`, data })
},
// 修改用户注销提示信息
updateWarningMessage: async (data: WarningMessage) => {
return await request.put({ url: `/keyboard/warning-message/update`, data })
},
// 删除用户注销提示信息
deleteWarningMessage: async (id: number) => {
return await request.delete({ url: `/keyboard/warning-message/delete?id=` + id })
},
/** 批量删除用户注销提示信息 */
deleteWarningMessageList: async (ids: number[]) => {
return await request.delete({ url: `/keyboard/warning-message/delete-list?ids=${ids.join(',')}` })
},
// 导出用户注销提示信息 Excel
exportWarningMessage: async (params) => {
return await request.download({ url: `/keyboard/warning-message/export-excel`, params })
}
}

View File

@@ -5,13 +5,11 @@
<el-avatar :size="60">
<Icon icon="ep:avatar" :size="60" />
</el-avatar>
<span class="text-18px font-bold">芋道源码</span>
<span class="text-18px font-bold">keyLove</span>
</div>
<Icon icon="tdesign:qrcode" :size="20" />
</div>
<div
class="flex items-center justify-between justify-between bg-white p-x-20px p-y-8px text-12px"
>
<div class="flex items-center justify-between justify-between bg-white p-x-20px p-y-8px text-12px">
<span class="color-#ff690d">点击绑定手机号</span>
<span class="rounded-26px bg-#ff6100 p-x-8px p-y-5px color-white">去绑定</span>
</div>

View File

@@ -32,11 +32,7 @@
<XTextButton title="预览JSON" @click="previewProcessJson" />
</template>
</el-tooltip>
<el-tooltip
v-if="props.simulation"
effect="light"
:content="simulationStatus ? '退出模拟' : '开启模拟'"
>
<el-tooltip v-if="props.simulation" effect="light" :content="simulationStatus ? '退出模拟' : '开启模拟'">
<XButton preIcon="ep:cpu" title="模拟" @click="processSimulation" />
</el-tooltip>
</ElButtonGroup>
@@ -47,11 +43,7 @@
icon="el-icon-s-data"
@click="elementsAlign('left')"
/> -->
<XButton
preIcon="fa:align-left"
class="align align-bottom"
@click="elementsAlign('left')"
/>
<XButton preIcon="fa:align-left" class="align align-bottom" @click="elementsAlign('left')" />
</el-tooltip>
<el-tooltip effect="light" content="向右对齐">
<!-- <el-button
@@ -59,11 +51,7 @@
icon="el-icon-s-data"
@click="elementsAlign('right')"
/> -->
<XButton
preIcon="fa:align-left"
class="align align-top"
@click="elementsAlign('right')"
/>
<XButton preIcon="fa:align-left" class="align align-top" @click="elementsAlign('right')" />
</el-tooltip>
<el-tooltip effect="light" content="向上对齐">
<!-- <el-button
@@ -71,11 +59,7 @@
icon="el-icon-s-data"
@click="elementsAlign('top')"
/> -->
<XButton
preIcon="fa:align-left"
class="align align-left"
@click="elementsAlign('top')"
/>
<XButton preIcon="fa:align-left" class="align align-left" @click="elementsAlign('top')" />
</el-tooltip>
<el-tooltip effect="light" content="向下对齐">
<!-- <el-button
@@ -83,11 +67,7 @@
icon="el-icon-s-data"
@click="elementsAlign('bottom')"
/> -->
<XButton
preIcon="fa:align-left"
class="align align-right"
@click="elementsAlign('bottom')"
/>
<XButton preIcon="fa:align-left" class="align align-right" @click="elementsAlign('bottom')" />
</el-tooltip>
<el-tooltip effect="light" content="水平居中">
<!-- <el-button
@@ -96,11 +76,7 @@
@click="elementsAlign('center')"
/> -->
<!-- class="align align-center" -->
<XButton
preIcon="fa:align-left"
class="align align-center"
@click="elementsAlign('center')"
/>
<XButton preIcon="fa:align-left" class="align align-center" @click="elementsAlign('center')" />
</el-tooltip>
<el-tooltip effect="light" content="垂直居中">
<!-- <el-button
@@ -108,11 +84,7 @@
icon="el-icon-s-data"
@click="elementsAlign('middle')"
/> -->
<XButton
preIcon="fa:align-left"
class="align align-middle"
@click="elementsAlign('middle')"
/>
<XButton preIcon="fa:align-left" class="align align-middle" @click="elementsAlign('middle')" />
</el-tooltip>
</ElButtonGroup>
<ElButtonGroup key="scale-control">
@@ -122,11 +94,7 @@
icon="el-icon-zoom-out"
@click="processZoomOut()"
/> -->
<XButton
preIcon="ep:zoom-out"
@click="processZoomOut()"
:disabled="defaultZoom < 0.2"
/>
<XButton preIcon="ep:zoom-out" @click="processZoomOut()" :disabled="defaultZoom < 0.2" />
</el-tooltip>
<el-button>{{ Math.floor(defaultZoom * 10 * 10) + '%' }}</el-button>
<el-tooltip effect="light" content="放大视图">
@@ -162,32 +130,16 @@
</ElButtonGroup>
</template>
<!-- 用于打开本地文件-->
<input
type="file"
id="files"
ref="refFile"
style="display: none"
accept=".xml, .bpmn"
@change="importLocalFile"
/>
<input type="file" id="files" ref="refFile" style="display: none" accept=".xml, .bpmn"
@change="importLocalFile" />
</div>
<div class="my-process-designer__container">
<div
class="my-process-designer__canvas"
ref="bpmnCanvas"
id="bpmnCanvas"
style="width: 1680px; height: 800px"
></div>
<div class="my-process-designer__canvas" ref="bpmnCanvas" id="bpmnCanvas" style="width: 1680px; height: 800px">
</div>
<!-- <div id="js-properties-panel" class="panel"></div> -->
<!-- <div class="my-process-designer__canvas" ref="bpmn-canvas"></div> -->
</div>
<Dialog
title="预览"
v-model="previewModelVisible"
width="80%"
:scroll="true"
max-height="600px"
>
<Dialog title="预览" v-model="previewModelVisible" width="80%" :scroll="true" max-height="600px">
<div>
<pre><code v-dompurify-html="highlightedCode(previewResult)" class="hljs"></code></pre>
</div>
@@ -261,13 +213,13 @@ const props = defineProps({
translations: {
// 自定义的翻译文件
type: Object,
default: () => {}
default: () => { }
},
additionalModel: [Object, Array], // 自定义model
moddleExtension: {
// 自定义moddle
type: Object,
default: () => {}
default: () => { }
},
onlyCustomizeAddi: {
type: Boolean,
@@ -542,8 +494,7 @@ const setEncoded = (type, data) => {
const encodedData = encodeURIComponent(data)
return {
filename: `${filename}.${type}`,
href: `data:application/${
type === 'svg' ? 'text/xml' : 'bpmn20-xml'
href: `data:application/${type === 'svg' ? 'text/xml' : 'bpmn20-xml'
};charset=UTF-8,${encodedData}`,
data: data
}
@@ -642,7 +593,7 @@ const previewProcessJson = () => {
})
}
/* ------------------------------------------------ 芋道源码 methods ------------------------------------------------------ */
/* ------------------------------------------------ keyLove methods ------------------------------------------------------ */
onMounted(() => {
initBpmnModeler()
createNewDiagram(props.value)

View File

@@ -1,5 +1,5 @@
/**
* Created by 芋道源码
* Created by keyLove
*
* 枚举类
*/

View File

@@ -1,12 +1,8 @@
<template>
<div
:class="prefixCls"
class="relative h-[100%] lt-md:px-10px lt-sm:px-10px lt-xl:px-10px lt-xl:px-10px"
>
<div :class="prefixCls" class="relative h-[100%] lt-md:px-10px lt-sm:px-10px lt-xl:px-10px lt-xl:px-10px">
<div class="relative mx-auto h-full flex">
<div
:class="`${prefixCls}__left flex-1 bg-gray-500 bg-opacity-20 relative p-30px lt-xl:hidden overflow-x-hidden overflow-y-auto`"
>
:class="`${prefixCls}__left flex-1 bg-gray-500 bg-opacity-20 relative p-30px lt-xl:hidden overflow-x-hidden overflow-y-auto`">
<!-- 左上角的 logo + 系统标题 -->
<div class="relative flex items-center text-white">
<img alt="" class="mr-10px h-48px w-48px" src="@/assets/imgs/logo.png" />
@@ -14,11 +10,7 @@
</div>
<!-- 左边的背景图 + 欢迎语 -->
<div class="h-[calc(100%-60px)] flex items-center justify-center">
<TransitionGroup
appear
enter-active-class="animate__animated animate__bounceInLeft"
tag="div"
>
<TransitionGroup appear enter-active-class="animate__animated animate__bounceInLeft" tag="div">
<img key="1" alt="" class="w-350px" src="@/assets/svgs/login-box-bg.svg" />
<div key="2" class="text-3xl text-white">{{ t('login.welcome') }}</div>
<div key="3" class="mt-5 text-14px font-normal text-white">
@@ -28,12 +20,9 @@
</div>
</div>
<div
class="relative flex-1 p-30px dark:bg-[var(--login-bg-color)] lt-sm:p-10px overflow-x-hidden overflow-y-auto"
>
class="relative flex-1 p-30px dark:bg-[var(--login-bg-color)] lt-sm:p-10px overflow-x-hidden overflow-y-auto">
<!-- 右上角的主题语言选择 -->
<div
class="flex items-center justify-between text-white at-2xl:justify-end at-xl:justify-end"
>
<div class="flex items-center justify-between text-white at-2xl:justify-end at-xl:justify-end">
<div class="flex items-center at-2xl:hidden at-xl:hidden">
<img alt="" class="mr-10px h-48px w-48px" src="@/assets/imgs/logo.png" />
<span class="text-20px font-bold">{{ underlineToHump(appStore.getTitle) }}</span>
@@ -46,19 +35,10 @@
<!-- 右边的登录界面 -->
<Transition appear enter-active-class="animate__animated animate__bounceInRight">
<div
class="m-auto h-[calc(100%-60px)] w-[100%] flex items-center at-2xl:max-w-500px at-lg:max-w-500px at-md:max-w-500px at-xl:max-w-500px"
>
class="m-auto h-[calc(100%-60px)] w-[100%] flex items-center at-2xl:max-w-500px at-lg:max-w-500px at-md:max-w-500px at-xl:max-w-500px">
<!-- 账号登录 -->
<el-form
v-show="getShow"
ref="formLogin"
:model="loginData.loginForm"
:rules="LoginRules"
class="login-form"
label-position="top"
label-width="120px"
size="large"
>
<el-form v-show="getShow" ref="formLogin" :model="loginData.loginForm" :rules="LoginRules"
class="login-form" label-position="top" label-width="120px" size="large">
<el-row style="margin-right: -10px; margin-left: -10px">
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item>
@@ -67,45 +47,28 @@
</el-col>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item v-if="loginData.tenantEnable" prop="tenantName">
<el-input
v-model="loginData.loginForm.tenantName"
:placeholder="t('login.tenantNamePlaceholder')"
:prefix-icon="iconHouse"
link
type="primary"
/>
<el-input v-model="loginData.loginForm.tenantName" :placeholder="t('login.tenantNamePlaceholder')"
:prefix-icon="iconHouse" link type="primary" />
</el-form-item>
</el-col>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item prop="username">
<el-input
v-model="loginData.loginForm.username"
:placeholder="t('login.usernamePlaceholder')"
:prefix-icon="iconAvatar"
/>
<el-input v-model="loginData.loginForm.username" :placeholder="t('login.usernamePlaceholder')"
:prefix-icon="iconAvatar" />
</el-form-item>
</el-col>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item prop="password">
<el-input
v-model="loginData.loginForm.password"
:placeholder="t('login.passwordPlaceholder')"
:prefix-icon="iconLock"
show-password
type="password"
@keyup.enter="getCode()"
/>
<el-input v-model="loginData.loginForm.password" :placeholder="t('login.passwordPlaceholder')"
:prefix-icon="iconLock" show-password type="password" @keyup.enter="getCode()" />
</el-form-item>
</el-col>
<el-col
:span="24"
style="
<el-col :span="24" style="
padding-right: 10px;
padding-left: 10px;
margin-top: -20px;
margin-bottom: -20px;
"
>
">
<el-form-item>
<el-row justify="space-between" style="width: 100%">
<el-col :span="6">
@@ -114,8 +77,7 @@
</el-checkbox>
</el-col>
<el-col :offset="6" :span="12">
<el-link style="float: right" type="primary"
>{{ t('login.forgetPassword') }}
<el-link style="float: right" type="primary">{{ t('login.forgetPassword') }}
</el-link>
</el-col>
</el-row>
@@ -123,23 +85,12 @@
</el-col>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item>
<XButton
:loading="loginLoading"
:title="t('login.login')"
class="w-[100%]"
type="primary"
@click="getCode()"
/>
<XButton :loading="loginLoading" :title="t('login.login')" class="w-[100%]" type="primary"
@click="getCode()" />
</el-form-item>
</el-col>
<Verify
v-if="loginData.captchaEnable === 'true'"
ref="verify"
:captchaType="captchaType"
:imgSize="{ width: '400px', height: '200px' }"
mode="pop"
@success="handleLogin"
/>
<Verify v-if="loginData.captchaEnable === 'true'" ref="verify" :captchaType="captchaType"
:imgSize="{ width: '400px', height: '200px' }" mode="pop" @success="handleLogin" />
</el-row>
</el-form>
</div>
@@ -199,7 +150,7 @@ const loginData = reactive({
captchaEnable: import.meta.env.VITE_APP_CAPTCHA_ENABLE !== 'false',
tenantEnable: import.meta.env.VITE_APP_TENANT_ENABLE !== 'false',
loginForm: {
tenantName: '芋道源码',
tenantName: 'keyLove',
username: 'admin',
password: 'admin123',
captchaVerification: '',
@@ -258,7 +209,7 @@ const tryLogin = async () => {
authUtil.setToken(res)
router.push({ path: redirect || '/' })
} catch (err) {}
} catch (err) { }
}
// 登录

View File

@@ -1,14 +1,6 @@
<template>
<el-form
v-show="getShow"
ref="formSmsLogin"
:model="loginData.loginForm"
:rules="rules"
class="login-form"
label-position="top"
label-width="120px"
size="large"
>
<el-form v-show="getShow" ref="formSmsLogin" :model="loginData.loginForm" :rules="rules" class="login-form"
label-position="top" label-width="120px" size="large">
<el-row class="mx-[-10px]">
<!-- 租户名 -->
<el-col :span="24" class="px-10px">
@@ -18,23 +10,15 @@
</el-col>
<el-col :span="24" class="px-10px">
<el-form-item v-if="loginData.tenantEnable === 'true'" prop="tenantName">
<el-input
v-model="loginData.loginForm.tenantName"
:placeholder="t('login.tenantNamePlaceholder')"
:prefix-icon="iconHouse"
type="primary"
link
/>
<el-input v-model="loginData.loginForm.tenantName" :placeholder="t('login.tenantNamePlaceholder')"
:prefix-icon="iconHouse" type="primary" link />
</el-form-item>
</el-col>
<!-- 手机号 -->
<el-col :span="24" class="px-10px">
<el-form-item prop="mobileNumber">
<el-input
v-model="loginData.loginForm.mobileNumber"
:placeholder="t('login.mobileNumberPlaceholder')"
:prefix-icon="iconCellphone"
/>
<el-input v-model="loginData.loginForm.mobileNumber" :placeholder="t('login.mobileNumberPlaceholder')"
:prefix-icon="iconCellphone" />
</el-form-item>
</el-col>
<!-- 验证码 -->
@@ -42,19 +26,11 @@
<el-form-item prop="code">
<el-row :gutter="5" justify="space-between" style="width: 100%">
<el-col :span="24">
<el-input
v-model="loginData.loginForm.code"
:placeholder="t('login.codePlaceholder')"
:prefix-icon="iconCircleCheck"
>
<el-input v-model="loginData.loginForm.code" :placeholder="t('login.codePlaceholder')"
:prefix-icon="iconCircleCheck">
<!-- <el-button class="w-[100%]"> -->
<template #append>
<span
v-if="mobileCodeTimer <= 0"
class="getMobileCode"
style="cursor: pointer"
@click="getSmsCode"
>
<span v-if="mobileCodeTimer <= 0" class="getMobileCode" style="cursor: pointer" @click="getSmsCode">
{{ t('login.getSmsCode') }}
</span>
<span v-if="mobileCodeTimer > 0" class="getMobileCode" style="cursor: pointer">
@@ -70,23 +46,12 @@
<!-- 登录按钮 / 返回按钮 -->
<el-col :span="24" class="px-10px">
<el-form-item>
<XButton
:loading="loginLoading"
:title="t('login.login')"
class="w-full"
type="primary"
@click="signIn()"
/>
<XButton :loading="loginLoading" :title="t('login.login')" class="w-full" type="primary" @click="signIn()" />
</el-form-item>
</el-col>
<el-col :span="24" class="px-10px">
<el-form-item>
<XButton
:loading="loginLoading"
:title="t('login.backLogin')"
class="w-full"
@click="handleBackLogin()"
/>
<XButton :loading="loginLoading" :title="t('login.backLogin')" class="w-full" @click="handleBackLogin()" />
</el-form-item>
</el-col>
</el-row>
@@ -133,7 +98,7 @@ const loginData = reactive({
},
loginForm: {
uuid: '',
tenantName: '芋道源码',
tenantName: 'keyLove',
mobileNumber: '',
code: ''
}
@@ -202,7 +167,7 @@ const signIn = async () => {
}
push({ path: redirect.value || permissionStore.addRouters[0].path })
})
.catch(() => {})
.catch(() => { })
.finally(() => {
loginLoading.value = false
setTimeout(() => {

View File

@@ -1,5 +1,5 @@
/**
* Created by 芋道源码
* Created by keyLove
*
* AI 枚举类
*

View File

@@ -1,5 +1,5 @@
/**
* Created by 芋道源码
* Created by keyLove
*
* AI 枚举类
*

View File

@@ -1,22 +1,14 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="100px"
v-loading="formLoading"
>
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px" v-loading="formLoading">
<el-form-item label="风格名称" prop="styleName">
<el-input v-model="formData.styleName" placeholder="请输入风格名称" />
</el-form-item>
<el-form-item label="所属地区" prop="local">
<el-input v-model="formData.local" placeholder="请输入所属地区" />
</el-form-item>
<el-form-item label="创建时间" prop="createdAt">
<el-date-picker
v-model="formData.createdAt"
type="date"
value-format="x"
placeholder="选择创建时间"
/>
<el-date-picker v-model="formData.createdAt" type="date" value-format="x" placeholder="选择创建时间" />
</el-form-item>
<el-form-item label="更新时间" prop="updatedAt">
<el-input v-model="formData.updatedAt" placeholder="请输入更新时间" />
@@ -44,6 +36,7 @@ const formType = ref('') // 表单的类型create - 新增update - 修改
const formData = ref({
id: undefined,
styleName: undefined,
local: undefined,
createdAt: undefined,
updatedAt: undefined
})

View File

@@ -1,68 +1,36 @@
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form class="-mb-15px" :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
<el-form-item label="风格名称" prop="styleName">
<el-input
v-model="queryParams.styleName"
placeholder="请输入风格名称"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
<el-input v-model="queryParams.styleName" placeholder="请输入风格名称" clearable @keyup.enter="handleQuery"
class="!w-240px" />
</el-form-item>
<el-form-item label="创建时间" prop="createdAt">
<el-date-picker
v-model="queryParams.createdAt"
value-format="YYYY-MM-DD"
type="date"
placeholder="选择创建时间"
clearable
class="!w-240px"
/>
<el-date-picker v-model="queryParams.createdAt" value-format="YYYY-MM-DD" type="date" placeholder="选择创建时间"
clearable class="!w-240px" />
</el-form-item>
<el-form-item label="更新时间" prop="updatedAt">
<el-input
v-model="queryParams.updatedAt"
placeholder="请输入更新时间"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
<el-input v-model="queryParams.updatedAt" placeholder="请输入更新时间" clearable @keyup.enter="handleQuery"
class="!w-240px" />
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['keyboard-server:keyboard-theme-styles:create']"
>
<el-button @click="handleQuery">
<Icon icon="ep:search" class="mr-5px" /> 搜索
</el-button>
<el-button @click="resetQuery">
<Icon icon="ep:refresh" class="mr-5px" /> 重置
</el-button>
<el-button type="primary" plain @click="openForm('create')"
v-hasPermi="['keyboard-server:keyboard-theme-styles:create']">
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-button
type="success"
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['keyboard-server:keyboard-theme-styles:export']"
>
<el-button type="success" plain @click="handleExport" :loading="exportLoading"
v-hasPermi="['keyboard-server:keyboard-theme-styles:export']">
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
<el-button
type="danger"
plain
:disabled="isEmpty(checkedIds)"
@click="handleDeleteBatch"
v-hasPermi="['keyboard-server:keyboard-theme-styles:delete']"
>
<el-button type="danger" plain :disabled="isEmpty(checkedIds)" @click="handleDeleteBatch"
v-hasPermi="['keyboard-server:keyboard-theme-styles:delete']">
<Icon icon="ep:delete" class="mr-5px" /> 批量删除
</el-button>
</el-form-item>
@@ -71,53 +39,30 @@
<!-- 列表 -->
<ContentWrap>
<el-table
row-key="id"
v-loading="loading"
:data="list"
:stripe="true"
:show-overflow-tooltip="true"
@selection-change="handleRowCheckboxChange"
>
<el-table row-key="id" v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true"
@selection-change="handleRowCheckboxChange">
<el-table-column type="selection" width="55" />
<el-table-column label="主键 Id" align="center" prop="id" />
<el-table-column label="风格名称" align="center" prop="styleName" />
<el-table-column
label="创建时间"
align="center"
prop="createdAt"
:formatter="dateFormatter"
width="180px"
/>
<el-table-column label="所属地区" align="center" prop="local" />
<el-table-column label="创建时间" align="center" prop="createdAt" :formatter="dateFormatter" width="180px" />
<el-table-column label="更新时间" align="center" prop="updatedAt" />
<el-table-column label="操作" align="center" min-width="120px">
<template #default="scope">
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['keyboard-server:keyboard-theme-styles:update']"
>
<el-button link type="primary" @click="openForm('update', scope.row.id)"
v-hasPermi="['keyboard-server:keyboard-theme-styles:update']">
编辑
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['keyboard-server:keyboard-theme-styles:delete']"
>
<el-button link type="danger" @click="handleDelete(scope.row.id)"
v-hasPermi="['keyboard-server:keyboard-theme-styles:delete']">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
<Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize"
@pagination="getList" />
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
@@ -191,7 +136,7 @@ const handleDelete = async (id: number) => {
message.success(t('common.delSuccess'))
// 刷新列表
await getList()
} catch {}
} catch { }
}
/** 批量删除主题风格 */
@@ -203,7 +148,7 @@ const handleDeleteBatch = async () => {
checkedIds.value = [];
message.success(t('common.delSuccess'))
await getList();
} catch {}
} catch { }
}
const checkedIds = ref<number[]>([])

View File

@@ -225,9 +225,9 @@ const queryFormRef = ref() // 搜索的表单
const exportLoading = ref(false) // 导出的加载中
const genderFormatter = (_row: KeyboardUser, _column: any, value: number) => {
if (value === 1) return '男'
if (value === 2) return '女'
if (value === 3) return '特殊'
if (value === 0) return '男'
if (value === 1) return '女'
if (value === 2) return '两性'
return ''
}
@@ -238,9 +238,9 @@ const yesNoFormatter = (_row: KeyboardUser, _column: any, value: boolean) => {
}
const genderTagType = (value: number) => {
if (value === 1) return 'success'
if (value === 2) return 'warning'
if (value === 3) return 'info'
if (value === 0) return 'success'
if (value === 1) return 'warning'
if (value === 2) return 'info'
return 'info'
}

View File

@@ -1,36 +1,7 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible" width="700px">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="100px"
v-loading="formLoading"
>
<!-- 基础信息 -->
<el-divider content-position="left">基础信息</el-divider>
<el-form-item label="角色名称" prop="name">
<el-input v-model="formData.name" placeholder="如Katie Leona" />
</el-form-item>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="性别" prop="gender">
<el-select v-model="formData.gender" placeholder="请选择性别">
<el-option label="男" value="male" />
<el-option label="女" value="female" />
<el-option label="其他" value="other" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="年龄段" prop="ageRange">
<el-input v-model="formData.ageRange" placeholder="如20s、25-30" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="头像" prop="avatarUrl">
<Dialog :title="dialogTitle" v-model="dialogVisible">
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px" v-loading="formLoading">
<el-form-item label="角色头像" prop="avatarUrl">
<div class="flex items-center gap-12px">
<el-avatar v-if="formData.avatarUrl" :src="formData.avatarUrl" :size="50" />
<el-button type="primary" plain @click="avatarInputRef?.click()">
@@ -39,32 +10,20 @@
<el-button v-if="formData.avatarUrl" type="danger" plain @click="formData.avatarUrl = undefined">
删除
</el-button>
<input
ref="avatarInputRef"
type="file"
accept="image/*"
class="hidden"
@change="onAvatarFileChange"
/>
<input ref="avatarInputRef" type="file" accept="image/*" class="hidden" @change="onAvatarFileChange" />
</div>
<!-- 头像裁剪弹窗 -->
<Dialog v-model="cropperVisible" title="裁剪头像50 x 50" width="800px" :canFullscreen="false">
<div class="flex gap-20px">
<div class="flex-1">
<CropperImage
v-if="avatarCropSrc"
:src="avatarCropSrc"
height="300px"
:options="{ aspectRatio: 1, viewMode: 1 }"
@ready="onCropperReady"
@cropend="onCropend"
/>
<CropperImage v-if="avatarCropSrc" :src="avatarCropSrc" height="300px"
:options="{ aspectRatio: 1, viewMode: 1 }" @ready="onCropperReady" @cropend="onCropend" />
</div>
<div class="flex flex-col items-center justify-center">
<div class="w-50px h-50px overflow-hidden border border-gray-300 rounded-full">
<img v-if="avatarPreview" :src="avatarPreview" class="w-full h-full" />
</div>
<span class="mt-8px text-12px text-gray-400">50 x 50 预览</span>
<span class="mt-8px text-12px text-gray-400">200x 200 预览</span>
</div>
</div>
<template #footer>
@@ -75,77 +34,40 @@
</template>
</Dialog>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="封面图" prop="coverImageUrl">
<UploadImg v-model="formData.coverImageUrl" directory="ai_companion_cover" />
<el-form-item label="角色封面图" prop="coverImageUrl">
<div class="flex items-center gap-12px">
<el-image v-if="formData.coverImageUrl" :src="formData.coverImageUrl" fit="cover" class="cover-image-preview"
:preview-src-list="[formData.coverImageUrl]" />
<el-button type="primary" plain @click="coverInputRef?.click()">
{{ formData.coverImageUrl ? '重新上传' : '选择图片' }}
</el-button>
<el-button v-if="formData.coverImageUrl" type="danger" plain @click="formData.coverImageUrl = undefined">
删除
</el-button>
<input ref="coverInputRef" type="file" accept="image/*" class="hidden" @change="onCoverFileChange" />
</div>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="一句话简介" prop="shortDesc">
<el-input v-model="formData.shortDesc" placeholder="用于卡片或列表展示" />
<el-form-item label="性别" prop="gender">
<el-input v-model="formData.gender" placeholder="male / female / other" />
</el-form-item>
<el-form-item label="详细介绍" prop="introText">
<el-input v-model="formData.introText" type="textarea" :rows="3" placeholder="用于角色详情页" />
<el-form-item label="年龄段" prop="ageRange">
<el-input v-model="formData.ageRange" placeholder="20s、25-30" />
</el-form-item>
<!-- 人设配置 -->
<el-divider content-position="left">人设配置</el-divider>
<el-form-item label="性格标签" prop="personalityTags">
<div class="w-full">
<div class="flex flex-wrap">
<el-tag
v-for="(tag, index) in formData.personalityTags"
:key="index"
class="mr-5px mb-5px"
closable
@close="removeTag(index)"
>
{{ tag }}
</el-tag>
<span v-if="!formData.personalityTags?.length" class="text-gray-400">暂无标签</span>
</div>
<div class="mt-8px flex items-center gap-8px">
<el-input v-model="newTag" placeholder="输入标签后回车" class="!w-200px" @keyup.enter="addTag" />
<el-button type="primary" plain @click="addTag">添加</el-button>
</div>
</div>
<el-input v-model="formData.personalityTags" placeholder="如:温柔、黏人、治愈" />
</el-form-item>
<el-form-item label="说话风格" prop="speakingStyle">
<el-input v-model="formData.speakingStyle" placeholder="如:撒娇型、理性型、活泼型" />
</el-form-item>
<el-form-item label="系统 Prompt" prop="systemPrompt">
<el-input
v-model="formData.systemPrompt"
type="textarea"
:rows="5"
placeholder="定义角色核心人设,仅供 AI 模型使用"
/>
<el-form-item label="系统Prompt" prop="systemPrompt">
<el-input type="textarea" v-model="formData.systemPrompt" placeholder="定义角色核心人设" />
</el-form-item>
<!-- 语音与开场白 -->
<el-divider content-position="left">语音与开场白</el-divider>
<el-form-item label="开场白" prop="prologue">
<el-input v-model="formData.prologue" type="textarea" :rows="2" placeholder="角色的开场白文案" />
</el-form-item>
<el-form-item label="开场白音频" prop="prologueAudio">
<el-input v-model="formData.prologueAudio" placeholder="开场白音频 URL" />
</el-form-item>
<el-form-item label="音色 ID" prop="voiceId">
<el-input v-model="formData.voiceId" placeholder="TTS 音色 ID" />
</el-form-item>
<!-- 运营配置 -->
<el-divider content-position="left">运营配置</el-divider>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="状态" prop="status">
<el-radio-group v-model="formData.status">
<el-radio :value="1">上线</el-radio>
<el-radio :value="0">下线</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="可见性" prop="visibility">
<el-select v-model="formData.visibility" placeholder="请选择可见性">
<el-option label="公开" :value="1" />
@@ -153,20 +75,27 @@
<el-option label="隐藏" :value="3" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="排序权重" prop="sortOrder">
<el-input-number v-model="formData.sortOrder" :min="0" controls-position="right" />
<el-input v-model="formData.sortOrder" placeholder="数值越大排序越靠前" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="热度评分" prop="popularityScore">
<el-input-number v-model="formData.popularityScore" :min="0" :precision="1" controls-position="right" />
<el-input v-model="formData.popularityScore" placeholder="用于推荐排序" />
</el-form-item>
<!-- <el-form-item label="创建时间" prop="createdAt">
<el-date-picker v-model="formData.createdAt" type="date" value-format="x" placeholder="选择创建时间" />
</el-form-item>
<el-form-item label="更新时间" prop="updatedAt">
<el-date-picker v-model="formData.updatedAt" type="date" value-format="x" placeholder="选择更新时间" />
</el-form-item> -->
<el-form-item label="开场白" prop="prologue">
<el-input v-model="formData.prologue" placeholder="请输入开场白" />
</el-form-item>
<el-form-item label="开场白音频" prop="prologueAudio">
<el-input v-model="formData.prologueAudio" placeholder="请输入开场白音频URL" />
</el-form-item>
<el-form-item label="音频Id" prop="voiceId">
<el-input v-model="formData.voiceId" placeholder="请输入音频Id" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading"> </el-button>
@@ -174,52 +103,51 @@
</template>
</Dialog>
</template>
<script setup lang="ts">
import { AiCompanionApi, AiCompanion } from '@/api/keyboard/aicompanion'
import { UploadImg } from '@/components/UploadFile'
import { CropperImage } from '@/components/Cropper'
import * as FileApi from '@/api/infra/file'
import type { Cropper } from 'cropperjs'
/** AI陪聊角色表用于定义恋爱/陪伴型虚拟角色的基础信息与人设 表单 */
defineOptions({ name: 'AiCompanionForm' })
const { t } = useI18n()
const message = useMessage()
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const dialogVisible = ref(false)
const dialogTitle = ref('')
const formLoading = ref(false)
const formType = ref('')
const dialogVisible = ref(false) // 弹窗的是否展示
const dialogTitle = ref('') // 弹窗的标题
const formLoading = ref(false) // 表单的加载中1修改时的数据加载2提交的按钮禁用
const formType = ref('') // 表单的类型create - 新增update - 修改
const formData = ref({
id: undefined,
name: undefined,
avatarUrl: undefined,
coverImageUrl: undefined,
gender: undefined,
ageRange: undefined,
shortDesc: undefined,
introText: undefined,
personalityTags: [] as string[],
personalityTags: undefined,
speakingStyle: undefined,
systemPrompt: undefined,
status: 1,
visibility: 1,
sortOrder: 0,
popularityScore: 0,
status: undefined,
visibility: undefined,
sortOrder: undefined,
popularityScore: undefined,
createdAt: undefined,
updatedAt: undefined,
prologue: undefined,
prologueAudio: undefined,
voiceId: undefined
})
const formRules = reactive({
name: [{ required: true, message: '角色名称不能为空', trigger: 'blur' }],
systemPrompt: [{ required: true, message: '系统 Prompt 不能为空', trigger: 'blur' }],
status: [{ required: true, message: '请选择状态', trigger: 'change' }],
visibility: [{ required: true, message: '请选择可见性', trigger: 'change' }]
systemPrompt: [{ required: true, message: '系统Prompt不能为空', trigger: 'blur' }],
status: [{ required: true, message: '状态不能为空', trigger: 'blur' }],
visibility: [{ required: true, message: '可见性不能为空', trigger: 'blur' }],
// createdAt: [{ required: true, message: '创建时间不能为空', trigger: 'blur' }],
// updatedAt: [{ required: true, message: '更新时间不能为空', trigger: 'blur' }]
})
const formRef = ref()
const formRef = ref() // 表单 Ref
/** 头像裁剪上传 */
/** 头像上传 */
const avatarInputRef = ref<HTMLInputElement>()
const cropperVisible = ref(false)
const avatarCropSrc = ref('')
@@ -227,6 +155,10 @@ const avatarPreview = ref('')
const avatarUploading = ref(false)
let cropperInstance: Cropper | null = null
/** 封面图上传 */
const coverInputRef = ref<HTMLInputElement>()
const coverUploading = ref(false)
const onAvatarFileChange = (e: Event) => {
const file = (e.target as HTMLInputElement).files?.[0]
if (!file) return
@@ -238,17 +170,20 @@ const onAvatarFileChange = (e: Event) => {
}
reader.readAsDataURL(file)
// 清空 input 以便重复选择同一文件
;(e.target as HTMLInputElement).value = ''
; (e.target as HTMLInputElement).value = ''
}
const onCropperReady = (cropper: Cropper) => {
cropperInstance = cropper
}
const onCropend = ({ imgBase64 }: { imgBase64: string }) => {
avatarPreview.value = imgBase64
}
const confirmCropAvatar = async () => {
if (!cropperInstance) return
const canvas = cropperInstance.getCroppedCanvas({ width: 50, height: 50 })
const canvas = cropperInstance.getCroppedCanvas({ width: 200, height: 200 })
canvas.toBlob(async (blob) => {
if (!blob) return
avatarUploading.value = true
@@ -264,31 +199,19 @@ const confirmCropAvatar = async () => {
}, 'image/png')
}
/** 性格标签 */
const newTag = ref('')
const normalizePersonalityTags = (raw: unknown): string[] => {
if (!raw) return []
if (Array.isArray(raw)) return raw.map(String)
if (typeof raw === 'string') {
const onCoverFileChange = async (e: Event) => {
const file = (e.target as HTMLInputElement).files?.[0]
if (!file) return
coverUploading.value = true
try {
const parsed = JSON.parse(raw)
if (Array.isArray(parsed)) return parsed.map(String)
} catch {
return raw.split(/[,]/).map((s) => s.trim()).filter(Boolean)
const res = await FileApi.updateFile({ file, directory: 'ai_companion_cover' })
formData.value.coverImageUrl = res.data
message.success('封面图上传成功')
} finally {
coverUploading.value = false
// 清空 input 以便重复选择同一文件
; (e.target as HTMLInputElement).value = ''
}
}
return []
}
const addTag = () => {
const text = newTag.value.trim()
if (!text) return
if (!formData.value.personalityTags.includes(text)) {
formData.value.personalityTags = [...formData.value.personalityTags, text]
}
newTag.value = ''
}
const removeTag = (index: number) => {
formData.value.personalityTags = formData.value.personalityTags.filter((_, i) => i !== index)
}
/** 打开弹窗 */
@@ -297,25 +220,24 @@ const open = async (type: string, id?: number) => {
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
// 修改时,设置数据
if (id) {
formLoading.value = true
try {
const data = await AiCompanionApi.getAiCompanion(id)
formData.value = {
...data,
personalityTags: normalizePersonalityTags(data.personalityTags)
}
formData.value = await AiCompanionApi.getAiCompanion(id)
} finally {
formLoading.value = false
}
}
}
defineExpose({ open })
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
/** 提交表单 */
const emit = defineEmits(['success'])
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
// 校验表单
await formRef.value.validate()
// 提交请求
formLoading.value = true
try {
const data = formData.value as unknown as AiCompanion
@@ -327,6 +249,7 @@ const submitForm = async () => {
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
// 发送操作成功的事件
emit('success')
} finally {
formLoading.value = false
@@ -337,20 +260,19 @@ const submitForm = async () => {
const resetForm = () => {
formData.value = {
id: undefined,
name: undefined,
avatarUrl: undefined,
coverImageUrl: undefined,
gender: undefined,
ageRange: undefined,
shortDesc: undefined,
introText: undefined,
personalityTags: [] as string[],
personalityTags: undefined,
speakingStyle: undefined,
systemPrompt: undefined,
status: 1,
visibility: 1,
sortOrder: 0,
popularityScore: 0,
status: undefined,
visibility: undefined,
sortOrder: undefined,
popularityScore: undefined,
createdAt: undefined,
updatedAt: undefined,
prologue: undefined,
prologueAudio: undefined,
voiceId: undefined
@@ -358,3 +280,22 @@ const resetForm = () => {
formRef.value?.resetFields()
}
</script>
<style scoped>
.cover-image-preview {
width: 100px;
height: 60px;
border-radius: 4px;
cursor: pointer;
}
/* 表单布局优化 */
.el-form-item {
margin-bottom: 16px;
}
/* 按钮间距 */
.el-button {
margin-right: 8px;
}
</style>

View File

@@ -0,0 +1,141 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible">
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px" v-loading="formLoading">
<!-- <el-form-item label="主表ID" prop="companionId">
<el-input v-model="formData.companionId" placeholder="请输入主表ID" />
</el-form-item> -->
<el-form-item label="语言标识" prop="locale">
<el-input v-model="formData.locale" placeholder="请选择语言标识" />
<!-- <el-select v-model="formData.locale" placeholder="请选择语言标识">
<el-option label="中文(简体)" value="zh-CN" />
<el-option label="英文(美国)" value="en-US" />
<el-option label="日文" value="ja-JP" />
<el-option label="韩文" value="ko-KR" />
<el-option label="法文" value="fr-FR" />
<el-option label="德文" value="de-DE" />
<el-option label="西班牙文" value="es-ES" />
</el-select> -->
</el-form-item>
<el-form-item label="角色名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入角色名称" />
</el-form-item>
<el-form-item label="一句话描述" prop="shortDesc">
<el-input v-model="formData.shortDesc" placeholder="请输入一句话人设描述" />
</el-form-item>
<el-form-item label="详细介绍" prop="introText">
<el-input v-model="formData.introText" placeholder="请输入角色详细介绍" type="textarea" :rows="4" />
</el-form-item>
<!-- <el-form-item label="创建时间" prop="createdAt">
<el-date-picker v-model="formData.createdAt" type="date" value-format="x" placeholder="选择创建时间" />
</el-form-item>
<el-form-item label="更新时间" prop="updatedAt">
<el-date-picker v-model="formData.updatedAt" type="date" value-format="x" placeholder="选择更新时间" />
</el-form-item> -->
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { AiCompanionApi, AiCompanionI18n } from '@/api/keyboard/aicompanion'
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗的是否展示
const dialogTitle = ref('') // 弹窗的标题
const formLoading = ref(false) // 表单的加载中1修改时的数据加载2提交的按钮禁用
const formType = ref('') // 表单的类型create - 新增update - 修改
const formData = ref({
id: undefined,
companionId: undefined,
locale: undefined,
name: undefined,
shortDesc: undefined,
introText: undefined,
createdAt: undefined,
updatedAt: undefined
})
const formRules = reactive({
locale: [{ required: true, message: '语言标识不能为空', trigger: 'blur' }],
name: [{ required: true, message: '角色名称不能为空', trigger: 'blur' }],
})
const formRef = ref() // 表单 Ref
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
formData.value.id = id as any
// 修改时,设置数据
if (id) {
formLoading.value = true
try {
formData.value = await AiCompanionApi.getAiCompanionI18n(id)
} finally {
formLoading.value = false
}
}
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
/** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
// 校验表单
await formRef.value.validate()
// 提交请求
formLoading.value = true
try {
const data = formData.value as unknown as AiCompanionI18n
if (formType.value === 'create') {
await AiCompanionApi.createAiCompanionI18n(data)
message.success(t('common.createSuccess'))
} else {
await AiCompanionApi.updateAiCompanionI18n(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
// 发送操作成功的事件
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
companionId: undefined,
locale: undefined,
name: undefined,
shortDesc: undefined,
introText: undefined,
createdAt: undefined,
updatedAt: undefined
}
formRef.value?.resetFields()
}
</script>
<style scoped>
/* 表单布局优化 */
.el-form-item {
margin-bottom: 16px;
}
/* 按钮间距 */
.el-button {
margin-right: 8px;
}
/* 输入框宽度 */
.el-input {
width: 100%;
}
</style>

View File

@@ -0,0 +1,150 @@
<template>
<!-- 列表 -->
<ContentWrap>
<el-button type="primary" plain @click="openForm('create')" v-hasPermi="['keyboard:ai-companion:create']">
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-button type="danger" plain :disabled="isEmpty(checkedIds)" @click="handleDeleteBatch"
v-hasPermi="['keyboard:ai-companion:delete']">
<Icon icon="ep:delete" class="mr-5px" /> 批量删除
</el-button>
<el-table row-key="id" v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true"
@selection-change="handleRowCheckboxChange">
<el-table-column type="selection" width="55" />
<el-table-column label="主表ID" align="center" prop="companionId" width="120px" />
<el-table-column label="语言标识" align="center" prop="locale" width="120px" />
<el-table-column label="角色名称" align="center" prop="name" width="180px" />
<el-table-column label="一句话描述" align="center" prop="shortDesc" width="200px" />
<el-table-column label="详细介绍" align="center" prop="introText" min-width="300px" />
<el-table-column label="创建时间" align="center" prop="createdAt" :formatter="dateFormatter" width="180px" />
<el-table-column label="更新时间" align="center" prop="updatedAt" :formatter="dateFormatter" width="180px" />
<el-table-column label="操作" align="center">
<template #default="scope">
<el-button link type="primary" @click="openForm('update', scope.row.id)"
v-hasPermi="['keyboard:ai-companion:update']">
编辑
</el-button>
<el-button link type="danger" @click="handleDelete(scope.row.id)"
v-hasPermi="['keyboard:ai-companion:delete']">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize"
@pagination="getList" />
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<AiCompanionI18nForm ref="formRef" @success="getList" />
</template>
<script setup lang="ts">
import { dateFormatter } from '@/utils/formatTime'
import { isEmpty } from '@/utils/is'
import { AiCompanionApi, AiCompanionI18n } from '@/api/keyboard/aicompanion'
import AiCompanionI18nForm from './AiCompanionI18nForm.vue'
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const props = defineProps<{
id?: number // 主键ID主表的关联字段
}>()
const loading = ref(false) // 列表的加载中
const list = ref([]) // 列表的数据
const total = ref(0) // 列表的总页数
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
id: undefined as unknown
})
/** 监听主表的关联字段的变化,加载对应的子表数据 */
watch(
() => props.id,
(val: number) => {
if (!val) {
list.value = [] // 清空列表
return
}
queryParams.id = val
handleQuery()
},
{ immediate: true, deep: true }
)
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await AiCompanionApi.getAiCompanionI18nPage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
if (!props.id) {
message.error('请选择一个AI陪聊角色表用于定义恋爱/陪伴型虚拟角色的基础信息与人设')
return
}
formRef.value.open(type, id, props.id)
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await AiCompanionApi.deleteAiCompanionI18n(id)
message.success(t('common.delSuccess'))
// 刷新列表
await getList()
} catch { }
}
/** 批量删除AI陪聊角色国际化表用于存储不同语言下的角色名称、一句话描述和详细介绍 */
const handleDeleteBatch = async () => {
try {
// 删除的二次确认
await message.delConfirm()
await AiCompanionApi.deleteAiCompanionI18nList(checkedIds.value);
checkedIds.value = [];
message.success(t('common.delSuccess'))
await getList();
} catch { }
}
const checkedIds = ref<number[]>([])
const handleRowCheckboxChange = (records: AiCompanionI18n[]) => {
checkedIds.value = records.map((item) => item.id!);
}
</script>
<style scoped>
/* 按钮间距 */
.el-button {
margin-right: 8px;
margin-bottom: 8px;
}
/* 表格样式优化 */
.el-table {
margin-top: 16px;
}
/* 操作按钮间距 */
.el-table-column__content .el-button {
margin-right: 4px;
}
</style>

View File

@@ -1,80 +1,92 @@
<template>
<!-- 搜索 -->
<ContentWrap>
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="角色名称" prop="name">
<el-input
v-model="queryParams.name"
placeholder="请输入角色名称"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
<!-- 搜索工作栏 -->
<el-form class="-mb-15px" :model="queryParams" ref="queryFormRef" :inline="true">
<el-form-item label="角色头像" prop="avatarUrl">
<el-input v-model="queryParams.avatarUrl" placeholder="请输入角色头像URL" clearable @keyup.enter="handleQuery"
class="!w-240px" />
</el-form-item>
<el-form-item label="角色封面图" prop="coverImageUrl">
<el-input v-model="queryParams.coverImageUrl" placeholder="请输入角色封面图URL" clearable @keyup.enter="handleQuery"
class="!w-240px" />
</el-form-item>
<el-form-item label="性别" prop="gender">
<el-select v-model="queryParams.gender" placeholder="全部" clearable class="!w-240px">
<el-option label="男" value="male" />
<el-option label="女" value="female" />
<el-option label="其他" value="other" />
</el-select>
<el-input v-model="queryParams.gender" placeholder="male / female / other" clearable @keyup.enter="handleQuery"
class="!w-240px" />
</el-form-item>
<el-form-item label="年龄段" prop="ageRange">
<el-input v-model="queryParams.ageRange" placeholder="如20s、25-30" clearable @keyup.enter="handleQuery"
class="!w-240px" />
</el-form-item>
<el-form-item label="性格标签" prop="personalityTags">
<el-input v-model="queryParams.personalityTags" placeholder="如:温柔、黏人、治愈" clearable @keyup.enter="handleQuery"
class="!w-240px" />
</el-form-item>
<el-form-item label="说话风格" prop="speakingStyle">
<el-input v-model="queryParams.speakingStyle" placeholder="如:撒娇型、理性型、活泼型" clearable @keyup.enter="handleQuery"
class="!w-240px" />
</el-form-item>
<el-form-item label="系统Prompt" prop="systemPrompt">
<el-input v-model="queryParams.systemPrompt" placeholder="定义角色核心人设" clearable @keyup.enter="handleQuery"
class="!w-240px" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="全部" clearable class="!w-240px">
<el-option label="上线" :value="1" />
<el-option label="下线" :value="0" />
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable class="!w-240px">
<el-option label="上线" value="1" />
<el-option label="下线" value="0" />
</el-select>
</el-form-item>
<el-form-item label="可见性" prop="visibility">
<el-select v-model="queryParams.visibility" placeholder="全部" clearable class="!w-240px">
<el-option label="公开" :value="1" />
<el-option label="内测" :value="2" />
<el-option label="隐藏" :value="3" />
<el-select v-model="queryParams.visibility" placeholder="请选择可见性" clearable class="!w-240px">
<el-option label="公开" value="1" />
<el-option label="内测" value="2" />
<el-option label="隐藏" value="3" />
</el-select>
</el-form-item>
<el-form-item label="创建时间" prop="createTime">
<el-date-picker
v-model="queryParams.createTime"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-240px"
/>
<el-form-item label="排序权重" prop="sortOrder">
<el-input v-model="queryParams.sortOrder" placeholder="数值越大排序越靠前" clearable @keyup.enter="handleQuery"
class="!w-240px" />
</el-form-item>
<el-form-item label="热度评分" prop="popularityScore">
<el-input v-model="queryParams.popularityScore" placeholder="用于推荐排序" clearable @keyup.enter="handleQuery"
class="!w-240px" />
</el-form-item>
<el-form-item label="创建时间" prop="createdAt">
<el-date-picker v-model="queryParams.createdAt" value-format="YYYY-MM-DD" type="date" placeholder="选择创建时间"
clearable class="!w-240px" />
</el-form-item>
<el-form-item label="更新时间" prop="updatedAt">
<el-date-picker v-model="queryParams.updatedAt" value-format="YYYY-MM-DD" type="date" placeholder="选择更新时间"
clearable class="!w-240px" />
</el-form-item>
<el-form-item label="开场白" prop="prologue">
<el-input v-model="queryParams.prologue" placeholder="请输入开场白" clearable @keyup.enter="handleQuery"
class="!w-240px" />
</el-form-item>
<el-form-item label="开场白音频" prop="prologueAudio">
<el-input v-model="queryParams.prologueAudio" placeholder="请输入开场白音频" clearable @keyup.enter="handleQuery"
class="!w-240px" />
</el-form-item>
<el-form-item label="音频Id" prop="voiceId">
<el-input v-model="queryParams.voiceId" placeholder="请输入音频Id" clearable @keyup.enter="handleQuery"
class="!w-240px" />
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['keyboard:ai-companion:create']"
>
<el-button @click="handleQuery">
<Icon icon="ep:search" class="mr-5px" /> 搜索
</el-button>
<el-button @click="resetQuery">
<Icon icon="ep:refresh" class="mr-5px" /> 重置
</el-button>
<el-button type="primary" plain @click="openForm('create')" v-hasPermi="['keyboard:ai-companion:create']">
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-button
type="success"
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['keyboard:ai-companion:export']"
>
<el-button type="success" plain @click="handleExport" :loading="exportLoading"
v-hasPermi="['keyboard:ai-companion:export']">
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
<el-button
type="danger"
plain
:disabled="isEmpty(checkedIds)"
@click="handleDeleteBatch"
v-hasPermi="['keyboard:ai-companion:delete']"
>
<el-button type="danger" plain :disabled="isEmpty(checkedIds)" @click="handleDeleteBatch"
v-hasPermi="['keyboard:ai-companion:delete']">
<Icon icon="ep:delete" class="mr-5px" /> 批量删除
</el-button>
</el-form-item>
@@ -83,93 +95,64 @@
<!-- 列表 -->
<ContentWrap>
<el-table
row-key="id"
v-loading="loading"
:data="list"
:stripe="true"
:show-overflow-tooltip="true"
@selection-change="handleRowCheckboxChange"
>
<el-table row-key="id" v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true"
highlight-current-row @current-change="handleCurrentChange" @selection-change="handleRowCheckboxChange">
<el-table-column type="selection" width="55" />
<el-table-column label="ID" align="center" prop="id" width="70" />
<el-table-column label="头像" align="center" prop="avatarUrl" width="80">
<el-table-column label="陪聊角色唯一ID" align="center" prop="id" width="120px" />
<el-table-column label="角色头像" align="center" prop="avatarUrl" width="100px">
<template #default="scope">
<el-image
v-if="scope.row.avatarUrl"
:src="scope.row.avatarUrl"
:preview-src-list="[scope.row.avatarUrl]"
preview-teleported
fit="cover"
class="w-40px h-40px rounded-full"
/>
<el-image v-if="scope.row.avatarUrl" :src="scope.row.avatarUrl" fit="cover" class="avatar-image" />
<span v-else></span>
</template>
</el-table-column>
<el-table-column label="角色名称" align="center" prop="name" min-width="120" />
<el-table-column label="性别" align="center" prop="gender" width="70">
<el-table-column label="角色封面图" align="center" prop="coverImageUrl" width="150px">
<template #default="scope">
<el-tag v-if="scope.row.gender === 'male'" type="primary"></el-tag>
<el-tag v-else-if="scope.row.gender === 'female'" type="danger"></el-tag>
<el-tag v-else type="info">其他</el-tag>
<el-image v-if="scope.row.coverImageUrl" :src="scope.row.coverImageUrl" fit="cover" class="cover-image" />
<span v-else></span>
</template>
</el-table-column>
<el-table-column label="年龄段" align="center" prop="ageRange" width="80" />
<el-table-column label="简介" align="center" prop="shortDesc" min-width="160" />
<el-table-column label="说话风格" align="center" prop="speakingStyle" width="100" />
<el-table-column label="状态" align="center" prop="status" width="80">
<el-table-column label="角色性别male / female / other" align="center" prop="gender" />
<el-table-column label="角色年龄段描述20s、25-30" align="center" prop="ageRange" />
<el-table-column label="角色性格标签数组(如:温柔、黏人、治愈)" align="center" prop="personalityTags" />
<el-table-column label="角色说话风格(如:撒娇型、理性型、活泼型)" align="center" prop="speakingStyle" />
<el-table-column label="AI系统Prompt定义角色核心人设仅供模型使用" align="center" prop="systemPrompt" />
<el-table-column label="角色状态1=上线0=下线" align="center" prop="status" />
<el-table-column label="角色可见性1=公开2=内测3=隐藏" align="center" prop="visibility" />
<el-table-column label="排序权重,数值越大排序越靠前" align="center" prop="sortOrder" />
<el-table-column label="角色热度评分,用于推荐排序" align="center" prop="popularityScore" />
<!-- <el-table-column label="创建时间" align="center" prop="createdAt" :formatter="dateFormatter" width="180px" />
<el-table-column label="更新时间" align="center" prop="updatedAt" :formatter="dateFormatter" width="180px" /> -->
<el-table-column label="开场白" align="center" prop="prologue" />
<el-table-column label="开场白音频" align="center" prop="prologueAudio" />
<el-table-column label="音频Id" align="center" prop="voiceId" />
<el-table-column label="操作" align="center" min-width="120px">
<template #default="scope">
<el-tag v-if="scope.row.status === 1" type="success">上线</el-tag>
<el-tag v-else type="info">下线</el-tag>
</template>
</el-table-column>
<el-table-column label="可见性" align="center" prop="visibility" width="80">
<template #default="scope">
<el-tag v-if="scope.row.visibility === 1">公开</el-tag>
<el-tag v-else-if="scope.row.visibility === 2" type="warning">内测</el-tag>
<el-tag v-else-if="scope.row.visibility === 3" type="info">隐藏</el-tag>
</template>
</el-table-column>
<el-table-column label="排序" align="center" prop="sortOrder" width="70" />
<el-table-column label="热度" align="center" prop="popularityScore" width="70" />
<el-table-column
label="创建时间"
align="center"
prop="createdAt"
:formatter="dateFormatter"
width="180"
/>
<el-table-column label="操作" align="center" fixed="right" width="130">
<template #default="scope">
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['keyboard:ai-companion:update']"
>
<el-button link type="primary" @click="openForm('update', scope.row.id)"
v-hasPermi="['keyboard:ai-companion:update']">
编辑
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['keyboard:ai-companion:delete']"
>
<el-button link type="danger" @click="handleDelete(scope.row.id)"
v-hasPermi="['keyboard:ai-companion:delete']">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
<Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize"
@pagination="getList" />
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<AiCompanionForm ref="formRef" @success="getList" />
<!-- 子表的列表 -->
<ContentWrap>
<el-tabs model-value="aiCompanionI18n">
<el-tab-pane label="AI陪聊角色国际化表用于存储不同语言下的角色名称、一句话描述和详细介绍" name="aiCompanionI18n">
<AiCompanionI18nList :id="currentRow.id" />
</el-tab-pane>
</el-tabs>
</ContentWrap>
</template>
<script setup lang="ts">
@@ -178,26 +161,41 @@ import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download'
import { AiCompanionApi, AiCompanion } from '@/api/keyboard/aicompanion'
import AiCompanionForm from './AiCompanionForm.vue'
import AiCompanionI18nList from './components/AiCompanionI18nList.vue'
/** AI陪聊角色表用于定义恋爱/陪伴型虚拟角色的基础信息与人设 列表 */
defineOptions({ name: 'KeyboardAiCompanion' })
const message = useMessage()
const { t } = useI18n()
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
const loading = ref(true)
const list = ref<AiCompanion[]>([])
const total = ref(0)
const loading = ref(true) // 列表的加载中
const list = ref<AiCompanion[]>([]) // 列表的数据
const total = ref(0) // 列表的总页数
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
name: undefined,
avatarUrl: undefined,
coverImageUrl: undefined,
gender: undefined,
ageRange: undefined,
personalityTags: undefined,
speakingStyle: undefined,
systemPrompt: undefined,
status: undefined,
visibility: undefined,
createTime: []
sortOrder: undefined,
popularityScore: undefined,
createdAt: undefined,
createdAt: [],
updatedAt: undefined,
updatedAt: [],
prologue: undefined,
prologueAudio: undefined,
voiceId: undefined
})
const queryFormRef = ref()
const exportLoading = ref(false)
const queryFormRef = ref() // 搜索的表单
const exportLoading = ref(false) // 导出的加载中
/** 查询列表 */
const getList = async () => {
@@ -232,43 +230,92 @@ const openForm = (type: string, id?: number) => {
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await AiCompanionApi.deleteAiCompanion(id)
message.success(t('common.delSuccess'))
currentRow.value = {}
// 刷新列表
await getList()
} catch {}
} catch { }
}
/** 批量删除 */
const checkedIds = ref<number[]>([])
const handleRowCheckboxChange = (records: AiCompanion[]) => {
checkedIds.value = records.map((item) => item.id!)
}
/** 批量删除AI陪聊角色表用于定义恋爱/陪伴型虚拟角色的基础信息与人设 */
const handleDeleteBatch = async () => {
try {
// 删除的二次确认
await message.delConfirm()
await AiCompanionApi.deleteAiCompanionList(checkedIds.value)
checkedIds.value = []
await AiCompanionApi.deleteAiCompanionList(checkedIds.value);
checkedIds.value = [];
message.success(t('common.delSuccess'))
await getList()
} catch {}
await getList();
} catch { }
}
const checkedIds = ref<number[]>([])
const handleRowCheckboxChange = (records: AiCompanion[]) => {
checkedIds.value = records.map((item) => item.id!);
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
// 导出的二次确认
await message.exportConfirm()
// 发起导出
exportLoading.value = true
const data = await AiCompanionApi.exportAiCompanion(queryParams)
download.excel(data, 'AI陪聊角色.xls')
download.excel(data, 'AI陪聊角色表,用于定义恋爱/陪伴型虚拟角色的基础信息与人设.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 选中行操作 */
const currentRow = ref({}) // 选中行
const handleCurrentChange = (row) => {
currentRow.value = row
}
/** 初始化 **/
onMounted(() => {
getList()
})
</script>
<style scoped>
.avatar-image {
width: 50px;
height: 50px;
border-radius: 50%;
cursor: pointer;
}
.cover-image {
width: 100px;
height: 60px;
border-radius: 4px;
cursor: pointer;
}
/* 表格行悬停效果 */
:deep(.el-table__row:hover) {
background-color: #f5f7fa !important;
}
/* 图片加载和错误处理 */
:deep(.el-image__error) {
display: flex;
align-items: center;
justify-content: center;
background-color: #f0f0f0;
color: #999;
font-size: 12px;
}
:deep(.el-image__placeholder) {
background-color: #f0f0f0;
}
</style>

View File

@@ -0,0 +1,241 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible" width="760px">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="110px"
v-loading="formLoading"
>
<el-divider content-position="left">基础信息</el-divider>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="应用标识" prop="appId">
<el-input v-model="formData.appId" placeholder="如main" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="平台" prop="platform">
<el-select v-model="formData.platform" placeholder="请选择平台">
<el-option label="Android" value="android" />
<el-option label="iOS" value="ios" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="渠道" prop="channel">
<el-input v-model="formData.channel" placeholder="如official" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="构建号" prop="buildNumber">
<el-input v-model="formData.buildNumber" placeholder="iOS 可选" />
</el-form-item>
</el-col>
</el-row>
<el-divider content-position="left">版本策略</el-divider>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="展示版本" prop="versionName">
<el-input v-model="formData.versionName" placeholder="如1.2.3" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="版本码" prop="versionCode">
<el-input-number v-model="formData.versionCode" :min="1" controls-position="right" class="!w-full" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="最低版本码" prop="minSupportedCode">
<el-input-number v-model="formData.minSupportedCode" :min="1" controls-position="right" class="!w-full" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="发布时间" prop="releasedAt">
<el-date-picker
v-model="formData.releasedAt"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="请选择发布时间"
class="!w-full"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="强制更新" prop="isForceUpdate">
<el-radio-group v-model="formData.isForceUpdate">
<el-radio :value="true"></el-radio>
<el-radio :value="false"></el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="是否生效" prop="isActive">
<el-radio-group v-model="formData.isActive">
<el-radio :value="true">生效</el-radio>
<el-radio :value="false">停用</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-divider content-position="left">更新内容</el-divider>
<el-form-item label="更新说明" prop="releaseNotes">
<el-input
v-model="formData.releaseNotes"
type="textarea"
:rows="3"
placeholder="展示给用户的更新内容"
/>
</el-form-item>
<el-form-item label="下载地址" prop="downloadUrl">
<el-input v-model="formData.downloadUrl" placeholder="APK 直链 / App Store 链接" />
</el-form-item>
<el-form-item label="商店链接" prop="storeUrl">
<el-input v-model="formData.storeUrl" placeholder="可选" />
</el-form-item>
<el-form-item label="扩展元数据" prop="metadataText">
<el-input
v-model="formData.metadataText"
type="textarea"
:rows="4"
placeholder='JSON 格式,例如:{"size":"20MB","md5":"xxx"}'
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { AppVersionsApi, AppVersions } from '@/api/keyboard/appversions'
defineOptions({ name: 'AppVersionsForm' })
const { t } = useI18n()
const message = useMessage()
const dialogVisible = ref(false)
const dialogTitle = ref('')
const formLoading = ref(false)
const formType = ref('')
const formData = ref({
id: undefined,
appId: undefined,
platform: undefined,
channel: undefined,
versionName: undefined,
versionCode: 1,
buildNumber: undefined,
minSupportedCode: 1,
isForceUpdate: false,
isActive: true,
releaseNotes: undefined,
downloadUrl: undefined,
storeUrl: undefined,
metadataText: undefined,
releasedAt: undefined
})
const formRules = reactive({
appId: [{ required: true, message: '应用标识不能为空', trigger: 'blur' }],
platform: [{ required: true, message: '请选择平台', trigger: 'change' }],
channel: [{ required: true, message: '渠道不能为空', trigger: 'blur' }],
versionName: [{ required: true, message: '展示版本不能为空', trigger: 'blur' }],
versionCode: [{ required: true, message: '版本码不能为空', trigger: 'change' }],
minSupportedCode: [{ required: true, message: '最低版本码不能为空', trigger: 'change' }],
isForceUpdate: [{ required: true, message: '请选择是否强制更新', trigger: 'change' }],
isActive: [{ required: true, message: '请选择是否生效', trigger: 'change' }],
releaseNotes: [{ required: true, message: '更新说明不能为空', trigger: 'blur' }],
downloadUrl: [{ required: true, message: '下载地址不能为空', trigger: 'blur' }]
})
const formRef = ref()
const open = async (type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
if (id) {
formLoading.value = true
try {
const data = await AppVersionsApi.getAppVersions(id)
formData.value = {
...data,
versionCode: data.versionCode || 1,
minSupportedCode: data.minSupportedCode || 1,
isForceUpdate: !!data.isForceUpdate,
isActive: !!data.isActive,
metadataText: data.metadata ? JSON.stringify(data.metadata, null, 2) : undefined
}
} finally {
formLoading.value = false
}
}
}
defineExpose({ open })
const emit = defineEmits(['success'])
const submitForm = async () => {
await formRef.value.validate()
let metadata: Record<string, any> | undefined
if (formData.value.metadataText) {
try {
metadata = JSON.parse(formData.value.metadataText)
} catch {
message.warning('扩展元数据不是有效 JSON请检查格式')
return
}
}
formLoading.value = true
try {
const { metadataText, ...rest } = formData.value
const data = { ...rest, metadata } as unknown as AppVersions
if (formType.value === 'create') {
await AppVersionsApi.createAppVersions(data)
message.success(t('common.createSuccess'))
} else {
await AppVersionsApi.updateAppVersions(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
emit('success')
} finally {
formLoading.value = false
}
}
const resetForm = () => {
formData.value = {
id: undefined,
appId: undefined,
platform: undefined,
channel: undefined,
versionName: undefined,
versionCode: 1,
buildNumber: undefined,
minSupportedCode: 1,
isForceUpdate: false,
isActive: true,
releaseNotes: undefined,
downloadUrl: undefined,
storeUrl: undefined,
metadataText: undefined,
releasedAt: undefined
}
formRef.value?.resetFields()
}
</script>

View File

@@ -0,0 +1,285 @@
<template>
<!-- 搜索 -->
<ContentWrap>
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="应用" prop="appId">
<el-input
v-model="queryParams.appId"
placeholder="请输入应用标识"
clearable
@keyup.enter="handleQuery"
class="!w-220px"
/>
</el-form-item>
<el-form-item label="平台" prop="platform">
<el-select v-model="queryParams.platform" placeholder="全部" clearable class="!w-160px">
<el-option label="Android" value="android" />
<el-option label="iOS" value="ios" />
</el-select>
</el-form-item>
<el-form-item label="渠道" prop="channel">
<el-input
v-model="queryParams.channel"
placeholder="请输入渠道"
clearable
@keyup.enter="handleQuery"
class="!w-200px"
/>
</el-form-item>
<el-form-item label="版本号" prop="versionName">
<el-input
v-model="queryParams.versionName"
placeholder="如 1.2.3"
clearable
@keyup.enter="handleQuery"
class="!w-160px"
/>
</el-form-item>
<el-form-item label="强更" prop="isForceUpdate">
<el-select v-model="queryParams.isForceUpdate" placeholder="全部" clearable class="!w-120px">
<el-option label="是" :value="true" />
<el-option label="否" :value="false" />
</el-select>
</el-form-item>
<el-form-item label="状态" prop="isActive">
<el-select v-model="queryParams.isActive" placeholder="全部" clearable class="!w-120px">
<el-option label="生效" :value="true" />
<el-option label="停用" :value="false" />
</el-select>
</el-form-item>
<el-form-item label="发布时间" prop="releasedAt">
<el-date-picker
v-model="queryParams.releasedAt"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-260px"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['keyboard:app-versions:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-button
type="success"
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['keyboard:app-versions:export']"
>
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
<el-button
type="danger"
plain
:disabled="isEmpty(checkedIds)"
@click="handleDeleteBatch"
v-hasPermi="['keyboard:app-versions:delete']"
>
<Icon icon="ep:delete" class="mr-5px" /> 批量删除
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table
row-key="id"
v-loading="loading"
:data="list"
:stripe="true"
:show-overflow-tooltip="true"
@selection-change="handleRowCheckboxChange"
>
<el-table-column type="selection" width="55" />
<el-table-column label="ID" align="center" prop="id" width="70" />
<el-table-column label="应用" align="center" prop="appId" min-width="120" />
<el-table-column label="平台" align="center" prop="platform" width="90">
<template #default="scope">
<el-tag v-if="scope.row.platform === 'android'" type="success">Android</el-tag>
<el-tag v-else-if="scope.row.platform === 'ios'" type="primary">iOS</el-tag>
<el-tag v-else type="info">未知</el-tag>
</template>
</el-table-column>
<el-table-column label="渠道" align="center" prop="channel" min-width="120" />
<el-table-column label="版本号" align="center" prop="versionName" width="120" />
<el-table-column label="版本码" align="center" prop="versionCode" width="100" />
<el-table-column label="最低版本" align="center" prop="minSupportedCode" width="100" />
<el-table-column label="强更" align="center" prop="isForceUpdate" width="80">
<template #default="scope">
<el-tag :type="scope.row.isForceUpdate ? 'danger' : 'info'">
{{ scope.row.isForceUpdate ? '是' : '否' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="状态" align="center" prop="isActive" width="80">
<template #default="scope">
<el-tag :type="scope.row.isActive ? 'success' : 'info'">
{{ scope.row.isActive ? '生效' : '停用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="下载地址" align="center" prop="downloadUrl" min-width="180">
<template #default="scope">
<el-link v-if="scope.row.downloadUrl" :href="scope.row.downloadUrl" target="_blank" type="primary">
打开链接
</el-link>
</template>
</el-table-column>
<el-table-column
label="发布时间"
align="center"
prop="releasedAt"
:formatter="dateFormatter"
width="180"
/>
<el-table-column
label="创建时间"
align="center"
prop="createdAt"
:formatter="dateFormatter"
width="180"
/>
<el-table-column label="操作" align="center" fixed="right" width="130">
<template #default="scope">
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['keyboard:app-versions:update']"
>
编辑
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['keyboard:app-versions:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<AppVersionsForm ref="formRef" @success="getList" />
</template>
<script setup lang="ts">
import { isEmpty } from '@/utils/is'
import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download'
import { AppVersionsApi, AppVersions } from '@/api/keyboard/appversions'
import AppVersionsForm from './AppVersionsForm.vue'
defineOptions({ name: 'AppVersions' })
const message = useMessage()
const { t } = useI18n()
const loading = ref(true)
const list = ref<AppVersions[]>([])
const total = ref(0)
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
appId: undefined,
platform: undefined,
channel: undefined,
versionName: undefined,
isForceUpdate: undefined,
isActive: undefined,
releasedAt: []
})
const queryFormRef = ref()
const exportLoading = ref(false)
const getList = async () => {
loading.value = true
try {
const data = await AppVersionsApi.getAppVersionsPage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
const handleDelete = async (id: number) => {
try {
await message.delConfirm()
await AppVersionsApi.deleteAppVersions(id)
message.success(t('common.delSuccess'))
await getList()
} catch {}
}
const checkedIds = ref<number[]>([])
const handleRowCheckboxChange = (records: AppVersions[]) => {
checkedIds.value = records.map((item) => item.id!)
}
const handleDeleteBatch = async () => {
try {
await message.delConfirm()
await AppVersionsApi.deleteAppVersionsList(checkedIds.value)
checkedIds.value = []
message.success(t('common.delSuccess'))
await getList()
} catch {}
}
const handleExport = async () => {
try {
await message.exportConfirm()
exportLoading.value = true
const data = await AppVersionsApi.exportAppVersions(queryParams)
download.excel(data, 'App版本管理.xls')
} catch {
} finally {
exportLoading.value = false
}
}
onMounted(() => {
getList()
})
</script>

View File

@@ -1,12 +1,12 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible">
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px" v-loading="formLoading">
<el-form-item label="标题" prop="characterName">
<!-- <el-form-item label="标题" prop="characterName">
<el-input v-model="formData.characterName" placeholder="请输入标题" />
</el-form-item>
<el-form-item label="背景描述" prop="characterBackground">
<el-input v-model="formData.characterBackground" placeholder="请输入背景描述" />
</el-form-item>
</el-form-item> -->
<el-form-item label="角色头像" prop="avatarUrl">
<UploadImg v-model="formData.avatarUrl" height="120px" width="120px" />
</el-form-item>

View File

@@ -0,0 +1,117 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible">
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px" v-loading="formLoading">
<el-form-item label="语言标识" prop="locale">
<el-input v-model="formData.locale" placeholder="请输入语言标识,如 zh-CN/en-US/ja-JP" />
</el-form-item>
<el-form-item label="标题" prop="characterName">
<el-input v-model="formData.characterName" placeholder="请输入标题" />
</el-form-item>
<el-form-item label="背景描述" prop="characterBackground">
<el-input v-model="formData.characterBackground" placeholder="请输入背景描述" />
</el-form-item>
<!-- <el-form-item label="创建时间" prop="createdAt">
<el-date-picker
v-model="formData.createdAt"
type="date"
value-format="x"
placeholder="选择创建时间"
/>
</el-form-item>
<el-form-item label="更新时间" prop="updatedAt">
<el-date-picker
v-model="formData.updatedAt"
type="date"
value-format="x"
placeholder="选择更新时间"
/>
</el-form-item> -->
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { CharacterApi, CharacterI18n } from '@/api/keyboard/character'
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗`的是否展示
const dialogTitle = ref('') // 弹窗的标题
const formLoading = ref(false) // 表单的加载中1修改时的数据加载2提交的按钮禁用
const formType = ref('') // 表单的类型create - 新增update - 修改
const formData = ref({
id: undefined,
characterId: undefined,
locale: undefined,
characterName: undefined,
characterBackground: undefined,
createdAt: undefined,
updatedAt: undefined
})
const formRules = reactive({
characterId: [{ required: true, message: '角色主表id不能为空', trigger: 'blur' }],
locale: [{ required: true, message: '语言标识,如 zh-CN/en-US/ja-JP不能为空', trigger: 'blur' }]
})
const formRef = ref() // 表单 Ref
/** 打开弹窗 */
const open = async (type: string, id?: number, characterId?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
formData.value.characterId = characterId as any
// 修改时,设置数据
if (id) {
formLoading.value = true
try {
formData.value = await CharacterApi.getCharacterI18n(id)
} finally {
formLoading.value = false
}
}
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
/** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
// 校验表单
await formRef.value.validate()
// 提交请求
formLoading.value = true
try {
const data = formData.value as unknown as CharacterI18n
if (formType.value === 'create') {
await CharacterApi.createCharacterI18n(data)
message.success(t('common.createSuccess'))
} else {
await CharacterApi.updateCharacterI18n(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
// 发送操作成功的事件
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
characterId: undefined,
locale: undefined,
characterName: undefined,
characterBackground: undefined,
createdAt: undefined,
updatedAt: undefined
}
formRef.value?.resetFields()
}
</script>

View File

@@ -0,0 +1,171 @@
<template>
<!-- 列表 -->
<ContentWrap>
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['keyboard:character:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-button
type="danger"
plain
:disabled="isEmpty(checkedIds)"
@click="handleDeleteBatch"
v-hasPermi="['keyboard:character:delete']"
>
<Icon icon="ep:delete" class="mr-5px" /> 批量删除
</el-button>
<el-table
row-key="id"
v-loading="loading"
:data="list"
:stripe="true"
:show-overflow-tooltip="true"
@selection-change="handleRowCheckboxChange"
>
<el-table-column type="selection" width="55" />
<el-table-column label="主键 Id" align="center" prop="id" />
<el-table-column label="语言标识,如 zh-CN/en-US/ja-JP" align="center" prop="locale" />
<el-table-column label="标题" align="center" prop="characterName" />
<el-table-column label="背景描述" align="center" prop="characterBackground" />
<el-table-column
label="创建时间"
align="center"
prop="createdAt"
:formatter="dateFormatter"
width="180px"
/>
<el-table-column
label="更新时间"
align="center"
prop="updatedAt"
:formatter="dateFormatter"
width="180px"
/>
<el-table-column label="操作" align="center">
<template #default="scope">
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['keyboard:character:update']"
>
编辑
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['keyboard:character:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<CharacterI18nForm ref="formRef" @success="getList" />
</template>
<script setup lang="ts">
import { dateFormatter } from '@/utils/formatTime'
import { isEmpty } from '@/utils/is'
import { CharacterApi, CharacterI18n } from '@/api/keyboard/character'
import CharacterI18nForm from './CharacterI18nForm.vue'
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const props = defineProps<{
characterId?: number // 角色主表id主表的关联字段
}>()
const loading = ref(false) // 列表的加载中
const list = ref([]) // 列表的数据
const total = ref(0) // 列表的总页数
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
characterId: undefined as unknown
})
/** 监听主表的关联字段的变化,加载对应的子表数据 */
watch(
() => props.characterId,
(val: number) => {
if (!val) {
list.value = [] // 清空列表
return
}
queryParams.characterId = val
handleQuery()
},
{ immediate: true, deep: true }
)
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await CharacterApi.getCharacterI18nPage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
if (!props.characterId) {
message.error('请选择一个键盘人设')
return
}
formRef.value.open(type, id, props.characterId)
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await CharacterApi.deleteCharacterI18n(id)
message.success(t('common.delSuccess'))
// 刷新列表
await getList()
} catch {}
}
/** 批量删除键盘人设国际化内容 */
const handleDeleteBatch = async () => {
try {
// 删除的二次确认
await message.delConfirm()
await CharacterApi.deleteCharacterI18nList(checkedIds.value);
checkedIds.value = [];
message.success(t('common.delSuccess'))
await getList();
} catch {}
}
const checkedIds = ref<number[]>([])
const handleRowCheckboxChange = (records: CharacterI18n[]) => {
checkedIds.value = records.map((item) => item.id!);
}
</script>

View File

@@ -67,15 +67,15 @@
<!-- 列表 -->
<ContentWrap>
<el-table row-key="id" v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true"
@selection-change="handleRowCheckboxChange">
highlight-current-row @current-change="handleCurrentChange" @selection-change="handleRowCheckboxChange">
<el-table-column type="selection" width="55" />
<el-table-column label="主键 Id" align="center" prop="id" />
<el-table-column label="标题" align="center" prop="characterName" />
<el-table-column label="背景描述" align="center" prop="characterBackground" />
<el-table-column label="角色头像" align="center" prop="avatarUrl" width="90px">
<template #default="{ row }">
<el-image v-if="row.avatarUrl" :src="row.avatarUrl" class="h-60px " @click="imagePreview(row.avatarUrl)" />
<span v-else>-</span>
<!-- <el-table-column label="标题" align="center" prop="characterName" />
<el-table-column label="背景描述" align="center" prop="characterBackground" /> -->
<el-table-column label="角色头像" align="center" prop="avatarUrl">
<template #default="scope">
<el-image v-if="scope.row.avatarUrl" :src="scope.row.avatarUrl" fit="cover" class="character-avatar" />
<span v-else></span>
</template>
</el-table-column>
<el-table-column label="下载次数" align="center" prop="download" />
@@ -104,6 +104,14 @@
<!-- 表单弹窗添加/修改 -->
<CharacterForm ref="formRef" @success="getList" />
<!-- 子表的列表 -->
<ContentWrap>
<el-tabs model-value="characterI18n">
<el-tab-pane label="键盘人设国际化内容" name="characterI18n">
<CharacterI18nList :character-id="currentRow.id" />
</el-tab-pane>
</el-tabs>
</ContentWrap>
</template>
<script setup lang="ts">
@@ -112,7 +120,7 @@ import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download'
import { CharacterApi, Character } from '@/api/keyboard/character'
import CharacterForm from './CharacterForm.vue'
import { createImageViewer } from '@/components/ImageViewer'
import CharacterI18nList from './components/CharacterI18nList.vue'
/** 键盘人设 列表 */
defineOptions({ name: 'KeyboardCharacter' })
@@ -180,6 +188,7 @@ const handleDelete = async (id: number) => {
// 发起删除
await CharacterApi.deleteCharacter(id)
message.success(t('common.delSuccess'))
currentRow.value = {}
// 刷新列表
await getList()
} catch { }
@@ -202,12 +211,6 @@ const handleRowCheckboxChange = (records: Character[]) => {
checkedIds.value = records.map((item) => item.id!);
}
const imagePreview = (imgUrl: string) => {
createImageViewer({
urlList: [imgUrl]
})
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
@@ -223,8 +226,23 @@ const handleExport = async () => {
}
}
/** 选中行操作 */
const currentRow = ref({}) // 选中行
const handleCurrentChange = (row) => {
currentRow.value = row
}
/** 初始化 **/
onMounted(() => {
getList()
})
</script>
<style scoped>
.character-avatar {
width: 80px;
height: 80px;
border-radius: 4px;
cursor: pointer;
}
</style>

View File

@@ -0,0 +1,138 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="100px"
v-loading="formLoading"
>
<el-form-item label="角色主表id" prop="characterId">
<el-input v-model="formData.characterId" placeholder="请输入角色主表id" />
</el-form-item>
<el-form-item label="语言标识,如 zh-CN/en-US/ja-JP" prop="locale">
<el-input v-model="formData.locale" placeholder="请输入语言标识,如 zh-CN/en-US/ja-JP" />
</el-form-item>
<el-form-item label="标题" prop="characterName">
<el-input v-model="formData.characterName" placeholder="请输入标题" />
</el-form-item>
<el-form-item label="背景描述" prop="characterBackground">
<el-input v-model="formData.characterBackground" placeholder="请输入背景描述" />
</el-form-item>
<el-form-item label="人设提示词" prop="prompt">
<el-input v-model="formData.prompt" placeholder="请输入人设提示词" />
</el-form-item>
<el-form-item label="emoji 标签" prop="emoji">
<el-input v-model="formData.emoji" placeholder="请输入emoji 标签" />
</el-form-item>
<el-form-item label="创建时间" prop="createdAt">
<el-date-picker
v-model="formData.createdAt"
type="date"
value-format="x"
placeholder="选择创建时间"
/>
</el-form-item>
<el-form-item label="更新时间" prop="updatedAt">
<el-date-picker
v-model="formData.updatedAt"
type="date"
value-format="x"
placeholder="选择更新时间"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { CharacterI18nApi, CharacterI18n } from '@/api/keyboard/characteri18n'
/** 键盘人设国际化内容 表单 */
defineOptions({ name: 'CharacterI18nForm' })
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗的是否展示
const dialogTitle = ref('') // 弹窗的标题
const formLoading = ref(false) // 表单的加载中1修改时的数据加载2提交的按钮禁用
const formType = ref('') // 表单的类型create - 新增update - 修改
const formData = ref({
id: undefined,
characterId: undefined,
locale: undefined,
characterName: undefined,
characterBackground: undefined,
prompt: undefined,
emoji: undefined,
createdAt: undefined,
updatedAt: undefined
})
const formRules = reactive({
characterId: [{ required: true, message: '角色主表id不能为空', trigger: 'blur' }],
locale: [{ required: true, message: '语言标识,如 zh-CN/en-US/ja-JP不能为空', trigger: 'blur' }]
})
const formRef = ref() // 表单 Ref
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
// 修改时,设置数据
if (id) {
formLoading.value = true
try {
formData.value = await CharacterI18nApi.getCharacterI18n(id)
} finally {
formLoading.value = false
}
}
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
/** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
// 校验表单
await formRef.value.validate()
// 提交请求
formLoading.value = true
try {
const data = formData.value as unknown as CharacterI18n
if (formType.value === 'create') {
await CharacterI18nApi.createCharacterI18n(data)
message.success(t('common.createSuccess'))
} else {
await CharacterI18nApi.updateCharacterI18n(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
// 发送操作成功的事件
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
characterId: undefined,
locale: undefined,
characterName: undefined,
characterBackground: undefined,
prompt: undefined,
emoji: undefined,
createdAt: undefined,
updatedAt: undefined
}
formRef.value?.resetFields()
}
</script>

View File

@@ -0,0 +1,296 @@
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="角色主表id" prop="characterId">
<el-input
v-model="queryParams.characterId"
placeholder="请输入角色主表id"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="语言标识,如 zh-CN/en-US/ja-JP" prop="locale">
<el-input
v-model="queryParams.locale"
placeholder="请输入语言标识,如 zh-CN/en-US/ja-JP"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="标题" prop="characterName">
<el-input
v-model="queryParams.characterName"
placeholder="请输入标题"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="背景描述" prop="characterBackground">
<el-input
v-model="queryParams.characterBackground"
placeholder="请输入背景描述"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="人设提示词" prop="prompt">
<el-input
v-model="queryParams.prompt"
placeholder="请输入人设提示词"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="emoji 标签" prop="emoji">
<el-input
v-model="queryParams.emoji"
placeholder="请输入emoji 标签"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="创建时间" prop="createdAt">
<el-date-picker
v-model="queryParams.createdAt"
value-format="YYYY-MM-DD"
type="date"
placeholder="选择创建时间"
clearable
class="!w-240px"
/>
</el-form-item>
<el-form-item label="更新时间" prop="updatedAt">
<el-date-picker
v-model="queryParams.updatedAt"
value-format="YYYY-MM-DD"
type="date"
placeholder="选择更新时间"
clearable
class="!w-240px"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['keyboard:character-i18n:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-button
type="success"
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['keyboard:character-i18n:export']"
>
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
<el-button
type="danger"
plain
:disabled="isEmpty(checkedIds)"
@click="handleDeleteBatch"
v-hasPermi="['keyboard:character-i18n:delete']"
>
<Icon icon="ep:delete" class="mr-5px" /> 批量删除
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table
row-key="id"
v-loading="loading"
:data="list"
:stripe="true"
:show-overflow-tooltip="true"
@selection-change="handleRowCheckboxChange"
>
<el-table-column type="selection" width="55" />
<el-table-column label="主键 Id" align="center" prop="id" />
<el-table-column label="角色主表id" align="center" prop="characterId" />
<el-table-column label="语言标识,如 zh-CN/en-US/ja-JP" align="center" prop="locale" />
<el-table-column label="标题" align="center" prop="characterName" />
<el-table-column label="背景描述" align="center" prop="characterBackground" />
<el-table-column label="人设提示词" align="center" prop="prompt" />
<el-table-column label="emoji 标签" align="center" prop="emoji" />
<el-table-column
label="创建时间"
align="center"
prop="createdAt"
:formatter="dateFormatter"
width="180px"
/>
<el-table-column
label="更新时间"
align="center"
prop="updatedAt"
:formatter="dateFormatter"
width="180px"
/>
<el-table-column label="操作" align="center" min-width="120px">
<template #default="scope">
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['keyboard:character-i18n:update']"
>
编辑
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['keyboard:character-i18n:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<CharacterI18nForm ref="formRef" @success="getList" />
</template>
<script setup lang="ts">
import { isEmpty } from '@/utils/is'
import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download'
import { CharacterI18nApi, CharacterI18n } from '@/api/keyboard/characteri18n'
import CharacterI18nForm from './CharacterI18nForm.vue'
/** 键盘人设国际化内容 列表 */
defineOptions({ name: 'CharacterI18n' })
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
const loading = ref(true) // 列表的加载中
const list = ref<CharacterI18n[]>([]) // 列表的数据
const total = ref(0) // 列表的总页数
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
characterId: undefined,
locale: undefined,
characterName: undefined,
characterBackground: undefined,
prompt: undefined,
emoji: undefined,
createdAt: undefined,
createdAt: [],
updatedAt: undefined,
updatedAt: []
})
const queryFormRef = ref() // 搜索的表单
const exportLoading = ref(false) // 导出的加载中
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await CharacterI18nApi.getCharacterI18nPage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await CharacterI18nApi.deleteCharacterI18n(id)
message.success(t('common.delSuccess'))
// 刷新列表
await getList()
} catch {}
}
/** 批量删除键盘人设国际化内容 */
const handleDeleteBatch = async () => {
try {
// 删除的二次确认
await message.delConfirm()
await CharacterI18nApi.deleteCharacterI18nList(checkedIds.value);
checkedIds.value = [];
message.success(t('common.delSuccess'))
await getList();
} catch {}
}
const checkedIds = ref<number[]>([])
const handleRowCheckboxChange = (records: CharacterI18n[]) => {
checkedIds.value = records.map((item) => item.id!);
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
// 导出的二次确认
await message.exportConfirm()
// 发起导出
exportLoading.value = true
const data = await CharacterI18nApi.exportCharacterI18n(queryParams)
download.excel(data, '键盘人设国际化内容.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 初始化 **/
onMounted(() => {
getList()
})
</script>

View File

@@ -31,7 +31,7 @@
<el-input v-model="formData.currency" placeholder="请输入货币单位" />
</el-form-item>
<el-form-item label="产品描述" prop="description">
<Editor v-model="formData.description" height="150px" />
<el-input v-model="formData.description" placeholder="请输入产品描述" />
</el-form-item>
<el-form-item label="创建时间" prop="createdAt">
<el-date-picker v-model="formData.createdAt" type="date" value-format="x" placeholder="选择创建时间" />

View File

@@ -7,9 +7,6 @@
label-width="100px"
v-loading="formLoading"
>
<el-form-item label="标签名" prop="tagName">
<el-input v-model="formData.tagName" placeholder="请输入标签名" />
</el-form-item>
<el-form-item label="创建时间" prop="createdAt">
<el-date-picker
v-model="formData.createdAt"
@@ -48,7 +45,6 @@ const formLoading = ref(false) // 表单的加载中1修改时的数据加
const formType = ref('') // 表单的类型create - 新增update - 修改
const formData = ref({
id: undefined,
tagName: undefined,
createdAt: undefined,
updatedAt: undefined
})
@@ -102,7 +98,6 @@ const submitForm = async () => {
const resetForm = () => {
formData.value = {
id: undefined,
tagName: undefined,
createdAt: undefined,
updatedAt: undefined
}

View File

@@ -0,0 +1,119 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="100px"
v-loading="formLoading"
>
<el-form-item label="语言标识,如 zh-CN、en-US、ja-JP" prop="locale">
<el-input v-model="formData.locale" placeholder="请输入语言标识,如 zh-CN、en-US、ja-JP" />
</el-form-item>
<el-form-item label="标签名称(多语言)" prop="tagName">
<el-input v-model="formData.tagName" placeholder="请输入标签名称(多语言)" />
</el-form-item>
<el-form-item label="创建时间" prop="createdAt">
<el-date-picker
v-model="formData.createdAt"
type="date"
value-format="x"
placeholder="选择创建时间"
/>
</el-form-item>
<el-form-item label="更新时间" prop="updatedAt">
<el-date-picker
v-model="formData.updatedAt"
type="date"
value-format="x"
placeholder="选择更新时间"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { TagApi, TagI18n } from '@/api/keyboard/tag'
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗的是否展示
const dialogTitle = ref('') // 弹窗的标题
const formLoading = ref(false) // 表单的加载中1修改时的数据加载2提交的按钮禁用
const formType = ref('') // 表单的类型create - 新增update - 修改
const formData = ref({
id: undefined,
tagId: undefined,
locale: undefined,
tagName: undefined,
createdAt: undefined,
updatedAt: undefined
})
const formRules = reactive({
tagId: [{ required: true, message: '标签主表ID对应 keyboard_tag.id不能为空', trigger: 'blur' }],
locale: [{ required: true, message: '语言标识,如 zh-CN、en-US、ja-JP不能为空', trigger: 'blur' }],
tagName: [{ required: true, message: '标签名称(多语言)不能为空', trigger: 'blur' }]
})
const formRef = ref() // 表单 Ref
/** 打开弹窗 */
const open = async (type: string, id?: number, tagId?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
formData.value.tagId = tagId as any
// 修改时,设置数据
if (id) {
formLoading.value = true
try {
formData.value = await TagApi.getTagI18n(id)
} finally {
formLoading.value = false
}
}
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
/** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
// 校验表单
await formRef.value.validate()
// 提交请求
formLoading.value = true
try {
const data = formData.value as unknown as TagI18n
if (formType.value === 'create') {
await TagApi.createTagI18n(data)
message.success(t('common.createSuccess'))
} else {
await TagApi.updateTagI18n(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
// 发送操作成功的事件
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
tagId: undefined,
locale: undefined,
tagName: undefined,
createdAt: undefined,
updatedAt: undefined
}
formRef.value?.resetFields()
}
</script>

View File

@@ -0,0 +1,170 @@
<template>
<!-- 列表 -->
<ContentWrap>
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['keyboard:tag:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-button
type="danger"
plain
:disabled="isEmpty(checkedIds)"
@click="handleDeleteBatch"
v-hasPermi="['keyboard:tag:delete']"
>
<Icon icon="ep:delete" class="mr-5px" /> 批量删除
</el-button>
<el-table
row-key="id"
v-loading="loading"
:data="list"
:stripe="true"
:show-overflow-tooltip="true"
@selection-change="handleRowCheckboxChange"
>
<el-table-column type="selection" width="55" />
<el-table-column label="主键 Id" align="center" prop="id" />
<el-table-column label="语言标识,如 zh-CN、en-US、ja-JP" align="center" prop="locale" />
<el-table-column label="标签名称(多语言)" align="center" prop="tagName" />
<el-table-column
label="创建时间"
align="center"
prop="createdAt"
:formatter="dateFormatter"
width="180px"
/>
<el-table-column
label="更新时间"
align="center"
prop="updatedAt"
:formatter="dateFormatter"
width="180px"
/>
<el-table-column label="操作" align="center">
<template #default="scope">
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['keyboard:tag:update']"
>
编辑
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['keyboard:tag:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<TagI18nForm ref="formRef" @success="getList" />
</template>
<script setup lang="ts">
import { dateFormatter } from '@/utils/formatTime'
import { isEmpty } from '@/utils/is'
import { TagApi, TagI18n } from '@/api/keyboard/tag'
import TagI18nForm from './TagI18nForm.vue'
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const props = defineProps<{
tagId?: number // 标签主表ID对应 keyboard_tag.id主表的关联字段
}>()
const loading = ref(false) // 列表的加载中
const list = ref([]) // 列表的数据
const total = ref(0) // 列表的总页数
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
tagId: undefined as unknown
})
/** 监听主表的关联字段的变化,加载对应的子表数据 */
watch(
() => props.tagId,
(val: number) => {
if (!val) {
list.value = [] // 清空列表
return
}
queryParams.tagId = val
handleQuery()
},
{ immediate: true, deep: true }
)
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await TagApi.getTagI18nPage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
if (!props.tagId) {
message.error('请选择一个人设标签')
return
}
formRef.value.open(type, id, props.tagId)
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await TagApi.deleteTagI18n(id)
message.success(t('common.delSuccess'))
// 刷新列表
await getList()
} catch {}
}
/** 批量删除人设标签国际化 */
const handleDeleteBatch = async () => {
try {
// 删除的二次确认
await message.delConfirm()
await TagApi.deleteTagI18nList(checkedIds.value);
checkedIds.value = [];
message.success(t('common.delSuccess'))
await getList();
} catch {}
}
const checkedIds = ref<number[]>([])
const handleRowCheckboxChange = (records: TagI18n[]) => {
checkedIds.value = records.map((item) => item.id!);
}
</script>

View File

@@ -1,35 +1,60 @@
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form class="-mb-15px" :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
<el-form-item label="标签名" prop="tagName">
<el-input v-model="queryParams.tagName" placeholder="请输入标签名" clearable @keyup.enter="handleQuery"
class="!w-240px" />
</el-form-item>
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="创建时间" prop="createdAt">
<el-date-picker v-model="queryParams.createdAt" value-format="YYYY-MM-DD" type="date" placeholder="选择创建时间"
clearable class="!w-240px" />
<el-date-picker
v-model="queryParams.createdAt"
value-format="YYYY-MM-DD"
type="date"
placeholder="选择创建时间"
clearable
class="!w-240px"
/>
</el-form-item>
<el-form-item label="更新时间" prop="updatedAt">
<el-date-picker v-model="queryParams.updatedAt" value-format="YYYY-MM-DD" type="date" placeholder="选择更新时间"
clearable class="!w-240px" />
<el-date-picker
v-model="queryParams.updatedAt"
value-format="YYYY-MM-DD"
type="date"
placeholder="选择更新时间"
clearable
class="!w-240px"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery">
<Icon icon="ep:search" class="mr-5px" /> 搜索
</el-button>
<el-button @click="resetQuery">
<Icon icon="ep:refresh" class="mr-5px" /> 重置
</el-button>
<el-button type="primary" plain @click="openForm('create')" v-hasPermi="['keyboard:tag:create']">
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['keyboard:tag:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-button type="success" plain @click="handleExport" :loading="exportLoading"
v-hasPermi="['keyboard:tag:export']">
<el-button
type="success"
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['keyboard:tag:export']"
>
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
<el-button type="danger" plain :disabled="isEmpty(checkedIds)" @click="handleDeleteBatch"
v-hasPermi="['keyboard:tag:delete']">
<el-button
type="danger"
plain
:disabled="isEmpty(checkedIds)"
@click="handleDeleteBatch"
v-hasPermi="['keyboard:tag:delete']"
>
<Icon icon="ep:delete" class="mr-5px" /> 批量删除
</el-button>
</el-form-item>
@@ -38,31 +63,72 @@
<!-- 列表 -->
<ContentWrap>
<el-table row-key="id" v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true"
@selection-change="handleRowCheckboxChange">
<el-table
row-key="id"
v-loading="loading"
:data="list"
:stripe="true"
:show-overflow-tooltip="true"
highlight-current-row
@current-change="handleCurrentChange"
@selection-change="handleRowCheckboxChange"
>
<el-table-column type="selection" width="55" />
<!-- <el-table-column label="主键 Id" align="center" prop="id" /> -->
<el-table-column label="标签名" align="center" prop="tagName" />
<el-table-column label="创建时间" align="center" prop="createdAt" :formatter="dateFormatter" width="180px" />
<el-table-column label="更新时间" align="center" prop="updatedAt" :formatter="dateFormatter" width="180px" />
<el-table-column label="主键 Id" align="center" prop="id" />
<el-table-column
label="创建时间"
align="center"
prop="createdAt"
:formatter="dateFormatter"
width="180px"
/>
<el-table-column
label="更新时间"
align="center"
prop="updatedAt"
:formatter="dateFormatter"
width="180px"
/>
<el-table-column label="操作" align="center" min-width="120px">
<template #default="scope">
<el-button link type="primary" @click="openForm('update', scope.row.id)" v-hasPermi="['keyboard:tag:update']">
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['keyboard:tag:update']"
>
编辑
</el-button>
<el-button link type="danger" @click="handleDelete(scope.row.id)" v-hasPermi="['keyboard:tag:delete']">
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['keyboard:tag:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize"
@pagination="getList" />
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<TagForm ref="formRef" @success="getList" />
<!-- 子表的列表 -->
<ContentWrap>
<el-tabs model-value="tagI18n">
<el-tab-pane label="人设标签国际化" name="tagI18n">
<TagI18nList :tag-id="currentRow.id" />
</el-tab-pane>
</el-tabs>
</ContentWrap>
</template>
<script setup lang="ts">
@@ -71,6 +137,7 @@ import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download'
import { TagApi, Tag } from '@/api/keyboard/tag'
import TagForm from './TagForm.vue'
import TagI18nList from './components/TagI18nList.vue'
/** 人设标签 列表 */
defineOptions({ name: 'KeyboardTag' })
@@ -84,7 +151,6 @@ const total = ref(0) // 列表的总页数
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
tagName: undefined,
createdAt: undefined,
createdAt: [],
updatedAt: undefined,
@@ -131,9 +197,10 @@ const handleDelete = async (id: number) => {
// 发起删除
await TagApi.deleteTag(id)
message.success(t('common.delSuccess'))
currentRow.value = {}
// 刷新列表
await getList()
} catch { }
} catch {}
}
/** 批量删除人设标签 */
@@ -145,7 +212,7 @@ const handleDeleteBatch = async () => {
checkedIds.value = [];
message.success(t('common.delSuccess'))
await getList();
} catch { }
} catch {}
}
const checkedIds = ref<number[]>([])
@@ -168,6 +235,12 @@ const handleExport = async () => {
}
}
/** 选中行操作 */
const currentRow = ref({}) // 选中行
const handleCurrentChange = (row) => {
currentRow.value = row
}
/** 初始化 **/
onMounted(() => {
getList()

View File

@@ -31,6 +31,9 @@
<el-option v-for="item in themeStyleOptions" :key="item.id" :label="item.styleName" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="所属国家" prop="local">
<el-input v-model="formData.local" placeholder="请输入所属国家" />
</el-form-item>
<el-form-item label="上架状态" prop="themeStatus">
<el-radio-group v-model="formData.themeStatus">
<el-radio v-for="item in themeStatusOptions" :key="item.value" :value="item.value">
@@ -96,6 +99,7 @@ const formData = ref({
themeTag: [],
themeDownload: undefined,
themeStyle: undefined,
local: undefined,
themeStatus: undefined,
themePurchasesNumber: undefined,
createdAt: undefined,

View File

@@ -53,6 +53,11 @@
<el-input v-model="queryParams.themeDownloadUrl" placeholder="请输入下载地址" clearable @keyup.enter="handleQuery"
class="!w-240px" />
</el-form-item>
<el-form-item label="所属国家" prop="local">
<el-input v-model="queryParams.local" placeholder="请输入所属国家" clearable @keyup.enter="handleQuery"
class="!w-240px" />
</el-form-item>
<el-form-item label="排序" prop="sort">
<el-input v-model="queryParams.sort" placeholder="请输入排序" clearable @keyup.enter="handleQuery"
class="!w-240px" />
@@ -121,6 +126,7 @@
</template>
</el-table-column>
<el-table-column label="主题购买次数" align="center" prop="themePurchasesNumber" />
<el-table-column label="所属国家" align="center" prop="local" />
<el-table-column label="创建时间" align="center" prop="createdAt" :formatter="dateFormatter" width="180px" />
<el-table-column label="更新时间" align="center" prop="updatedAt" :formatter="dateFormatter" width="180px" />
<el-table-column label="预览图" align="center" prop="themePreviewImageUrl" width="120px">
@@ -196,6 +202,7 @@ const queryParams = reactive({
themePreviewImageUrl: undefined,
isFree: undefined,
themeDownloadUrl: undefined,
local: undefined,
sort: undefined,
realDownloadCount: undefined
})

View File

@@ -0,0 +1,116 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="100px"
v-loading="formLoading"
>
<el-form-item label="地区" prop="locale">
<el-input v-model="formData.locale" placeholder="请输入地区" />
</el-form-item>
<el-form-item label="正文" prop="content">
<Editor v-model="formData.content" height="150px" />
</el-form-item>
<el-form-item label="更新时间" prop="updatedAt">
<el-date-picker
v-model="formData.updatedAt"
type="date"
value-format="x"
placeholder="选择更新时间"
/>
</el-form-item>
<el-form-item label="创建时间" prop="created">
<el-date-picker
v-model="formData.created"
type="date"
value-format="x"
placeholder="选择创建时间"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { WarningMessageApi, WarningMessage } from '@/api/keyboard/warningmessage'
/** 用户注销提示信息 表单 */
defineOptions({ name: 'WarningMessageForm' })
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗的是否展示
const dialogTitle = ref('') // 弹窗的标题
const formLoading = ref(false) // 表单的加载中1修改时的数据加载2提交的按钮禁用
const formType = ref('') // 表单的类型create - 新增update - 修改
const formData = ref({
id: undefined,
locale: undefined,
content: undefined,
updatedAt: undefined,
created: undefined
})
const formRules = reactive({
})
const formRef = ref() // 表单 Ref
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
// 修改时,设置数据
if (id) {
formLoading.value = true
try {
formData.value = await WarningMessageApi.getWarningMessage(id)
} finally {
formLoading.value = false
}
}
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
/** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
// 校验表单
await formRef.value.validate()
// 提交请求
formLoading.value = true
try {
const data = formData.value as unknown as WarningMessage
if (formType.value === 'create') {
await WarningMessageApi.createWarningMessage(data)
message.success(t('common.createSuccess'))
} else {
await WarningMessageApi.updateWarningMessage(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
// 发送操作成功的事件
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
locale: undefined,
content: undefined,
updatedAt: undefined,
created: undefined
}
formRef.value?.resetFields()
}
</script>

View File

@@ -0,0 +1,243 @@
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="地区" prop="locale">
<el-input
v-model="queryParams.locale"
placeholder="请输入地区"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="更新时间" prop="updatedAt">
<el-date-picker
v-model="queryParams.updatedAt"
value-format="YYYY-MM-DD"
type="date"
placeholder="选择更新时间"
clearable
class="!w-240px"
/>
</el-form-item>
<el-form-item label="创建时间" prop="created">
<el-date-picker
v-model="queryParams.created"
value-format="YYYY-MM-DD"
type="date"
placeholder="选择创建时间"
clearable
class="!w-240px"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['keyboard:warning-message:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-button
type="success"
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['keyboard:warning-message:export']"
>
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
<el-button
type="danger"
plain
:disabled="isEmpty(checkedIds)"
@click="handleDeleteBatch"
v-hasPermi="['keyboard:warning-message:delete']"
>
<Icon icon="ep:delete" class="mr-5px" /> 批量删除
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table
row-key="id"
v-loading="loading"
:data="list"
:stripe="true"
:show-overflow-tooltip="true"
@selection-change="handleRowCheckboxChange"
>
<el-table-column type="selection" width="55" />
<el-table-column label="主键" align="center" prop="id" />
<el-table-column label="地区" align="center" prop="locale" />
<el-table-column label="正文" align="center" prop="content" />
<el-table-column
label="更新时间"
align="center"
prop="updatedAt"
:formatter="dateFormatter"
width="180px"
/>
<el-table-column
label="创建时间"
align="center"
prop="created"
:formatter="dateFormatter"
width="180px"
/>
<el-table-column label="操作" align="center" min-width="120px">
<template #default="scope">
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['keyboard:warning-message:update']"
>
编辑
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['keyboard:warning-message:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<WarningMessageForm ref="formRef" @success="getList" />
</template>
<script setup lang="ts">
import { isEmpty } from '@/utils/is'
import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download'
import { WarningMessageApi, WarningMessage } from '@/api/keyboard/warningmessage'
import WarningMessageForm from './WarningMessageForm.vue'
/** 用户注销提示信息 列表 */
defineOptions({ name: 'KeyboardWarningMessage' })
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
const loading = ref(true) // 列表的加载中
const list = ref<WarningMessage[]>([]) // 列表的数据
const total = ref(0) // 列表的总页数
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
locale: undefined,
content: undefined,
updatedAt: undefined,
updatedAt: [],
created: undefined,
created: []
})
const queryFormRef = ref() // 搜索的表单
const exportLoading = ref(false) // 导出的加载中
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await WarningMessageApi.getWarningMessagePage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await WarningMessageApi.deleteWarningMessage(id)
message.success(t('common.delSuccess'))
// 刷新列表
await getList()
} catch {}
}
/** 批量删除用户注销提示信息 */
const handleDeleteBatch = async () => {
try {
// 删除的二次确认
await message.delConfirm()
await WarningMessageApi.deleteWarningMessageList(checkedIds.value);
checkedIds.value = [];
message.success(t('common.delSuccess'))
await getList();
} catch {}
}
const checkedIds = ref<number[]>([])
const handleRowCheckboxChange = (records: WarningMessage[]) => {
checkedIds.value = records.map((item) => item.id!);
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
// 导出的二次确认
await message.exportConfirm()
// 发起导出
exportLoading.value = true
const data = await WarningMessageApi.exportWarningMessage(queryParams)
download.excel(data, '用户注销提示信息.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 初始化 **/
onMounted(() => {
getList()
})
</script>

View File

@@ -1,7 +1,7 @@
<!--
- Copyright (C) 2018-2019
- All rights reserved, Designed By www.joolun.com
芋道源码
keyLove
移除 avue 组件使用 ElementUI 原生组件
-->
<template>
@@ -21,12 +21,8 @@
</div>
</div>
<!-- 分页组件 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getMaterialPageFun"
/>
<Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize"
@pagination="getMaterialPageFun" />
</div>
<!-- 类型voice -->
<div v-else-if="props.type === 'voice'">
@@ -39,29 +35,18 @@
<WxVoicePlayer :url="scope.row.url" />
</template>
</el-table-column>
<el-table-column
label="上传时间"
align="center"
prop="createTime"
width="180"
:formatter="dateFormatter"
/>
<el-table-column label="上传时间" align="center" prop="createTime" width="180" :formatter="dateFormatter" />
<el-table-column label="操作" align="center" fixed="right">
<template #default="scope">
<el-button type="primary" link @click="selectMaterialFun(scope.row)"
>选择
<el-button type="primary" link @click="selectMaterialFun(scope.row)">选择
<Icon icon="ep:plus" />
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页组件 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getPage"
/>
<Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize"
@pagination="getPage" />
</div>
<!-- 类型video -->
<div v-else-if="props.type === 'video'">
@@ -76,34 +61,18 @@
<WxVideoPlayer :url="scope.row.url" />
</template>
</el-table-column>
<el-table-column
label="上传时间"
align="center"
prop="createTime"
width="180"
:formatter="dateFormatter"
/>
<el-table-column
label="操作"
align="center"
fixed="right"
class-name="small-padding fixed-width"
>
<el-table-column label="上传时间" align="center" prop="createTime" width="180" :formatter="dateFormatter" />
<el-table-column label="操作" align="center" fixed="right" class-name="small-padding fixed-width">
<template #default="scope">
<el-button type="primary" link @click="selectMaterialFun(scope.row)"
>选择
<el-button type="primary" link @click="selectMaterialFun(scope.row)">选择
<Icon icon="akar-icons:circle-plus" />
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页组件 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getMaterialPageFun"
/>
<Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize"
@pagination="getMaterialPageFun" />
</div>
<!-- 类型news -->
<div v-else-if="props.type === 'news'">
@@ -121,12 +90,8 @@
</div>
</div>
<!-- 分页组件 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getMaterialPageFun"
/>
<Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize"
@pagination="getMaterialPageFun" />
</div>
</div>
</template>
@@ -229,7 +194,7 @@ onMounted(async () => {
})
</script>
<style lang="scss" scoped>
@media (width >= 992px) and (width <= 1300px) {
@media (width >=992px) and (width <=1300px) {
.waterfall {
column-count: 3;
}
@@ -239,7 +204,7 @@ onMounted(async () => {
}
}
@media (width >= 768px) and (width <= 991px) {
@media (width >=768px) and (width <=991px) {
.waterfall {
column-count: 2;
}
@@ -249,7 +214,7 @@ onMounted(async () => {
}
}
@media (width <= 767px) {
@media (width <=767px) {
.waterfall {
column-count: 1;
}

View File

@@ -1,7 +1,7 @@
<!--
- Copyright (C) 2018-2019
- All rights reserved, Designed By www.joolun.com
芋道源码
keyLove
移除暂时用不到的 websocket
代码优化补充注释提升阅读性
-->
@@ -11,12 +11,9 @@
<!-- 加载更多 -->
<div v-loading="loading"></div>
<div v-if="!loading">
<div class="el-table__empty-block" v-if="hasMore" @click="loadMore"
><span class="el-table__empty-text">点击加载更多</span></div
>
<div class="el-table__empty-block" v-if="!hasMore"
><span class="el-table__empty-text">没有更多了</span></div
>
<div class="el-table__empty-block" v-if="hasMore" @click="loadMore"><span
class="el-table__empty-text">点击加载更多</span></div>
<div class="el-table__empty-block" v-if="!hasMore"><span class="el-table__empty-text">没有更多了</span></div>
</div>
<!-- 消息列表 -->

View File

@@ -2,7 +2,7 @@
- Copyright (C) 2018-2019
- All rights reserved, Designed By www.joolun.com
微信消息 - 图文
芋道源码
keyLove
代码优化补充注释提升阅读性
-->
<template>
@@ -12,11 +12,8 @@
<a v-if="index === 0" :href="article.url" target="_blank">
<div class="news-main">
<div class="news-content">
<el-image
:src="article.picUrl || article.thumbUrl"
class="material-img"
style="width: 100%; height: 120px"
/>
<el-image :src="article.picUrl || article.thumbUrl" class="material-img"
style="width: 100%; height: 120px" />
<div class="news-content-title">
<span>{{ article.title }}</span>
</div>

View File

@@ -1,7 +1,7 @@
<!--
- Copyright (C) 2018-2019
- All rights reserved, Designed By www.joolun.com
芋道源码
keyLove
移除多余的 rep 为前缀的变量 message 消息更简单
代码优化补充注释提升阅读性
优化消息的临时缓存策略发送消息时只清理被发送消息的 tab不会强制切回到 text 输入
@@ -12,7 +12,9 @@
<!-- 类型 1文本 -->
<el-tab-pane :name="ReplyType.Text">
<template #label>
<el-row align="middle"><Icon icon="ep:document" /> 文本</el-row>
<el-row align="middle">
<Icon icon="ep:document" /> 文本
</el-row>
</template>
<TabText v-model="reply.content" />
</el-tab-pane>
@@ -20,7 +22,9 @@
<!-- 类型 2图片 -->
<el-tab-pane :name="ReplyType.Image">
<template #label>
<el-row align="middle"><Icon icon="ep:picture" class="mr-5px" /> 图片</el-row>
<el-row align="middle">
<Icon icon="ep:picture" class="mr-5px" /> 图片
</el-row>
</template>
<TabImage v-model="reply" />
</el-tab-pane>
@@ -28,7 +32,9 @@
<!-- 类型 3语音 -->
<el-tab-pane :name="ReplyType.Voice">
<template #label>
<el-row align="middle"><Icon icon="ep:phone" /> 语音</el-row>
<el-row align="middle">
<Icon icon="ep:phone" /> 语音
</el-row>
</template>
<TabVoice v-model="reply" />
</el-tab-pane>
@@ -36,7 +42,9 @@
<!-- 类型 4视频 -->
<el-tab-pane :name="ReplyType.Video">
<template #label>
<el-row align="middle"><Icon icon="ep:share" /> 视频</el-row>
<el-row align="middle">
<Icon icon="ep:share" /> 视频
</el-row>
</template>
<TabVideo v-model="reply" />
</el-tab-pane>
@@ -44,7 +52,9 @@
<!-- 类型 5图文 -->
<el-tab-pane :name="ReplyType.News">
<template #label>
<el-row align="middle"><Icon icon="ep:reading" /> 图文</el-row>
<el-row align="middle">
<Icon icon="ep:reading" /> 图文
</el-row>
</template>
<TabNews v-model="reply" :news-type="newsType" />
</el-tab-pane>
@@ -52,7 +62,9 @@
<!-- 类型 6音乐 -->
<el-tab-pane :name="ReplyType.Music">
<template #label>
<el-row align="middle"><Icon icon="ep:service" />音乐</el-row>
<el-row align="middle">
<Icon icon="ep:service" />音乐
</el-row>
</template>
<TabMusic v-model="reply" />
</el-tab-pane>

View File

@@ -2,7 +2,7 @@
- Copyright (C) 2018-2019
- All rights reserved, Designed By www.joolun.com
微信消息 - 视频
芋道源码
keyLove
bug 修复
1joolun 的做法使用 mediaId 从微信公众号下载对应的 mp4 素材从而播放内容
存在的问题mediaId 有效期是 3 超过时间后无法播放
@@ -20,18 +20,9 @@
<!-- 弹窗播放 -->
<el-dialog v-model="dialogVideo" title="视频播放" append-to-body>
<video-player
v-if="dialogVideo"
class="video-player vjs-big-play-centered"
:src="props.url"
poster=""
crossorigin="anonymous"
controls
playsinline
:volume="0.6"
:width="800"
:playback-rates="[0.7, 1.0, 1.5, 2.0]"
/>
<video-player v-if="dialogVideo" class="video-player vjs-big-play-centered" :src="props.url" poster=""
crossorigin="anonymous" controls playsinline :volume="0.6" :width="800"
:playback-rates="[0.7, 1.0, 1.5, 2.0]" />
<!-- 事件暫時沒用
@mounted="handleMounted"-->
<!-- @ready="handleEvent($event)"-->

View File

@@ -2,7 +2,7 @@
- Copyright (C) 2018-2019
- All rights reserved, Designed By www.joolun.com
微信消息 - 语音
芋道源码
keyLove
bug 修复
1joolun 的做法使用 mediaId 从微信公众号下载对应的 mp4 素材从而播放内容
存在的问题mediaId 有效期是 3 超过时间后无法播放

View File

@@ -6,12 +6,7 @@
<div class="item-name">{{ item.name }}</div>
</a>
<el-row justify="center">
<el-button
type="danger"
circle
@click="emit('delete', item.id)"
v-hasPermi="['mp:material:delete']"
>
<el-button type="danger" circle @click="emit('delete', item.id)" v-hasPermi="['mp:material:delete']">
<Icon icon="ep:delete" />
</el-button>
</el-row>
@@ -31,7 +26,7 @@ const emit = defineEmits<{
</script>
<style lang="scss" scoped>
@media (width >= 992px) and (width <= 1300px) {
@media (width >=992px) and (width <=1300px) {
.waterfall {
column-count: 3;
}
@@ -41,7 +36,7 @@ const emit = defineEmits<{
}
}
@media (width >= 768px) and (width <= 991px) {
@media (width >=768px) and (width <=991px) {
.waterfall {
column-count: 2;
}
@@ -51,7 +46,7 @@ const emit = defineEmits<{
}
}
@media (width <= 767px) {
@media (width <=767px) {
.waterfall {
column-count: 1;
}
@@ -63,7 +58,7 @@ const emit = defineEmits<{
column-count: 5;
margin-top: 10px;
/* 芋道源码:增加 10px避免顶着上面 */
/* keyLove:增加 10px避免顶着上面 */
}
.waterfall-item {