yumi-admin/apps/src/views/app-system/components/emoji-resource-panel.vue
2026-04-22 17:06:02 +08:00

576 lines
15 KiB
Vue

<script lang="ts" setup>
import { reactive, ref } from 'vue';
import { useAccessStore } from '@vben/stores';
import type {
LegacyEmojiGroupItem,
LegacyEmojiItem,
} from '#/api/legacy/app-system';
import {
addOrUpdateEmojiConfig,
listEmojiGroupsBySysOrigin,
offShelfEmojiById,
pageEmojiConfigs,
} from '#/api/legacy/app-system';
import {
OSS_FILE_BUCKETS,
getAccessImgUrl,
simpleUploadFile,
} from '#/api/legacy/oss';
import SysOriginTag from '#/components/sys-origin-tag.vue';
import InlineFilterField from '#/views/_shared/inline-filter-field.vue';
import InlineFilterToolbar from '#/views/_shared/inline-filter-toolbar.vue';
import {
EMOJI_SOURCE_TYPE_OPTIONS,
EMOJI_TYPE_OPTIONS,
PRODUCT_SHOWCASE_OPTIONS,
formatDate,
getAllowedSysOrigins,
} from '#/views/system/shared';
import {
Button,
Input,
Modal,
Pagination,
Select,
SelectOption,
Switch,
Table,
message,
} from 'antdv-next';
const accessStore = useAccessStore();
const loading = ref(false);
const groupLoading = ref(false);
const submitLoading = ref(false);
const coverUploading = ref(false);
const sourceUploading = ref(false);
const total = ref(0);
const list = ref<LegacyEmojiItem[]>([]);
const groupList = ref<LegacyEmojiGroupItem[]>([]);
const formOpen = ref(false);
const coverInputRef = ref<HTMLInputElement>();
const sourceInputRef = ref<HTMLInputElement>();
const sysOriginOptions = getAllowedSysOrigins(accessStore.accessCodes || []);
const query = reactive<Record<string, any>>({
cursor: 1,
groupId: '',
limit: 20,
shelfStatus: undefined,
sysOrigin: sysOriginOptions[0]?.value ?? 'LIKEI',
type: undefined,
});
const form = reactive<Record<string, any>>({
coverUrl: '',
groupId: '',
id: '',
shelfStatus: '',
sort: '',
sourceType: '',
sourceUrl: '',
sysOrigin: '',
type: '',
});
const columns = [
{ dataIndex: 'sysOrigin', key: 'sysOrigin', title: '系统', width: 100 },
{ dataIndex: 'sourceType', key: 'sourceType', title: '资源类型', width: 120 },
{ dataIndex: 'coverUrl', key: 'coverUrl', title: '封面', width: 120 },
{ dataIndex: 'groupName', key: 'groupName', title: '分组', width: 160 },
{ dataIndex: 'type', key: 'type', title: '表情类型', width: 120 },
{ dataIndex: 'shelfStatus', key: 'shelfStatus', title: '上/下架', width: 120 },
{ dataIndex: 'sort', key: 'sort', title: '排序', width: 100 },
{ dataIndex: 'createTime', key: 'createTime', title: '创建时间', width: 180 },
{ dataIndex: 'actions', key: 'actions', title: '操作', width: 100 },
];
function createFormState() {
return {
coverUrl: '',
groupId: '',
id: '',
shelfStatus: '',
sort: '',
sourceType: '',
sourceUrl: '',
sysOrigin: '',
type: '',
};
}
function resetForm() {
Object.assign(form, createFormState());
}
async function loadGroupList(reset = false) {
if (reset) {
query.groupId = '';
}
groupLoading.value = true;
try {
groupList.value = await listEmojiGroupsBySysOrigin(query.sysOrigin);
} finally {
groupLoading.value = false;
}
}
async function loadData(reset = false) {
if (reset) {
query.cursor = 1;
}
loading.value = true;
try {
const result = await pageEmojiConfigs({ ...query });
list.value = result.records || [];
total.value = result.total || 0;
} finally {
loading.value = false;
}
}
function handlePageChange(page: number, pageSize: number) {
query.cursor = page;
query.limit = pageSize;
loadData();
}
function handleSysOriginChange() {
loadGroupList(true);
loadData(true);
}
function openCreate() {
resetForm();
formOpen.value = true;
}
function openEdit(record: LegacyEmojiItem) {
resetForm();
form.coverUrl = record.coverUrl ?? '';
form.groupId = record.groupId ?? '';
form.id = record.id ?? '';
form.shelfStatus = record.shelfStatus ?? '';
form.sort = record.sort ?? '';
form.sourceType = record.sourceType ?? '';
form.sourceUrl = record.sourceUrl ?? '';
form.sysOrigin = record.sysOrigin ?? '';
form.type = record.type ?? '';
formOpen.value = true;
}
function openCoverPicker() {
coverInputRef.value?.click();
}
function openSourcePicker() {
sourceInputRef.value?.click();
}
async function handleCoverFileChange(event: Event) {
const target = event.target as HTMLInputElement;
const file = target.files?.[0];
target.value = '';
if (!file) {
return;
}
coverUploading.value = true;
try {
const result = await simpleUploadFile(file, OSS_FILE_BUCKETS.svgasource);
form.coverUrl = getAccessImgUrl(result.name);
message.success('上传成功');
} finally {
coverUploading.value = false;
}
}
async function handleSourceFileChange(event: Event) {
const target = event.target as HTMLInputElement;
const file = target.files?.[0];
target.value = '';
if (!file) {
return;
}
sourceUploading.value = true;
try {
const result = await simpleUploadFile(file, OSS_FILE_BUCKETS.svgasource);
form.sourceUrl = getAccessImgUrl(result.name);
message.success('上传成功');
} finally {
sourceUploading.value = false;
}
}
function validateForm() {
if (!form.groupId) {
message.warning('请选择分组');
return false;
}
if (!String(form.coverUrl || '').trim()) {
message.warning('请上传封面');
return false;
}
if (!form.type) {
message.warning('请选择类型');
return false;
}
if (form.shelfStatus === '' || form.shelfStatus === null || form.shelfStatus === undefined) {
message.warning('请选择上架状态');
return false;
}
return true;
}
async function handleSubmit() {
if (!validateForm()) {
return;
}
submitLoading.value = true;
try {
await addOrUpdateEmojiConfig({
...form,
sysOrigin: query.sysOrigin,
});
message.success('保存成功');
formOpen.value = false;
resetForm();
await loadData(true);
} finally {
submitLoading.value = false;
}
}
async function handleShelfStatusChange(record: LegacyEmojiItem, checked: boolean) {
const previous = record.shelfStatus;
record.shelfStatus = checked;
try {
await offShelfEmojiById(record.id as number | string);
await loadData(true);
} catch (error) {
record.shelfStatus = previous;
throw error;
}
}
function getSourceTypeName(value: number | string) {
return (
EMOJI_SOURCE_TYPE_OPTIONS.find((item) => item.value === Number(value))?.name ||
value ||
'-'
);
}
function getTypeName(value?: string) {
return EMOJI_TYPE_OPTIONS.find((item) => item.value === value)?.name || value || '-';
}
loadGroupList(true);
loadData(true);
</script>
<template>
<div>
<InlineFilterToolbar class="toolbar">
<InlineFilterField label="系统">
<SysOriginSelect
v-model:value="query.sysOrigin"
style="width: 140px"
@change="handleSysOriginChange"
:options="sysOriginOptions"
></SysOriginSelect>
</InlineFilterField>
<InlineFilterField label="分组">
<Select
v-model:value="query.groupId"
:loading="groupLoading"
allow-clear
option-label-prop="label"
placeholder="分组"
style="width: 160px"
@change="loadData(true)"
>
<SelectOption
v-for="item in groupList"
:key="item.id"
:value="item.id"
:label="`${item.groupName}`">
{{ item.groupName }}
</SelectOption>
</Select>
</InlineFilterField>
<InlineFilterField label="类型">
<Select
v-model:value="query.type"
allow-clear
option-label-prop="label"
placeholder="类型"
style="width: 140px"
@change="loadData(true)"
>
<SelectOption
v-for="item in EMOJI_TYPE_OPTIONS"
:key="item.value"
:value="item.value"
:label="`${item.name}`">
{{ item.name }}
</SelectOption>
</Select>
</InlineFilterField>
<InlineFilterField label="状态">
<Select
v-model:value="query.shelfStatus"
allow-clear
option-label-prop="label"
placeholder="状态"
style="width: 140px"
@change="loadData(true)"
>
<SelectOption
v-for="item in PRODUCT_SHOWCASE_OPTIONS"
:key="String(item.value)"
:value="item.value"
:label="`${item.name}`">
{{ item.name }}
</SelectOption>
</Select>
</InlineFilterField>
<Button :loading="loading" type="primary" @click="loadData(true)">
搜索
</Button>
<Button @click="openCreate">新增</Button>
</InlineFilterToolbar>
<Table
:columns="columns"
:data-source="list"
:loading="loading"
:pagination="false"
row-key="id"
:scroll="{ x: 1380 }"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'sysOrigin'">
<SysOriginTag :value="record.sysOrigin" />
</template>
<template v-else-if="column.key === 'sourceType'">
{{ getSourceTypeName(record.sourceType as number | string) }}
</template>
<template v-else-if="column.key === 'coverUrl'">
<img
:src="record.coverUrl || 'https://dummyimage.com/56x56/e2e8f0/64748b&text=IMG'"
alt=""
class="cover-image"
>
</template>
<template v-else-if="column.key === 'groupName'">
{{ record.group?.groupName || '-' }}
</template>
<template v-else-if="column.key === 'type'">
{{ getTypeName(record.type) }}
</template>
<template v-else-if="column.key === 'shelfStatus'">
<Switch
:checked="Boolean(record.shelfStatus)"
checked-children="上架"
un-checked-children="下架"
@change="(checked: boolean) => handleShelfStatusChange(record, checked)"
/>
</template>
<template v-else-if="column.key === 'createTime'">
{{ formatDate(record.createTime) }}
</template>
<template v-else-if="column.key === 'actions'">
<Button size="small" type="link" @click="openEdit(record)">
修改
</Button>
</template>
</template>
</Table>
<div class="pager">
<Pagination
:current="query.cursor"
:page-size="query.limit"
:total="total"
show-size-changer
@change="handlePageChange"
@showSizeChange="handlePageChange"
/>
</div>
<Modal
:confirm-loading="submitLoading"
:open="formOpen"
destroy-on-close
ok-text="保存"
:title="form.id ? '修改' : '添加'"
width="700"
@cancel="formOpen = false"
@ok="handleSubmit"
>
<div class="form-grid">
<div class="form-item">
<label class="form-label">分组</label>
<Select option-label-prop="label"
v-model:value="form.groupId"
:loading="groupLoading"
placeholder="请选择分组"
>
<SelectOption
v-for="item in groupList"
:key="item.id"
:value="item.id"
:label="`${item.groupName}`">
{{ item.groupName }}
</SelectOption>
</Select>
</div>
<div class="form-item">
<label class="form-label">资源类型</label>
<Select option-label-prop="label" v-model:value="form.sourceType" placeholder="资源类型">
<SelectOption
v-for="item in EMOJI_SOURCE_TYPE_OPTIONS"
:key="item.value"
:value="item.value"
:label="`${item.name}`">
{{ item.name }}
</SelectOption>
</Select>
</div>
<div class="form-item">
<label class="form-label">封面</label>
<div class="upload-row">
<Button :loading="coverUploading" @click="openCoverPicker">上传封面</Button>
<Button v-if="form.coverUrl" type="link" @click="form.coverUrl = ''">清空</Button>
</div>
<img
v-if="form.coverUrl"
:src="form.coverUrl"
alt=""
class="cover-preview"
>
<input
ref="coverInputRef"
accept="image/*"
class="hidden-input"
type="file"
@change="handleCoverFileChange"
>
</div>
<div class="form-item">
<label class="form-label">svga/gif</label>
<div class="upload-row">
<Button :loading="sourceUploading" @click="openSourcePicker">点击上传</Button>
<Button v-if="form.sourceUrl" type="link" @click="form.sourceUrl = ''">清空</Button>
</div>
<a
v-if="form.sourceUrl"
:href="form.sourceUrl"
rel="noreferrer"
target="_blank"
class="source-link"
>
{{ form.sourceUrl }}
</a>
<input
ref="sourceInputRef"
accept=".svga,.gif,.pag"
class="hidden-input"
type="file"
@change="handleSourceFileChange"
>
</div>
<div class="form-item">
<label class="form-label">类型</label>
<Select option-label-prop="label" v-model:value="form.type" placeholder="类型">
<SelectOption
v-for="item in EMOJI_TYPE_OPTIONS"
:key="item.value"
:value="item.value"
:label="`${item.name}`">
{{ item.name }}
</SelectOption>
</Select>
</div>
<div class="form-item">
<label class="form-label">上架</label>
<Select option-label-prop="label" v-model:value="form.shelfStatus" placeholder="上架">
<SelectOption
v-for="item in PRODUCT_SHOWCASE_OPTIONS"
:key="String(item.value)"
:value="item.value"
:label="`${item.name}`">
{{ item.name }}
</SelectOption>
</Select>
</div>
<div class="form-item">
<label class="form-label">顺序</label>
<Input v-model:value="form.sort" placeholder="数字越小越靠前" />
</div>
</div>
</Modal>
</div>
</template>
<style scoped>
.toolbar {
margin-bottom: 16px;
}
.pager {
display: flex;
justify-content: flex-end;
margin-top: 16px;
}
.cover-image,
.cover-preview {
border-radius: 12px;
height: 56px;
object-fit: cover;
width: 56px;
}
.form-grid {
display: grid;
gap: 16px;
}
.form-item {
display: grid;
gap: 8px;
}
.form-label {
color: #334155;
font-size: 14px;
}
.upload-row {
display: flex;
gap: 8px;
}
.hidden-input {
display: none;
}
.source-link {
color: #2563eb;
overflow-wrap: anywhere;
}
</style>