Browse Source

feat(vben5): add texts manage

pull/1143/head
colin 11 months ago
parent
commit
eea3c29a24
  1. 3
      apps/vben5/apps/app-antd/src/locales/langs/en-US/abp.json
  2. 3
      apps/vben5/apps/app-antd/src/locales/langs/zh-CN/abp.json
  3. 9
      apps/vben5/apps/app-antd/src/router/routes/modules/abp.ts
  4. 15
      apps/vben5/apps/app-antd/src/views/localization/texts/index.vue
  5. 62
      apps/vben5/packages/@abp/localization/src/api/useTextsApi.ts
  6. 1
      apps/vben5/packages/@abp/localization/src/components/index.ts
  7. 40
      apps/vben5/packages/@abp/localization/src/components/languages/LocalizationLanguageTable.vue
  8. 23
      apps/vben5/packages/@abp/localization/src/components/resources/LocalizationResourceTable.vue
  9. 196
      apps/vben5/packages/@abp/localization/src/components/texts/LocalizationTextModal.vue
  10. 317
      apps/vben5/packages/@abp/localization/src/components/texts/LocalizationTextTable.vue
  11. 51
      apps/vben5/packages/@abp/localization/src/types/texts.ts

3
apps/vben5/apps/app-antd/src/locales/langs/en-US/abp.json

