diff --git a/backend/apps/pisadmin/basicinfo/migrations/0005_company_company_name_en_company_company_name_zh_tw_and_more.py b/backend/apps/pisadmin/basicinfo/migrations/0005_company_company_name_en_company_company_name_zh_tw_and_more.py new file mode 100644 index 0000000..1ae1a63 --- /dev/null +++ b/backend/apps/pisadmin/basicinfo/migrations/0005_company_company_name_en_company_company_name_zh_tw_and_more.py @@ -0,0 +1,76 @@ +# Generated by Django 4.2.14 on 2026-04-08 20:38 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('basicinfo', '0004_emailnotice'), + ] + + operations = [ + migrations.AddField( + model_name='company', + name='company_name_en', + field=models.CharField(blank=True, help_text='公司全称英文', max_length=100, null=True, verbose_name='公司全称英文'), + ), + migrations.AddField( + model_name='company', + name='company_name_zh_tw', + field=models.CharField(blank=True, help_text='公司全称繁体', max_length=100, null=True, verbose_name='公司全称繁体'), + ), + migrations.AddField( + model_name='company', + name='company_short_name_en', + field=models.CharField(blank=True, help_text='公司简称英文', max_length=50, null=True, verbose_name='公司简称英文'), + ), + migrations.AddField( + model_name='company', + name='company_short_name_zh_tw', + field=models.CharField(blank=True, help_text='公司简称繁体', max_length=50, null=True, verbose_name='公司简称繁体'), + ), + migrations.AddField( + model_name='currency', + name='currencyname_en', + field=models.CharField(blank=True, help_text='货币英文名', max_length=50, null=True, verbose_name='货币英文名'), + ), + migrations.AddField( + model_name='currency', + name='currencyname_zh_tw', + field=models.CharField(blank=True, help_text='货币繁体名', max_length=50, null=True, verbose_name='货币繁体名'), + ), + migrations.AddField( + model_name='supplier', + name='supplier_name_en', + field=models.CharField(blank=True, help_text='供应商全称英文', max_length=100, null=True, verbose_name='供应商全称英文'), + ), + migrations.AddField( + model_name='supplier', + name='supplier_name_zh_tw', + field=models.CharField(blank=True, help_text='供应商全称繁体', max_length=100, null=True, verbose_name='供应商全称繁体'), + ), + migrations.AddField( + model_name='supplier', + name='supplier_short_name_en', + field=models.CharField(blank=True, help_text='供应商简称英文', max_length=50, null=True, verbose_name='供应商简称英文'), + ), + migrations.AddField( + model_name='supplier', + name='supplier_short_name_zh_tw', + field=models.CharField(blank=True, help_text='供应商简称繁体', max_length=50, null=True, verbose_name='供应商简称繁体'), + ), + migrations.AddField( + model_name='unit', + name='unitname_en', + field=models.CharField(blank=True, help_text='计量单位英文名', max_length=50, null=True, verbose_name='计量单位英文名'), + ), + migrations.AddField( + model_name='unit', + name='unitname_zh_tw', + field=models.CharField(blank=True, help_text='计量单位繁体名', max_length=50, null=True, verbose_name='计量单位繁体名'), + ), + ] diff --git a/backend/apps/pisadmin/miscprocurement/migrations/0008_add_i18n_fields.py b/backend/apps/pisadmin/miscprocurement/migrations/0008_add_i18n_fields.py new file mode 100644 index 0000000..6777798 --- /dev/null +++ b/backend/apps/pisadmin/miscprocurement/migrations/0008_add_i18n_fields.py @@ -0,0 +1,188 @@ +# Generated by Django 4.2.14 on 2026-04-08 20:41 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('miscprocurement', '0007_alter_misclowpriceheader_item_no'), + ] + + operations = [ + migrations.CreateModel( + name='MiscProcMaterialMinPrices', + fields=[ + ('id', models.BigAutoField(help_text='Id', primary_key=True, serialize=False, verbose_name='Id')), + ('description', models.CharField(blank=True, help_text='描述', max_length=255, null=True, verbose_name='描述')), + ('modifier', models.CharField(blank=True, help_text='修改人', max_length=255, null=True, verbose_name='修改人')), + ('dept_belong_id', models.CharField(blank=True, help_text='数据归属部门', max_length=255, null=True, verbose_name='数据归属部门')), + ('update_datetime', models.DateTimeField(auto_now=True, help_text='修改时间', null=True, verbose_name='修改时间')), + ('create_datetime', models.DateTimeField(auto_now_add=True, help_text='创建时间', null=True, verbose_name='创建时间')), + ('part_id', models.CharField(db_column='partid', max_length=50, verbose_name='采购价料号')), + ('material_spec', models.CharField(db_column='material_spec', max_length=20, verbose_name='材料规格')), + ('currency_code', models.CharField(db_column='currency_code', max_length=50, verbose_name='货币代码')), + ('item_num', models.CharField(db_column='item_num', max_length=20, verbose_name='项次名')), + ('lowest_value', models.CharField(db_column='lowest_value', max_length=20, verbose_name='最低值')), + ('source_number', models.CharField(db_column='source_number', max_length=50, verbose_name='报价来源单号')), + ('quotation_time', models.DateTimeField(db_column='quotation_time', verbose_name='报价时间')), + ], + options={ + 'verbose_name': '杂采材料制程最低价信息表', + 'verbose_name_plural': '杂采材料制程最低价信息表', + 'db_table': 'pis_misc_proc_material_min_prices', + 'ordering': ('-create_datetime', 'id'), + }, + ), + migrations.CreateModel( + name='MiscProcProcessingMinPrices', + fields=[ + ('id', models.BigAutoField(help_text='Id', primary_key=True, serialize=False, verbose_name='Id')), + ('description', models.CharField(blank=True, help_text='描述', max_length=255, null=True, verbose_name='描述')), + ('modifier', models.CharField(blank=True, help_text='修改人', max_length=255, null=True, verbose_name='修改人')), + ('dept_belong_id', models.CharField(blank=True, help_text='数据归属部门', max_length=255, null=True, verbose_name='数据归属部门')), + ('update_datetime', models.DateTimeField(auto_now=True, help_text='修改时间', null=True, verbose_name='修改时间')), + ('create_datetime', models.DateTimeField(auto_now_add=True, help_text='创建时间', null=True, verbose_name='创建时间')), + ('part_id', models.CharField(db_column='partid', max_length=50, verbose_name='采购件料号')), + ('currency_code', models.CharField(db_column='currencycode', max_length=50, verbose_name='货币代码')), + ('lowest_value', models.CharField(db_column='lowest_vaule', max_length=20, verbose_name='加工费用最低价')), + ('source_number', models.CharField(blank=True, db_column='sourcenumber', max_length=50, null=True, verbose_name='报价来源单号')), + ('quotation_time', models.DateTimeField(blank=True, db_column='quotationtime', null=True, verbose_name='报价时间')), + ], + options={ + 'verbose_name': '杂采加工费用最低价信息表', + 'verbose_name_plural': '杂采加工费用最低价信息表', + 'db_table': 'pis_misc_proc_processing_min_prices', + 'ordering': ('-create_datetime', 'id'), + }, + ), + migrations.RenameIndex( + model_name='inquirymaterialcost', + new_name='pis_misc_pr_PartId_1213f3_idx', + old_name='pis_proc_in_PartId_4bedee_idx', + ), + migrations.RenameIndex( + model_name='inquiryothercost', + new_name='pis_misc_pr_PartId_d3323e_idx', + old_name='pis_proc_in_PartId_33e9d8_idx', + ), + migrations.RenameIndex( + model_name='inquiryprocesscost', + new_name='pis_misc_pr_PartId_cebba1_idx', + old_name='pis_proc_in_PartId_65ad97_idx', + ), + migrations.RenameIndex( + model_name='inquiryprofitcost', + new_name='pis_misc_pr_PartId_313c85_idx', + old_name='pis_proc_in_PartId_aaa0eb_idx', + ), + migrations.AddField( + model_name='costestimatetemplatehead', + name='template_name_en', + field=models.CharField(blank=True, db_column='TemplateNameEn', max_length=50, null=True, verbose_name='模板英文名'), + ), + migrations.AddField( + model_name='costestimatetemplatehead', + name='template_name_zh_tw', + field=models.CharField(blank=True, db_column='TemplateNameZhTw', max_length=50, null=True, verbose_name='模板繁体名'), + ), + migrations.AddField( + model_name='inquiry', + name='title_en', + field=models.CharField(blank=True, max_length=20, null=True, verbose_name='询价单英文名'), + ), + migrations.AddField( + model_name='inquiry', + name='title_zh_tw', + field=models.CharField(blank=True, max_length=20, null=True, verbose_name='询价单繁体名'), + ), + migrations.AddField( + model_name='miscprocurementmaterialinfo', + name='materialtype_en', + field=models.CharField(blank=True, help_text='材质英文名', max_length=50, null=True, verbose_name='材质英文名'), + ), + migrations.AddField( + model_name='miscprocurementmaterialinfo', + name='materialtype_zh_tw', + field=models.CharField(blank=True, help_text='材质繁体名', max_length=50, null=True, verbose_name='材质繁体名'), + ), + migrations.AddField( + model_name='miscprocurementstationinfo', + name='stationname_en', + field=models.CharField(blank=True, help_text='工站英文名', max_length=50, null=True, verbose_name='工站英文名'), + ), + migrations.AddField( + model_name='miscprocurementstationinfo', + name='stationname_zh_tw', + field=models.CharField(blank=True, help_text='工站繁体名', max_length=50, null=True, verbose_name='工站繁体名'), + ), + migrations.AlterField( + model_name='misclowpricedetail', + name='item_no', + field=models.CharField(choices=[('1', '重量'), ('2', '单价')], db_column='ItemNo', help_text='策采:重量、损耗、单价;杂采:重量、单价', max_length=100, verbose_name='项次名'), + ), + migrations.AlterField( + model_name='misclowpricedetail', + name='souce_no', + field=models.CharField(db_column='SouceNo', help_text='重量/单价取最小值时对应报价单单号;单价来自杂采材料信息时存交易厂区代码', max_length=200, verbose_name='来源单号(报价单号或交易厂区)'), + ), + migrations.AlterField( + model_name='misclowpriceheader', + name='souce_no', + field=models.CharField(blank=True, db_column='SouceNo', max_length=200, null=True, verbose_name='来源单号(报价单&询价单)'), + ), + migrations.AlterField( + model_name='rfqoperationlogs', + name='operation_type', + field=models.CharField(choices=[(1, '询价单创建'), (2, '询价单确认'), (3, '询价单发布'), (4, '询价单还原'), (5, '报价截止'), (6, '供应商报价'), (7, '比议价'), (8, '议价审核提交'), (9, '议价审核完成'), (10, '议价审核驳回')], db_column='OperationType', max_length=20, verbose_name='操作类型'), + ), + migrations.AlterModelTable( + name='inquiry', + table='pis_misc_proc_inquiry_master', + ), + migrations.AlterModelTable( + name='inquiryattachment', + table='pis_misc_proc_inquiry_attachment', + ), + migrations.AlterModelTable( + name='inquirymaterialcost', + table='pis_misc_proc_inquiry_material_cost', + ), + migrations.AlterModelTable( + name='inquiryothercost', + table='pis_misc_proc_inquiry_other_cost', + ), + migrations.AlterModelTable( + name='inquiryprocesscost', + table='pis_misc_proc_inquiry_process_cost', + ), + migrations.AlterModelTable( + name='inquiryprofitcost', + table='pis_misc_proc_inquiry_profit_cost', + ), + migrations.AlterModelTable( + name='inquirysupplier', + table='pis_misc_proc_inquiry_supplier', + ), + migrations.AddField( + model_name='miscprocprocessingminprices', + name='creator', + field=models.ForeignKey(db_constraint=False, help_text='创建人', null=True, on_delete=django.db.models.deletion.SET_NULL, related_query_name='creator_query', to=settings.AUTH_USER_MODEL, verbose_name='创建人'), + ), + migrations.AddField( + model_name='miscprocmaterialminprices', + name='creator', + field=models.ForeignKey(db_constraint=False, help_text='创建人', null=True, on_delete=django.db.models.deletion.SET_NULL, related_query_name='creator_query', to=settings.AUTH_USER_MODEL, verbose_name='创建人'), + ), + migrations.AddIndex( + model_name='miscprocprocessingminprices', + index=models.Index(fields=['part_id', 'currency_code'], name='pis_misc_pr_partid_116944_idx'), + ), + migrations.AddIndex( + model_name='miscprocmaterialminprices', + index=models.Index(fields=['part_id', 'material_spec', 'currency_code', 'item_num'], name='pis_misc_pr_partid_0a5142_idx'), + ), + ] diff --git a/backend/apps/pissupplier/migrations/0006_rename_pis_sup_quo_partid_99a155_idx_pis_misc_su_partid_526e30_idx_and_more.py b/backend/apps/pissupplier/migrations/0006_rename_pis_sup_quo_partid_99a155_idx_pis_misc_su_partid_526e30_idx_and_more.py new file mode 100644 index 0000000..825a413 --- /dev/null +++ b/backend/apps/pissupplier/migrations/0006_rename_pis_sup_quo_partid_99a155_idx_pis_misc_su_partid_526e30_idx_and_more.py @@ -0,0 +1,76 @@ +# Generated by Django 4.2.14 on 2026-04-08 20:38 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('pissupplier', '0005_alter_quotationmaster_quote_deadline_and_more'), + ] + + operations = [ + migrations.RenameIndex( + model_name='quotationitem', + new_name='pis_misc_su_Partid_526e30_idx', + old_name='pis_sup_quo_Partid_99a155_idx', + ), + migrations.RenameIndex( + model_name='quotationmaterial', + new_name='pis_misc_su_Partid_8d3a7c_idx', + old_name='pis_sup_quo_Partid_cf933e_idx', + ), + migrations.RenameIndex( + model_name='quotationother', + new_name='pis_misc_su_Partid_68c79f_idx', + old_name='pis_sup_quo_Partid_f50af0_idx', + ), + migrations.RenameIndex( + model_name='quotationprocess', + new_name='pis_misc_su_Partid_28aa7a_idx', + old_name='pis_sup_quo_Partid_e14a9f_idx', + ), + migrations.RenameIndex( + model_name='quotationprofit', + new_name='pis_misc_su_Partid_e7fb57_idx', + old_name='pis_sup_quo_Partid_b34018_idx', + ), + migrations.AddField( + model_name='quotationmaster', + name='supplier_name_en', + field=models.CharField(blank=True, help_text='供应商英文名', max_length=20, null=True, verbose_name='供应商英文名'), + ), + migrations.AddField( + model_name='quotationmaster', + name='supplier_name_zh_tw', + field=models.CharField(blank=True, help_text='供应商繁体名', max_length=20, null=True, verbose_name='供应商繁体名'), + ), + migrations.AlterModelTable( + name='quotationattachment', + table='pis_misc_sup_quotation_attachment', + ), + migrations.AlterModelTable( + name='quotationitem', + table='pis_misc_sup_quot_items', + ), + migrations.AlterModelTable( + name='quotationmaster', + table='pis_misc_sup_quotation_master', + ), + migrations.AlterModelTable( + name='quotationmaterial', + table='pis_misc_sup_quotation_material', + ), + migrations.AlterModelTable( + name='quotationother', + table='pis_misc_sup_quotation_other', + ), + migrations.AlterModelTable( + name='quotationprocess', + table='pis_misc_sup_quotation_process', + ), + migrations.AlterModelTable( + name='quotationprofit', + table='pis_misc_sup_quotation_profit', + ), + ] diff --git a/backend/dvadmin/system/management/commands/sync_menu_i18n.py b/backend/dvadmin/system/management/commands/sync_menu_i18n.py new file mode 100644 index 0000000..6778fe6 --- /dev/null +++ b/backend/dvadmin/system/management/commands/sync_menu_i18n.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- +""" +同步菜单多语言数据 +用法: python manage.py sync_menu_i18n +""" +import os +import json + +from django.core.management.base import BaseCommand + +from dvadmin.system.models import Menu, MenuButton + + +class Command(BaseCommand): + help = '同步菜单多语言数据从init_menu.json' + + def add_arguments(self, parser): + parser.add_argument( + '--reset', + action='store_true', + help='重置所有现有菜单的i18n字段为空后再同步', + ) + + def handle(self, *args, **options): + reset = options.get('reset', False) + + # 获取init_menu.json路径 + # sync_menu_i18n.py -> commands(1) -> management(2) -> system(3) -> dvadmin(4) -> backend(5) + backend_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) + json_path = os.path.join(backend_dir, 'dvadmin', 'system', 'fixtures', 'init_menu.json') + + if not os.path.exists(json_path): + self.stderr.write(f'文件不存在: {json_path}') + return + + with open(json_path, 'r', encoding='utf-8') as f: + menu_data = json.load(f) + + menu_count = 0 + button_count = 0 + + def process_menus(menus, parent_id=None): + nonlocal menu_count, button_count + + for menu in menus: + name_en = menu.get('name_en', '') + name_zh_tw = menu.get('name_zh_tw', '') + + # 更新菜单 + filter_data = { + 'name': menu['name'], + 'web_path': menu.get('web_path', ''), + 'component': menu.get('component', ''), + 'component_name': menu.get('component_name', ''), + } + + db_menu = Menu.objects.filter(**filter_data).first() + if db_menu: + if reset or not db_menu.name_en: + db_menu.name_en = name_en + if reset or not db_menu.name_zh_tw: + db_menu.name_zh_tw = name_zh_tw + db_menu.save(update_fields=['name_en', 'name_zh_tw']) + menu_count += 1 + self.stdout.write(f" 更新菜单: {menu['name']} -> en: {name_en}, zh_tw: {name_zh_tw}") + + # 处理菜单按钮 + for btn in menu.get('menu_button', []): + btn_name_en = btn.get('name_en', '') + btn_name_zh_tw = btn.get('name_zh_tw', '') + + if db_menu: + db_btn = MenuButton.objects.filter(menu=db_menu, value=btn.get('value', '')).first() + if db_btn: + if reset or not db_btn.name_en: + db_btn.name_en = btn_name_en + if reset or not db_btn.name_zh_tw: + db_btn.name_zh_tw = btn_name_zh_tw + db_btn.save(update_fields=['name_en', 'name_zh_tw']) + button_count += 1 + self.stdout.write(f" 更新按钮: {btn['name']} -> en: {btn_name_en}, zh_tw: {btn_name_zh_tw}") + + # 递归处理子菜单 + if menu.get('children'): + process_menus(menu['children'], menu.get('id')) + + self.stdout.write('开始同步菜单多语言数据...\n') + + for top_menu in menu_data: + self.stdout.write(f"\n处理顶级菜单: {top_menu['name']}") + process_menus([top_menu]) + + self.stdout.write(self.style.SUCCESS(f'\n同步完成!')) + self.stdout.write(f' 更新菜单数: {menu_count}') + self.stdout.write(f' 更新按钮数: {button_count}') diff --git a/backend/dvadmin/system/views/menu.py b/backend/dvadmin/system/views/menu.py index 24af6e0..3ae4449 100644 --- a/backend/dvadmin/system/views/menu.py +++ b/backend/dvadmin/system/views/menu.py @@ -69,14 +69,25 @@ class WebRouterSerializer(CustomModelSerializer): title = serializers.SerializerMethodField() def get_title(self, obj): - """Return title based on request language preference (D-05)""" - request = self.context.get('request') - if not request: - return obj.name - lang = getattr(request, 'LANGUAGE_CODE', None) or 'zh-hans' + lang = None + req = getattr(self, 'request', None) + if req: + user = getattr(req, 'user', None) + if user and getattr(user, 'is_authenticated', False): + lang = getattr(user, 'language', None) + # query_params.get() returns string (DRF QueryDict), or list if same param sent multiple times + _qp_lang = req.query_params.get('language', None) + if _qp_lang: + lang = _qp_lang[0] if isinstance(_qp_lang, list) else _qp_lang + if not lang: + meta_lang = req.META.get('HTTP_ACCEPT_LANGUAGE', '') + if meta_lang.startswith('en'): + lang = 'en' + elif 'zh-tw' in meta_lang or 'zh-hant' in meta_lang: + lang = 'zh-tw' if lang == 'en': return obj.name_en or obj.name - elif lang == 'zh-hant': + elif lang == 'zh-tw': return obj.name_zh_tw or obj.name return obj.name diff --git a/backend/dvadmin/system/views/user.py b/backend/dvadmin/system/views/user.py index bbf9853..c2919da 100644 --- a/backend/dvadmin/system/views/user.py +++ b/backend/dvadmin/system/views/user.py @@ -371,7 +371,7 @@ class UserViewSet(CustomModelViewSet): return ErrorResponse(msg="Invalid language code") user = request.user user.language = lang - user.save(update_fields=["language", "modifier", "modifier_time"]) + user.save(update_fields=["language", "modifier", "update_datetime"]) return DetailResponse(data={"language": lang}, msg="Language updated successfully") @action(methods=["PUT"], detail=True, permission_classes=[IsAuthenticated]) diff --git a/web/src/router/backEnd.ts b/web/src/router/backEnd.ts index 688c1ba..ccb57bc 100644 --- a/web/src/router/backEnd.ts +++ b/web/src/router/backEnd.ts @@ -267,6 +267,8 @@ export async function refreshRoutesForI18n() { storesRoutesList.setRoutesList([...(dynamicRoutes[0].children || []), ...frameOut]); // 通知侧边栏刷新菜单 mitt.emit('getBreadcrumbIndexSetFilterRoutes'); + // 清除 tagsView 缓存,重新加载时会用新语言读取标题 + Session.remove('tagsViewList'); } /**