@ -49,7 +49,8 @@
"localization": {
"title": "Localization",
"resources": "Resources",
"languages": "Languages"
"languages": "Languages",
"texts": "Texts"
}
},
"openiddict": {

3
apps/vben5/apps/app-antd/src/locales/langs/zh-CN/abp.json

@ -49,7 +49,8 @@
"localization": {
"title": "本地化管理",
"resources": "资源管理",
"languages": "语言管理"
"languages": "语言管理",
"texts": "文档管理"
}
},
"openiddict": {

9
apps/vben5/apps/app-antd/src/router/routes/modules/abp.ts

@ -203,6 +203,15 @@ const routes: RouteRecordRaw[] = [
component: () =>
import('#/views/localization/languages/index.vue'),
},
{
meta: {
title: $t('abp.manage.localization.texts'),
icon: 'mi:text',
},
name: 'LocalizationTexts',
path: '/manage/localization/texts',
component: () => import('#/views/localization/texts/index.vue'),
},
],
},
{

15
apps/vben5/apps/app-antd/src/views/localization/texts/index.vue

@ -0,0 +1,15 @@
<script lang="ts" setup>
import { Page } from '@vben/common-ui';
import { LocalizationTextTable } from '@abp/localization';
defineOptions({
name: 'LocalizationTexts',
});
</script>
<template>
<Page>
<LocalizationTextTable />
</Page>
</template>

62
apps/vben5/packages/@abp/localization/src/api/useTextsApi.ts

@ -0,0 +1,62 @@
import type { ListResultDto } from '@abp/core';
import type {
GetTextByKeyInput,
GetTextsInput,
SetTextInput,
TextDifferenceDto,
TextDto,
} from '../types/texts';
import { useRequest } from '@abp/request';
export function useTextsApi() {
const { cancel, request } = useRequest();
/**
*
* @param input
* @returns
*/
function getListApi(
input: GetTextsInput,
): Promise<ListResultDto<TextDifferenceDto>> {
return request<ListResultDto<TextDifferenceDto>>(
'/api/abp/localization/texts',
{
method: 'GET',
params: input,
},
);
}
/**
*
* @param input
* @returns
*/
function getApi(input: GetTextByKeyInput): Promise<TextDto> {
return request<TextDto>('/api/abp/localization/texts/by-culture-key', {
method: 'GET',
params: input,
});
}
/**
*
* @param input
*/
function setApi(input: SetTextInput): Promise<void> {
return request('/api/localization/texts', {
data: input,
method: 'PUT',
});
}
return {
cancel,
getApi,
getListApi,
setApi,
};
}

1
apps/vben5/packages/@abp/localization/src/components/index.ts

@ -1,2 +1,3 @@
export { default as LocalizationLanguageTable } from './languages/LocalizationLanguageTable.vue';
export { default as LocalizationResourceTable } from './resources/LocalizationResourceTable.vue';
export { default as LocalizationTextTable } from './texts/LocalizationTextTable.vue';

40
apps/vben5/packages/@abp/localization/src/components/languages/LocalizationLanguageTable.vue

@ -8,11 +8,7 @@ import { defineAsyncComponent, h, onMounted, reactive, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { $t } from '@vben/locales';
import {
useAbpStore,
useLocalization,
useLocalizationSerializer,
} from '@abp/core';
import { useAbpStore } from '@abp/core';
import { useVbenVxeGrid } from '@abp/ui';
import {
DeleteOutlined,
@ -29,7 +25,7 @@ defineOptions({
name: 'LocalizationLanguageTable',
});
const permissionGroups = ref<LanguageDto[]>([]);
const dataSource = ref<LanguageDto[]>([]);
const pageState = reactive({
current: 1,
size: 10,
@ -37,8 +33,6 @@ const pageState = reactive({
});
const abpStore = useAbpStore();
const { Lr } = useLocalization();
const { deserialize } = useLocalizationSerializer();
const { deleteApi, getListApi } = useLanguagesApi();
const { getLocalizationApi } = useLocalizationsApi();
@ -127,13 +121,7 @@ async function onGet(input?: Record<string, string>) {
gridApi.setLoading(true);
const { items } = await getListApi(input);
pageState.total = items.length;
permissionGroups.value = items.map((item) => {
const localizableString = deserialize(item.displayName);
return {
...item,
displayName: Lr(localizableString.resourceName, localizableString.name),
};
});
dataSource.value = items;
onPageChange();
} finally {
gridApi.setLoading(false);
@ -147,7 +135,7 @@ async function onReset() {
}
function onPageChange() {
const items = permissionGroups.value.slice(
const items = dataSource.value.slice(
(pageState.current - 1) * pageState.size,
pageState.current * pageState.size,
);
@ -178,20 +166,24 @@ function onDelete(row: LanguageDto) {
onOk: async () => {
await deleteApi(row.cultureName);
message.success($t('AbpUi.DeletedSuccessfully'));
onChange();
onChange(row);
},
title: $t('AbpUi.AreYouSure'),
});
}
async function onChange() {
gridApi.query();
async function onChange(data: LanguageDto) {
const input = await gridApi.formApi.getValues();
onGet(input);
//
const cultureName = abpStore.localization!.currentCulture.cultureName;
const localization = await getLocalizationApi({
cultureName,
});
abpStore.setLocalization(localization);
const cultureName =
abpStore.application!.localization.currentCulture.cultureName;
if (data.cultureName === cultureName) {
const localization = await getLocalizationApi({
cultureName,
});
abpStore.setLocalization(localization);
}
}
onMounted(onGet);

23
apps/vben5/packages/@abp/localization/src/components/resources/LocalizationResourceTable.vue

@ -8,11 +8,7 @@ import { defineAsyncComponent, h, onMounted, reactive, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { $t } from '@vben/locales';
import {
useAbpStore,
useLocalization,
useLocalizationSerializer,
} from '@abp/core';
import { useAbpStore } from '@abp/core';
import { useVbenVxeGrid } from '@abp/ui';
import {
DeleteOutlined,
@ -29,7 +25,7 @@ defineOptions({
name: 'LocalizationResourceTable',
});
const permissionGroups = ref<ResourceDto[]>([]);
const dataSource = ref<ResourceDto[]>([]);
const pageState = reactive({
current: 1,
size: 10,
@ -37,8 +33,6 @@ const pageState = reactive({
});
const abpStore = useAbpStore();
const { Lr } = useLocalization();
const { deserialize } = useLocalizationSerializer();
const { deleteApi, getListApi } = useResourcesApi();
const { getLocalizationApi } = useLocalizationsApi();
@ -121,13 +115,7 @@ async function onGet(input?: Record<string, string>) {
gridApi.setLoading(true);
const { items } = await getListApi(input);
pageState.total = items.length;
permissionGroups.value = items.map((item) => {
const localizableString = deserialize(item.displayName);
return {
...item,
displayName: Lr(localizableString.resourceName, localizableString.name),
};
});
dataSource.value = items;
onPageChange();
} finally {
gridApi.setLoading(false);
@ -141,7 +129,7 @@ async function onReset() {
}
function onPageChange() {
const items = permissionGroups.value.slice(
const items = dataSource.value.slice(
(pageState.current - 1) * pageState.size,
pageState.current * pageState.size,
);
@ -179,7 +167,8 @@ function onDelete(row: ResourceDto) {
}
async function onChange() {
gridApi.query();
const input = await gridApi.formApi.getValues();
onGet(input);
//
const cultureName = abpStore.localization!.currentCulture.cultureName;
const localization = await getLocalizationApi({

196
apps/vben5/packages/@abp/localization/src/components/texts/LocalizationTextModal.vue

@ -0,0 +1,196 @@
<script setup lang="ts">
import type { FormInstance } from 'ant-design-vue';
import type { LanguageDto } from '../../types/languages';
import type { ResourceDto } from '../../types/resources';
import type {
GetTextByKeyInput,
TextDifferenceDto,
TextDto,
} from '../../types/texts';
import {
defineEmits,
defineOptions,
onMounted,
ref,
toValue,
useTemplateRef,
} from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { Form, Input, message, Select, Textarea } from 'ant-design-vue';
import { useLanguagesApi } from '../../api/useLanguagesApi';
import { useResourcesApi } from '../../api/useResourcesApi';
import { useTextsApi } from '../../api/useTextsApi';
defineOptions({
name: 'LocalizationTextModal',
});
const emits = defineEmits<{
(event: 'change', data: TextDto): void;
}>();
const FormItem = Form.Item;
const defaultModel = {} as TextDto;
const isEditModal = ref(false);
const form = useTemplateRef<FormInstance>('form');
const formModel = ref<TextDto>({ ...defaultModel });
const resources = ref<ResourceDto[]>([]);
const languages = ref<LanguageDto[]>([]);
const { getListApi: getLanguagesApi } = useLanguagesApi();
const { getListApi: getResourcesApi } = useResourcesApi();
const { cancel, getApi, setApi } = useTextsApi();
const [Modal, modalApi] = useVbenModal({
class: 'w-1/2',
draggable: true,
fullscreenButton: false,
onCancel() {
modalApi.close();
},
onClosed() {
cancel('LocalizationTextModal has closed!');
},
onConfirm: onSubmit,
onOpenChange: async (isOpen: boolean) => {
if (isOpen) {
isEditModal.value = false;
modalApi.setState({
title: $t('LocalizationManagement.Text:AddNew'),
});
const modalData = modalApi.getData<TextDifferenceDto>();
formModel.value = { ...modalData };
await onGet();
}
},
title: $t('LocalizationManagement.Text:AddNew'),
});
async function onInit() {
const [languageRes, resourceRes] = await Promise.all([
getLanguagesApi(),
getResourcesApi(),
]);
languages.value = languageRes.items;
resources.value = resourceRes.items;
}
async function onGet() {
const dto = modalApi.getData<TextDifferenceDto>();
if (dto.targetCultureName) {
isEditModal.value = true;
await onLoad({
cultureName: dto.targetCultureName,
key: dto.key,
resourceName: dto.resourceName,
});
}
}
async function onLanguageChange(value: string) {
const dto = modalApi.getData<TextDifferenceDto>();
if (dto.targetCultureName) {
await onLoad({
cultureName: value,
key: dto.key,
resourceName: dto.resourceName,
});
}
}
async function onLoad(input: GetTextByKeyInput) {
try {
modalApi.setState({ loading: true });
const textDto = await getApi(input);
formModel.value = textDto;
modalApi.setState({
title: `${$t('AbpLocalization.Texts')} - ${textDto.key}`,
});
} finally {
modalApi.setState({ loading: false });
}
}
async function onSubmit() {
await form.value?.validate();
try {
modalApi.setState({ submitting: true });
const input = toValue(formModel);
await setApi(input);
message.success($t('AbpUi.SavedSuccessfully'));
emits('change', input);
modalApi.close();
} finally {
modalApi.setState({ submitting: false });
}
}
onMounted(onInit);
</script>
<template>
<Modal>
<Form
ref="form"
:label-col="{ span: 6 }"
:model="formModel"
:wrapper-col="{ span: 18 }"
>
<FormItem
:label="$t('AbpLocalization.DisplayName:CultureName')"
name="cultureName"
required
>
<Select
v-model:value="formModel.cultureName"
:options="languages"
:field-names="{ label: 'displayName', value: 'cultureName' }"
@change="(value) => onLanguageChange(value!.toString())"
/>
</FormItem>
<FormItem
:label="$t('AbpLocalization.DisplayName:ResourceName')"
name="resourceName"
required
>
<Select
v-model:value="formModel.resourceName"
:options="resources"
:disabled="isEditModal"
:field-names="{ label: 'displayName', value: 'name' }"
/>
</FormItem>
<FormItem
:label="$t('AbpLocalization.DisplayName:Key')"
name="key"
required
>
<Input
:disabled="isEditModal"
v-model:value="formModel.key"
autocomplete="off"
/>
</FormItem>
<FormItem
:label="$t('AbpLocalization.DisplayName:Value')"
name="value"
required
>
<Textarea
:auto-size="{ minRows: 3 }"
v-model:value="formModel.value"
autocomplete="off"
/>
</FormItem>
</Form>
</Modal>
</template>
<style scoped></style>

317
apps/vben5/packages/@abp/localization/src/components/texts/LocalizationTextTable.vue

@ -0,0 +1,317 @@
<script setup lang="ts">
import type { VbenFormProps, VxeGridListeners, VxeGridProps } from '@abp/ui';
import type { LanguageDto, ResourceDto } from '../../types';
import type { TextDifferenceDto, TextDto } from '../../types/texts';
import { defineAsyncComponent, h, onMounted, reactive, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { useAbpStore } from '@abp/core';
import { useVbenVxeGrid } from '@abp/ui';
import { EditOutlined, PlusOutlined } from '@ant-design/icons-vue';
import { Button, Select } from 'ant-design-vue';
import { useLanguagesApi } from '../../api/useLanguagesApi';
import { useLocalizationsApi } from '../../api/useLocalizationsApi';
import { useResourcesApi } from '../../api/useResourcesApi';
import { useTextsApi } from '../../api/useTextsApi';
import { ResourcesPermissions } from '../../constants/permissions';
defineOptions({
name: 'LocalizationTextTable',
});
const dataSource = ref<TextDifferenceDto[]>([]);
const resources = ref<ResourceDto[]>([]);
const languages = ref<LanguageDto[]>([]);
const targetValueOptions = reactive([
{
label: $t('AbpLocalization.DisplayName:Any'),
value: 'false',
},
{
label: $t('AbpLocalization.DisplayName:OnlyNull'),
value: 'true',
},
]);
const pageState = reactive({
current: 1,
size: 10,
total: 0,
});
const abpStore = useAbpStore();
const { getListApi } = useTextsApi();
const { getListApi: getLanguagesApi } = useLanguagesApi();
const { getListApi: getResourcesApi } = useResourcesApi();
const { getLocalizationApi } = useLocalizationsApi();
const formOptions: VbenFormProps = {
//
collapsed: false,
collapsedRows: 2,
commonConfig: {
//
componentProps: {
class: 'w-full',
},
},
compact: false,
handleReset: onReset,
async handleSubmit(params) {
pageState.current = 1;
await onGet(params);
},
schema: [
{
component: 'Input',
fieldName: 'cultureName',
formItemClass: 'items-baseline',
label: $t('AbpLocalization.DisplayName:CultureName'),
rules: 'selectRequired',
},
{
component: 'Input',
fieldName: 'targetCultureName',
formItemClass: 'items-baseline',
label: $t('AbpLocalization.DisplayName:TargetCultureName'),
rules: 'selectRequired',
},
{
component: 'Input',
fieldName: 'resourceName',
label: $t('AbpLocalization.DisplayName:ResourceName'),
},
{
component: 'Input',
fieldName: 'onlyNull',
label: $t('AbpLocalization.DisplayName:TargetValue'),
},
{
component: 'Input',
fieldName: 'filter',
formItemClass: 'col-span-2 items-baseline',
label: $t('AbpUi.Search'),
},
],
//
showCollapseButton: true,
//
submitOnEnter: true,
};
const gridOptions: VxeGridProps<TextDifferenceDto> = {
columns: [
{
align: 'left',
field: 'key',
minWidth: 150,
title: $t('AbpLocalization.DisplayName:Key'),
},
{
align: 'left',
field: 'value',
minWidth: 150,
title: $t('AbpLocalization.DisplayName:Value'),
},
{
align: 'left',
field: 'targetValue',
minWidth: 150,
title: $t('AbpLocalization.DisplayName:TargetValue'),
},
{
align: 'left',
field: 'resourceName',
minWidth: 150,
title: $t('AbpLocalization.DisplayName:ResourceName'),
},
{
field: 'action',
fixed: 'right',
slots: { default: 'action' },
title: $t('AbpUi.Actions'),
width: 220,
},
],
exportConfig: {},
keepSource: true,
toolbarConfig: {
custom: true,
export: true,
refresh: false,
zoom: true,
},
};
const gridEvents: VxeGridListeners<TextDifferenceDto> = {
pageChange(params) {
pageState.current = params.currentPage;
pageState.size = params.pageSize;
onPageChange();
},
};
const [Grid, gridApi] = useVbenVxeGrid({
formOptions,
gridEvents,
gridOptions,
});
const [LocalizationTextModal, modalApi] = useVbenModal({
connectedComponent: defineAsyncComponent(
() => import('./LocalizationTextModal.vue'),
),
});
async function onInit() {
const [languageRes, resourceRes] = await Promise.all([
getLanguagesApi(),
getResourcesApi(),
]);
languages.value = languageRes.items;
resources.value = resourceRes.items;
}
function onFieldChange(fieldName: string, value?: string) {
gridApi.formApi.setFieldValue(fieldName, value);
}
async function onGet(input: Record<string, string>) {
try {
gridApi.setLoading(true);
const { items } = await getListApi(input as any);
pageState.total = items.length;
dataSource.value = items;
onPageChange();
} finally {
gridApi.setLoading(false);
}
}
async function onReset() {
await gridApi.formApi.resetForm();
const input = await gridApi.formApi.getValues();
await onGet(input);
}
function onPageChange() {
const items = dataSource.value.slice(
(pageState.current - 1) * pageState.size,
pageState.current * pageState.size,
);
gridApi.setGridOptions({
data: items,
pagerConfig: {
currentPage: pageState.current,
pageSize: pageState.size,
total: pageState.total,
},
});
}
async function onCreate() {
const input = await gridApi.formApi.getValues();
modalApi.setData({
cultureName: input.targetCultureName,
resourceName: input.resourceName,
});
modalApi.open();
}
function onUpdate(row: TextDifferenceDto) {
modalApi.setData(row);
modalApi.open();
}
async function onChange(data: TextDto) {
const input = await gridApi.formApi.getValues();
onGet(input);
//
const cultureName =
abpStore.application!.localization.currentCulture.cultureName;
if (data.cultureName === cultureName) {
const localization = await getLocalizationApi({
cultureName,
});
abpStore.setLocalization(localization);
}
}
onMounted(onInit);
</script>
<template>
<Grid :table-title="$t('AbpLocalization.Texts')">
<template #form-cultureName="{ modelValue }">
<Select
class="w-full"
allow-clear
:options="languages"
:value="modelValue"
:field-names="{ label: 'displayName', value: 'cultureName' }"
@change="(value) => onFieldChange('cultureName', value?.toString())"
/>
</template>
<template #form-targetCultureName="{ modelValue }">
<Select
class="w-full"
allow-clear
:options="languages"
:value="modelValue"
:field-names="{ label: 'displayName', value: 'cultureName' }"
@change="
(value) => onFieldChange('targetCultureName', value?.toString())
"
/>
</template>
<template #form-resourceName="{ modelValue }">
<Select
class="w-full"
allow-clear
:options="resources"
:value="modelValue"
:field-names="{ label: 'displayName', value: 'name' }"
@change="(value) => onFieldChange('resourceName', value?.toString())"
/>
</template>
<template #form-onlyNull="{ modelValue }">
<Select
class="w-full"
allow-clear
:options="targetValueOptions"
:value="modelValue"
@change="(value) => onFieldChange('onlyNull', value?.toString())"
/>
</template>
<template #toolbar-tools>
<Button
:icon="h(PlusOutlined)"
type="primary"
v-access:code="[ResourcesPermissions.Create]"
@click="onCreate"
>
{{ $t('LocalizationManagement.Text:AddNew') }}
</Button>
</template>
<template #action="{ row }">
<div class="flex flex-row">
<Button
:icon="h(EditOutlined)"
block
type="link"
v-access:code="[ResourcesPermissions.Update]"
@click="onUpdate(row)"
>
{{ $t('AbpUi.Edit') }}
</Button>
</div>
</template>
</Grid>
<LocalizationTextModal @change="onChange" />
</template>
<style scoped></style>

51
apps/vben5/packages/@abp/localization/src/types/texts.ts

@ -0,0 +1,51 @@
interface GetTextByKeyInput {
cultureName: string;
key: string;
resourceName: string;
}
interface GetTextsInput {
cultureName: string;
filter?: string;
onlyNull?: boolean;
resourceName?: string;
targetCultureName: string;
}
interface SetTextInput {
cultureName: string;
key: string;
resourceName: string;
value: string;
}
interface RestoreDefaultTextInput {
cultureName: string;
key: string;
resourceName: string;
}
interface TextDifferenceDto {
cultureName: string;
key: string;
resourceName: string;
targetCultureName: string;
targetValue: string;
value: string;
}
interface TextDto {
cultureName: string;
key: string;
resourceName: string;
value: string;
}
export type {
GetTextByKeyInput,
GetTextsInput,
RestoreDefaultTextInput,
SetTextInput,
TextDifferenceDto,
TextDto,
};
Loading…
Cancel
Save