1 В избранное 0 Ответвления 0

OSCHINA-MIRROR/zxfjd3g-wh220822_gshop-admin

В этом репозитории не указан файл с открытой лицензией (LICENSE). При использовании обратитесь к конкретному описанию проекта и его зависимостям в коде.
Клонировать/Скачать
笔记.md 67 КБ
Копировать Редактировать Web IDE Исходные данные Просмотреть построчно История
Отправлено 02.06.2025 10:05 80ff7d5

day01

1. Использование Git для управления проектом

1. Создание локального репозитория
		- Создание файла игнорирования .gitignore и настройка игнорируемых файлов/папок
		- Создание рабочей области: git init
		- Добавление кода рабочей области в кэш: git add .
		- Коммит кода из кэша в локальный репозиторий: git commit -m "init app"
2. Создание удаленного репозитория
		- Войти на gitee и создать удаленный репозиторий
3. Загрузка кода локального репозитория в удаленный репозиторий
		- Связывание: Запись адреса удаленного репозитория в локальном репозитории (название: origin)
				git remote add origin https://gitee.com/zxfjd3g/wh220822_gshop-admin.git
		- Загрузка: git push origin master
4. Если код рабочей области изменен, сначала сделать коммит в локальный репозиторий, а затем загрузить в удаленный репозиторий
		- Коммит в локальный репозиторий: git add . => git commit -m "обновление"
		- Загрузка в удаленный репозиторий: git push origin master
5. Если удаленный репозиторий обновлен, получить обновления в локальном репозитории/рабочей области
    - git pull origin master
6. Клонирование репозитория
		- Первое, что нужно сделать при входе в компанию
		- git clone адрес репозитория
		- Затем следует процесс 4/5

2. Понимание проекта```

Шаблон проекта административной панели: базовый код административного проекта уже завершен (есть базовый эффект) Вторичное развитие: реализация проектных требований на основе шаблонного проекта Познакомьтесь с двумя шаблонными проектами Цзячжэня Пан (js + vue2 + element-ui + vuex) Наш проект (ts + vue3 + element-plus + pinia + vue-router4 + axios) Запуск наших двух проектов (шаблонный проект + завершенный проект)

```js
  # Важные части
  src  
    assets # содержит статические ресурсы, такие как изображения
    components # содержит общие нероутинговые компоненты
    layout # управляет общей структурой интерфейса
    router # содержит маршрутизацию
    store # содержит данные для pinia
  		userInfo.ts  # управляет данными для авторизованных пользователей
  		index.ts  # основной файл для pinia
    styles # содержит модули стилей в формате scss
    utils # содержит модули с утилитами
      token-utils.ts # содержит модуль для хранения токенов
      request.ts # содержит модуль для переопределения axios
    views # содержит роутинговые компоненты
      login/index.vue # содержит компонент для входа в систему
    App.vue # содержит основной компонент приложения
    main.ts # содержит основной файл js
    permission.ts # содержит модуль для управления правами доступа
  .env.development # содержит переменные для среды разработки, такие как префикс для прокси
  .env.production # содержит переменные для среды продакшена, такие как префикс для прокси
  tsconfig.json # содержит конфигурацию для импорта модулей, позволяет использовать @ для подсказок
  package-lock.json # содержит точную информацию о загруженных зависимостях
  vite.config.ts # содержит конфигурацию для vite, включая настройки прокси

Связанные с интерфейсами

Тестирование интерфейсов: проверка доступности и корректности работы интерфейсовPostman: инструмент для тестирования API. Также требуется документация для тестирования.

Swagger: документация для API + инструмент для тестирования.

Использование Swagger для тестирования нескольких API, связанных с управлением брендами.

Обзор TypeScript

Тип any

Объединенные типы

Типовые утверждения

Типы перечисления

Интерфейсы

Жесткие типы

Понимание жестких типов в методах запроса Axios

Синтаксис Axios

http://lcoalhost:3000/user/3?name=tom&age=12

  1. Отправка запроса

    Как функция
    	axios(url) отправляет GET запрос
    	axios({ отправляет любой тип запроса
    		url: 'адрес запроса', // параметры params могут быть только в пути
    		method: 'get/post/delete/put' // тип запроса
    		params: {}, // указывает параметры query
    		data: {}, // параметры тела запроса
    		headers: {} // параметры заголовка запроса
    	})
    Как объект для вызова статических методов
    	.get(url, config)
    	.delete(url, config)
    	.post(url, data, config)
    	.put(url, data, config)
  2. Интерцепторы

    Регистрация интерцептора запроса
    axios.interceptors.request.use((config) => {
    	return config // для внутреннего использования AJAX запроса
    })
    Регистрация интерцептора ответа
    axios.interceptors.response.use(
    	response => {
    		return response.data.data
    	},
    	error => {
    		// обработка ошибок
    		throw error // передача вперед
    	}
    )
  3. Создание нового экземпляра Axios

    const request = axios.create({ // возвращает функцию, похожую на Axios
    	baseUrl: '/dev',
    	timeout: 20000
    })
    
    // Отправка запроса
    request.get()
    request()
    ```## Кодирование для входа и автоматического входа
    

Настройка прокси: vite.config.ts

Добавление токена в интерцептор запроса Axios: utils/request.ts

Компонент входа: views/login/index.vue

Функция запроса интерфейса: api/acl/login.ts

Типы данных ответа интерфейса: api/acl/model/loginModel.ts

Модуль Pinia для управления данными пользователя: stores/userInfo.ts

Модуль для управления токеном: utils/token-utils.ts

Настройка маршрутов для управления товарами

  • Определение компонентов маршрутов
  • Настройка таблицы маршрутов```js { path: '/product', // Путь управления продуктами name: 'Product', // Имя маршрута component: () => import('@/layout/index.vue'), // Основной компонент маршрута meta: { // Описание навигационного меню title: 'Управление продуктами', icon: 'ele-ShoppingBag' }, redirect: '/product/category/list', // Автоматическое перенаправление на стандартный подмаршрут children: [ // Описание подмаршрутов { path: 'category/list', // Путь подмаршрута name: 'Category', component: () => import('@/views/product/category/index.vue'), meta: { title: 'Управление категориями', } }, ] }

> Завершение функционала управления брендами
>
> Страница с пагинацией брендов
>
> Добавление бренда
>
> Редактирование бренда
>
> Удаление бренда



## Определение типов интерфейсов и функций запросов к интерфейсам

Определение типов: на основе структуры данных интерфейсов / на основе параметров запроса можно определить типы interface / type## Статический интерфейс для пагинации списка брендов

Использование element-plus для построения интерфейса: Card / Button / Icon / Table / Image / Pagination

Использование документации компонентов UI-библиотеки: Примеры => Затем просмотр API



## Динамическое отображение списка с пагинацией

- Проектирование данных: list / total / currentPage / pageSize / loading
- В коллбеке onMounted запросить данные пагинации: total / records
- При клике на номер страницы или выборе количества записей на странице, заново запросить данные пагинации для отображения <el-pagination @current-change="getList" />
- Необходимо заранее определить функцию для получения данных пагинации getList
- Отображение эффекта загрузки: v-loading



## Интерфейс для добавления/обновления

Компоненты element-plus: Dialog / Form / Input / Upload

Проектирование данных: isShowDialog / trademarkForm

Как определить, что это добавление или обновление: наличие значения trademarkForm.id

Ошибка: После отображения интерфейса для редактирования отображается интерфейс для добавления

​	Причина: При клике на добавление, данные для редактирования trademarkForm остаются

​	Решение: При клике на добавление, передать пустой объект бренда



## Загрузка логотипа бренда

Использование компонента Upload для загрузки изображений

​	Компонент Upload отправляет AJAX-запрос на URL, указанный в свойстве action​			Необходимо избегать кросс-доменных запросов, используя прокси-сервер для перенаправления запросов. Значение атрибута action должно содержать префикс пути.Ограничение типа и размера изображений

Использование функции beforeAvatarUpload для ограничения типа и размера изображений

Сохранение URL загруженного изображения в trademarkForm.logoUrl

В коллбеке handleAvatarSuccess сохранить данные из ответа в trademarkForm.logoUrl



## Запрос на добавление/обновление бренда

Отправка запроса на добавление/обновление, проверка наличия значения trademarkForm.id

При успешном выполнении скрыть dialog и заново получить данные для отображения



## Использование element-plus для валидации форм

Использование vee-validate для валидации форм в проекте для ПК

Необходимо изучить примеры в документации, использовать Form для валидации

Время валидации:

При вводе данных trigger: 'change' по умолчанию
При потере фокуса trigger: 'blur' необходимо указать
При нажатии кнопки отправки валидация всех полей formRef.value.validate()

Два способа кодирования

Использование встроенных правил проверки
Пользовательские правила проверки

Кодирование:
Должно соответствовать свойству в trademarkForm
<el-form-item prop="logoUrl">

rules = reactive({ tmName: [ {required: true, message: 'Ошибка', trigger: 'blur/change'}, {validator: validateTmName, trigger: 'blur/change'} ], logoUrl: [] })

const ruleFormRef = ref() ruleFormRef.value.validate()

Пользовательская проверка: {validator: функция_проверки}

  if (value === '') {
    // Если проверка не пройдена, вызовите callback с ошибкой, содержащей сообщение об ошибке
    callback(new Error('Имя должно быть введено'))
    callback('Имя должно быть введено')
  } else {
    // Если проверка пройдена, вызовите callback без параметров
    callback()
  }
}Ручное удаление сообщений об ошибках: formRef.value.clearValidate(['logoUrl'])

# День 03

> Удаление бренда
>
> CRUD операции с атрибутами платформы
>
> Выбор третьего уровня категорий     ==> pinia
>
> Список атрибутов
>
> Добавление атрибута
>
> Редактирование атрибута
>
> Удаление атрибута

## Удаление указанного бренда

Конфигурация подтверждения удаления: MessageBox

При нажатии кнопки "Подтвердить" отправляется запрос на удаление соответствующего бренда

При успешном удалении, выводится уведомление, и запрашивается обновленный список страниц

Проблема: Если на текущей странице только одна запись, запрос остается на текущей странице

Причина: Текущий номер страницы не уменьшается на 1

Решение: Перед запросом, проверьте, если записей только одна, уменьшите номер страницы на 1

Проблема: При неудачном удалении, отображается уведомление отмены

Причина: При нажатии кнопки "Отмена" или при неудачном запросе, вызывается функция ошибки

Решение: Проверьте, если ошибка - это отмена, и отобразите уведомление

## Определение типов данных для третьего уровня категорий и атрибутов платформы

Определение типов для ответов на запросы и сложных параметров запроса

Для вложенных структур данных, определите несколько интерфейсов и типов от внутреннего к внешнему## Запросы к интерфейсам для категорий и атрибутов платформы

Определение функций запроса по документации интерфейсов

Использование axios для вторичной обработки запросов request

Использование определённых типов (interface/type) для ограничения ответов и параметров запроса

## Компонент выбора третьего уровня категорий_статический

Компонент plus: Card / Form / Select / Option

## Использование pinia для управления данными выбора категорий

// useXxxStore : для генерации объекта store для управления текущими данными (сущность - это прокси)

```markdown
## Динамическое отображение трёхуровневого списка в селекторе категорий

### Инициализация, запрос получения первого уровня списка

- Генерация объекта хранилища категорий, вызов `store.getCategory1List()` в коллбэке `onMounted`: получение данных первого уровня списка с сервера и сохранение их в `state`.
- Чтение `store.category1List` и отображение первого уровня списка.

### При выборе определённой категории первого уровня

- Сбор выбранного идентификатора категории `v-model="categoryStore.category1Id"`.
- Запрос получения соответствующего списка второго уровня и отображение его.
  - Наблюдение за изменениями `categoryStore.category1Id`.
  - Вызов `categoryStore.getCategory2List` в коллбэке.

## Решение нескольких ошибок
``````javascript
1. Ошибка валидации формы
Описание проблемы: при переходе с формы редактирования на форму добавления отображаются предыдущие сообщения об ошибках. Предпосылка: `trigger: 'change'`.
Причина: при открытии формы добавления значение `tmName` изменяется, что вызывает автоматическую валидацию.
Решение: ручное очистить сообщения об ошибках: `formRef.value.clearValidate()`.
  // Причина: внутренняя асинхронная валидация запускается после выполнения коллбэка, как микротаск.
  // Если использовать только один `nextTick`, коллбэк выполнится до валидации => не будет эффекта.
  // Если использовать два `nextTick`, внутренний коллбэк выполнится после валидации => будет эффект.
  // Если использовать таймер, он будет запущен как макротаск и выполнится после валидации => будет эффект.
```## Список атрибутов

UI компоненты: Card/Button/Table/Tag Дизайн состояния данных: attrList Чтение store трёх уровней классификации id, и использование storeToRefs для их преобразования в ref объекты Только при наличии category3Id запрашивать соответствующий список атрибутов отслеживание pinia состояния category3Id, запрашивать при наличии значения, при отсутствии — очистка существующего списка атрибутов


## Добавление и редактирование атрибутов

UI компоненты: Input/Button/Table Дизайн данных: isShowList: true attr: { // AttrModel attrName: '', attrValueList: [], ... } Обновление нескольких свойств reactive объекта за один раз? Object.assign(reactive объект, новый данные объект) Упрощение операций с данными state в store объекте? store.xxx => storeToRefs(store) + декомпозиция xxx


# day04

> Добавление атрибута
>
> Редактирование атрибута
>
> Удаление атрибута
>
> Управление SPU и SKU (начальная часть)
>
> Повторение TS
>```
typeof
	JS синтаксис: получение имени типа
			typeof null 'object'
			typeof функция 'function'
			typeof массив 'object'
	TS синтаксис: получение типа данных
			базовые типы
			объект: все имена и типы свойств
			функция: типы параметров и возвращаемого значения
	Используется JS или TS? Если используется как ограничение типа => TS
ReturnType: получение типа возвращаемого значения функции
	ReturnType<typeof fn> Получение типа возвращаемого значения из типа функции
InstanceType: получение типа экземпляра компонента
	InstanceType<typeof компонент>
```## Редактирование имени значения атрибута```
 {
 	attrName: 'Имя атрибута',
 	attrValueList: [
 		{id: 1, valueName: 'Имя значения атрибута'}  // Объект значения атрибута  Необходимо добавить свойство isEdit
 	]
 }
 ```Переключение между режимом просмотра и режимом редактирования
 	Дизайн метки: isEdit: true/false   Сохраняется в объекте свойств    Добавить isEdit в TS типе?
 	Проверка метки isEdit для определения отображения span или input
 	При отображении списка значений свойств, необходимо определять отображение input или span на основе isEdit
 	При клике на span, переключение на режим редактирования  isEdit в объекте свойств становится true
 	При потере фокуса input, переключение на режим просмотра   isEdit в объекте свойств становится false
  	ошибка 1: Отображение input не получает фокус автоматически?
 			Получение объекта ElInput через ref и вызов метода focus
 			Необходимо указать тип ref: InstanceType<typeof ElInput> | InputInstance
 			Прямое использование не работает: обновление данных происходит асинхронно, поэтому прямое получение не работает
 			Решение: ждать обновления DOM перед получением => использовать nextTick
  	ошибка 2: Изменение имени свойства не может быть отменено?
 			Причина: Общий объект списка свойств, прямое изменение имени свойства в списке
 			Решение: Использование lodash cloneDeep() для глубокого копирования строки для изменения    JSON.parse(JSON.stringify(obj))
  	ошибка 3: Отклик на клик слишком мал (только текстовая область)
 			Причина: Ширина span элемента ограничена текстовой областью
  ```			Решение: Изменение стиля span для заполнения родительского элемента
  	ошибка 4: Если нет имени, при потере фокуса остается пустая строка  ==> Удаление этого объекта свойств
				Если имя совпадает с другим, при потере фокуса остается две одинаковые строки ==> Удаление этого объекта свойств## Удаление значения атрибутаattr.attrValueList.splice($index, 1)

## Добавление значения атрибута

Создать новый объект значения атрибута

Добавить в список значений атрибута attr.attrValueList

При отображении необходимо установить isEdit в true в объекте значения атрибута

Автоматическое получение фокуса: nextTick + input.focus()



## Запрос на сохранение (добавление/обновление) атрибута

Перед отправкой запроса обработать данные для отправки: attr
		Удалить isEdit из объекта значения атрибута
		Установить значение categoryId как category3Id.value

Отправить запрос на добавление/обновление атрибута

При успешном выполнении запроса: показать уведомление / вернуться на страницу со списком / обновить список

## Удаление атрибута

UI-компонент: PopConfirm (попап-подтверждение)

Отправить запрос на удаление

При успешном выполнении запроса: показать уведомление / обновить список

## Ограничение действий```
Кнопка добавления атрибута активна только после выбора третьего уровня категории: :disabled="!category3Id"
Компонент выбора категории CategorySelector активен только при отображении списка: 
	атрибут передает isShowList в CategorySelector
	Компонент CategorySelector определяет, активен ли Select на основе переданного isShowList: :disabled=""
		Проектируются props: disabled: false
Кнопка сохранения атрибута активна только при указании имени атрибута и наличии хотя бы одного значения атрибута (необходимо наличие имени значения атрибута)
	Необходимо определить вычисляемое свойство для проверки
	Кнопка активна только при указании имени атрибута и наличии хотя бы одного значения атрибута (необходимо наличие имени значения атрибута)
	Проверка наличия хотя бы одного значения атрибута с именем значения атрибута в списке значений атрибута ==> метод some
```## Понимание SPU и SKU

Оба термина означают информацию о товаре

Iphone13

Цвет: белый/черный/розовый Объем памяти: 128/256/512 ГБ Версия: сотовая сеть/мобильная сеть/общая сеть Можно загрузить несколько изображений (до 20) ... Все эти данные для одного и того же товара составляют SPU Комбинация цвета/объема памяти/версии и т.д. образует SKU Один SPU соответствует нескольким SKU


# day05

## Определение типа данных SPU

Тип данных может быть изменен в будущем

1. Добавление новых ограничений для атрибутов     (isEdit)

2. Изменение существующих атрибутов   (добавление ? / добавление undefined к типу number)

## Определение функций запроса для SPU

Согласно документации на API Использование вторично упакованного request с помощью axios request.get<any, тип данных>()/post()/put()/delete() Использование определенных интерфейсов/типов для ограничения данных ответа и параметров запроса


## Переключение отображения/условное отображение связанных интерфейсов SPU

Разбиение компонента SPU на подкомпоненты CategorySelector SpuList/SpuForm/SkuForm


## Определение перечня для хранения трех состояний

Для хранения трех состояний, которые указывают на компонент, можно определить перечисление (enum) в Vue. В родительском компоненте Spu нужно создать состояние `showStatus`, которое будет обновляться при нажатии на дочерний компонент.### Решение: использование пользовательских событий Vue
#### Код:
```vue
<!-- В родительском компоненте Spu -->
<child-component @update-status="handleStatusUpdate" />

<script>
export default {
  data() {
    return {
      showStatus: null,
    };
  },
  methods: {
    handleStatusUpdate(status) {
      this.showStatus = status;
    },
  },
};
</script>

<!-- В дочернем компоненте -->
<script>
export default {
  methods: {
    emitStatusUpdate(status) {
      this.$emit('update-status', status);
    },
  },
};
</script>

Страница списка Spu

UI компоненты: Card / Button / Table / Pagination

Дизайн данных:

data() {
  return {
    list: [], // Список Spu
    total: 0, // Общее количество элементов
    page: 1, // Текущая страница
    limit: 3, // Количество элементов на странице
    category3Id: 0, // ID категории
  };
}

Динамическое получение списка постраничной навигации

watch: {
  category3Id: {
    handler(newVal) {
      if (newVal) {
        // Вызов API для получения списка Spu
        this.$api.getSpuList(newVal, this.page, this.limit).then(response => {
          this.list = response.data.list;
          this.total = response.data.total;
        });
      } else {
        // Сброс списка и общего количества элементов
        this.list = [];
        this.total = 0;
      }
    },
    immediate: true, // Решение проблемы отображения списка при возврате на страницу
  },
},

Компонент SpuForm_статический интерфейс

UI компоненты: Form / Input / Select / Option / Table / Button

Передача объекта Spu из SpuList в SpuForm

Коммуникация между братскими компонентами

Используя родительский компонент для передачи данных через props или пользовательские события.#### Родительский компонент Spu:

<template>
  <SpuForm :spu="selectedSpu" />
</template>

<script>
export default {
  data() {
    return {
      selectedSpu: null,
    };
  },
  methods: {
    handleSpuSelect(spu) {
      this.selectedSpu = spu;
    },
  },
};
</script>

Компонент SpuList:

<template>
  <Spu @spu-select="handleSpuSelect" />
</template>

<script>
export default {
  methods: {
    handleSpuSelect(spu) {
      this.$emit('spu-select', spu);
    },
  },
};
</script>

Компонент SpuForm_инициализация запросов

  1. Получение списка всех брендов trademark.getAllTrademarkListApi()
  2. Получение списка базовых продажных атрибутов spu.getBaseSaleAttrListApi()
  3. Получение списка изображений Spu spu.getSpuImageListApi(spuId)
  4. Получение списка продажных атрибутов Spu spu.getSpuSaleAttrListApi(spuId)

Инициализация запросов при монтировании компонента

onMounted(() => {
  // Вызов всех необходимых API
});

Компонент SpuForm_название/бренд/описание

Использование v-model для двусторонней привязки данных:

<template>
  <input v-model="spuInfo.name" placeholder="Введите название" />
  <select v-model="spuInfo.brandId">
    <option v-for="brand in trademarkList" :key="brand.id" :value="brand.id">
      {{ brand.name }}
    </option>
  </select>
  <textarea v-model="spuInfo.description" placeholder="Введите описание"></textarea>
</template>

<script>
export default {
  data() {
    return {
      spuInfo: {
        name: '',
        brandId: null,
        description: '',
      },
    };
  },
};
</script>

Компонент SpuForm_изображения Spu

Динамическое отображение списка изображений

<template>
  <div v-for="image in spuInfo.spuImageList" :key="image.id">
    <img :src="image.url" alt="Spu Image" />
  </div>
</template><script>
export default {
  data() {
    return {
      spuInfo: {
        spuImageList: [],
      },
    };
  },
  methods: {
    updateSpuImageList(images) {
      this.spuInfo.spuImageList = images;
    },
  },
};
</script>
```Общая проблема: Запрошенная структура данных не удовлетворяет требованиям отображения.
Компонент Upload требует объект изображения с полями {name, url},
в то время как наши данные имеют структуру {imgName, imgUrl}.
Решение: После получения данных необходимо их преобразовать.
Присвоить значение imgName полю name, а значение imgUrl полю url.```Проблема: Компонент Upload выдает ошибку при первоначальной отрисовке, говоря, что fileList не является массивом.
Причина: spuInfo.spuImageList имеет начальное значение null, а Upload компонент не может принимать null.
Решение: При передаче spu указать spuImageList как [] {...row, xxx: value}

## Компонент SpuForm - Структура продажи SPU

Отображение списка структуры продажи SPU Отображение оставшихся списков структуры продажи в виде выпадающего списка Для отображения saleAttrList используется два метода массива: filter + some Элементы, которые еще не добавлены в список структуры продажи SPU spuSaleAttrList



# День 06

## Добавление названия значения структуры продажи SPU

Переключение между режимами просмотра и редактирования Данные: isEdit: boolean Каждый объект структуры продажи SPU имеет свойство isEdit, которое необходимо добавить в интерфейс В зависимости от isEdit динамически отображаются Input и Button Переключение из режима просмотра в режим редактирования: При нажатии кнопки isEdit становится true, и поле получает фокус Переключение из режима редактирования в режим просмотра: При потере фокуса, на основе собранных названий значений создается объект значения и добавляется в список, isEdit становится false


## Удаление структуры продажи SPU и значений

Использование метода splice массива для удаления определенного элемента или значения по индексу

Методы с обратным вызовом для перебора: forEach/filter/map/reduce/some/every/find/findIndex
Другие методы: concat/slice/includes/findIndex

## Перейти в интерфейс добавления SPU

Передать объект SPU с category3Id, spuImageList должен быть указан как [] => иначе Upload выдаст ошибку В onMount компонента SpuForm проверить, что передан spuId, и отправить запрос на получение списка изображений SPU и списка структуры продажи SPU


## Добавление структуры продажи SPU

Сбор выбранного id и имени атрибута: Определить переменную ref: valueIdValueName Использовать v-model для автоматического сбора Значение Option: id:name Кнопка добавления: Управление доступностью на основе valueIdValueName При нажатии кнопки добавления: Разделить valueIdValueName на id и имя Создать новый объект атрибута на основе структуры интерфейса Добавить объект атрибута в spuSaleAttrList


## Проверка формы SpuForm

```markdown
Проверка формы SpuForm:

```html
<el-form ref="formRef" :model="spuInfo" rules="rules">
<el-form-item prop="spuName">
const formRef = ref<FormInstance>()
const rules: FormRules = {
	spuName: [
		{required: true, message: 'Ошибка ввода', trigger: 'change/blur', type: 'array'},
		{validator: функция_валидации},
	]
}

При нажатии на кнопку "Сохранить" производится общая проверка: formRef.value.validate()

После успешной загрузки изображения, очистка ошибок: formRef.value.clearValidate()
```## Запрос на сохранение (добавление/обновление) spu

```markdown
Запрос на сохранение spu:

```javascript
При отправке запроса производится обработка данных spuInfo
1. Обработка структуры spuImage
	Целевая структура:
		{
      "imgName": "download (1).jpg",
      "imgUrl": "http://47.93.148.192:8080/xxx.jpg"
    }
  Текущая структура:
  	{
  		name: 'Имя файла изображения',
  		imgUrl: 'URL изображения'
  	}
  	// Существующее изображение
  	{
  		name: 'Имя файла изображения',
  		response: {data: 'URL изображения'}   // Новое загруженное изображение
  	}
  	
  	Необходимо добавить структуру response
  	Добавить imgName в качестве значения name
  	Передать значение imgUrl или response.data в imgUrl
2. Обработка объекта spuSaleAttr
		Удалить атрибут isEdit
		Отфильтровать объекты без значений атрибутов
Отправить запрос на добавление/обновление: addOrUpdateSpuApi
При успешном ответе: показать уведомление / перейти на страницу со списком

Ошибка: При отправке запроса, список изображений на экране отображается некорректно
			Запрос содержит два лишних атрибута spuImage
Причина:
		Перед отправкой запроса удаляется url из spuImage
		Значение spuImageList обновляется компонентом upload
Решение: Перед отправкой запроса не обновлять spuInfo.spuImageList напрямую
		Создать глубокую копию spuInfo и обработать копию

При отправке запроса показать эффект загрузки: 
	el-button   :loading="saveLoading"
При успешном или неудачном ответе скрыть эффект загрузки: try...finally{скрыть загрузку}
```## Типы данных SKU и функции запросов

## Отображение списка SKU для SPU

```markdown
Отображение списка SKU для SPU:

```html
Используйте Dialog для отображения, изучите документацию Dialog, найдите похожий пример кода
Данные:
	isShowDialog: boolean  false - показывать/скрывать диалог
	skuList: SkuListModel [] - список SKU для отображения
	spu: SpuModel  - текущий SPU (строка)
	isLoading: boolean   v-loading="isLoading" - показать эффект загрузки
  
 Запрос на получение списка SKU: getSkuListBySpuIdApi(spuId)   skuList

Отображение списка SKU```

UI компоненты: Таблица / Изображение / Кнопка / Пагинация Дизайн данных: список: SkuListModel [] общее количество: число 0 страница: число 1 размер страницы: число 10 загрузка: логическое значение false const tableData = reactive<{ список: SkuListModel общее количество: число страница: число размер страницы: число загрузка: логическое значение }>({ список: [] общее количество: 0 страница: 1 размер страницы: 10 загрузка: false })

Инициализация запроса для получения списка SKU с пагинацией: getSkuPageListApi(tableData.page, tableData.size)


# день07

> SkuForm   Добавление SKU
>
> Sku компоненты маршрутизации   Размещение/Снятие с продажи / Просмотр деталей

## SkuForm_Статический интерфейс

Форма / Ввод / Выбор / Таблица / Кнопка


## SkuForm_Инициализация данных```
Возможно, потребуется передать некоторые данные: объект spu
Возможно, потребуется отправить запросы для получения данных
	получение списка изображений spu: spuImageList/5
	получение списка атрибутов платформы: attrInfoList/2/13/61
	получение списка продажных атрибутов spu: spuSaleAttrList/5
```## SkuForm_Динамическое отображение инициализированных данных

С использованием полученных списков данных отображать их динамически с помощью v-for или таблицы


## SkuForm_Сбор данных```
Определение целевых данных:
  проверить интерфейсы, указанные в запросах, структуру данных для отправки запросов
 Источники данных для сбора
  передать данные через props
  использовать v-model для сбора данных
  слушать события: click / изменение выбора в таблице
 Сбор данных о платформенных атрибутах
  цель: [{attrId: идентификатор атрибута, valueId: идентификатор выбранного значения}]
  собранные данные: добавить идентификатор выбранного значения в объект атрибута valueId
   перед отправкой запроса: сгенерировать объект {attrId: id, valueId: valueId} на основе идентификаторов атрибута и значения
 Сбор данных о продажных атрибутах
  цель: [{"saleAttrValueId": идентификатор выбранного значения}]
  собранные данные: добавить идентификатор выбранного значения в объект атрибута valueId
   перед отправкой запроса: сгенерировать объект {saleAttrValueId: valueId}
 Сбор списка изображений
  цель: 
   [{  // SkuImage
     "imgName": "download (1).jpg",
     "imgUrl": "http://47.93.148.192:8080/xxx.jpg",
     "spuImgId": 337, // идентификатор текущего изображения spu
     "isDefault": "1"   // значение по умолчанию "1", не значение по умолчанию "0"
   }]
  собранные данные: selectedImageList
   [{ // SpuImage
     "id": 333,
     "spuId": 26,
     "imgName": "rBHu8l6UcKyAfzDsAAAPN5YrVxw870.jpg",
     "imgUrl": "http://47.93.148.192:8080/xxx.jpg"
   }]
  Изображение по умолчанию
   отображение: если текущий URL изображения равен skuInfo.skuDefaultImg, это изображение по умолчанию, отображать тег
```  	нажмите для установки по умолчанию: сохранить текущий URL изображения в skuInfo.skuDefaultImg
   	может ли кнопка быть использована: находится ли текущий объект изображения в selectedImageList
  ```## SkuForm_форма валидации``````
Широко используются пользовательские валидаторы.
1. Требование к цене/весу: должно быть больше 0.
2. Требование к платформенным и продажным атрибутам: необходимо выбрать хотя бы один.
	Проблема: При сохранении по умолчанию, вручную введенные данные не проходят валидацию.
	Причина: При выборе атрибута, свойство prop не изменяется, а изменяется значение valueId в attrList или spuSaleAttr.
	Решение: Использование пользовательского валидатора для проверки значения valueId в attrList или spuSaleAttr.
	Проблема: После выбора атрибута, автоматическая валидация не запускается.
	Причина: При выборе атрибута, свойство prop не изменяется.
	Решение: В событии change для select очистить соответствующие сообщения об ошибках.
3. Валидация изображений:
	Необходимо выбрать хотя бы одно изображение. Использование пользовательского валидатора для проверки длины selectedImageList.
	Необходимо указать изображение по умолчанию: использование prop="skuDefaultImg" в элементе el-form-item.
	Проблема: После выбора изображения по умолчанию, сообщения об ошибках не очищаются.
	Решение: В обработчике события выбора очистить сообщения об ошибках.

SkuForm_сохранение данных```

  1. Подготовка данных: skuInfo / selectedImageList / attrList / spuSaleAttrList
    1. skuAttrValueList Целевая структура: { "attrId": "2", "valueId": "9" } Текущая структура: attrList { id => attrId valueId => valueId }
  1. skuSaleAttrValueList: [], Целевая структура: { "saleAttrValueId": 258 } Текущая структура: spuSaleAttrList { valueId => saleAttrValueId }
  2. skuImageList: [], Целевая структура: { "imgName": "download (1).jpg", "imgUrl": "http://47.93.148.192:8080/xxx.jpg", "spuImgId": 337, // ID текущего изображения Spu "isDefault": "1" // По умолчанию "1", не по умолчанию "0" } Текущая структура: selectedImageList { "id": 333, "spuId": 26, "imgName": "rBHu8l6UcKyAfzDsAAAPN5YrVxw870.jpg", "imgUrl": "http://47.93.148.192:8080/xxx.jpg" }
  1. Отправка запроса на сохранение sku, передача skuInfo.
  2. При успешном ответе: вывод сообщения / перенаправление на страницу со списком Spu.
  1. Отправка запроса на включение/исключение.
  2. Обновление списка для отображения.

## Отображение информации о Sku```
UI компоненты: Drawer / Row & Col / Tag / Carousel
Дизайн данных: SkuInfoData
	isShowInfo: boolean - отображать ли информацию
	skuInfo: объект с информацией о sku
При нажатии на кнопку "Просмотреть детали":
	isShowInfo = true
	Отправка запроса для получения skuInfo.
Изменение стиля UI компонентов:
	Постарайтесь указать класс для корневого элемента компонента.
	Используйте инструмент Elements для просмотра классов элементов.
style тег указывает scoped, стили внутри него называются "локальными стилями".
``Область применения стилей scoped
	Область действия стилей scoped
		Все теги текущего компонента
		Корневые теги подкомпонентов (UI-компонентов)
	Примечание: не может влиять на внутренние теги подкомпонентов
Как сделать, чтобы стили scoped влияли на внутренние стили подкомпонентов (UI-компонентов)?
	Используйте глубокий селектор области видимости :deep(селектор подкомпонента)
Принцип:
	Принцип работы стилей scoped
		1: добавляет уникальный атрибут data-xxx ко всем тегам текущего компонента и корневым тегам подкомпонентов
		2: добавляет к селекторам стилей атрибутный селектор [data-xxx]
				что означает: целевой элемент должен иметь атрибут data-xxx => соответствует только тегам текущего компонента и корневым тегам подкомпонентов
	Принцип глубокого селектора области видимости (deep)
```Перемещает атрибутный селектор влево, что означает, что нет требований к наличию этого атрибута у целевого элемента — может соответствовать внутренним тегам подкомпонентов``` 

![image-20230220170540131](images/image-20230220170540131.png)

# day08

> Управление правами доступа

## Понимание управления правами доступа

Цель управления правами доступа: чтобы разные типы пользователей, вошедшие в систему, видели только те страницы (маршруты) и кнопки, к которым у них есть доступ.

 1. Управление данными прав доступа (3 маршрута меню)
 2. Управление правами доступа: разные пользователи, вошедшие в систему, видят разные страницы/кнопки

Уровни контроля доступа:
	Страница (роут) уровень: видит только разрешенные страницы    Грубый уровень
	Кнопка уровень: видит только разрешенные кнопки   Тонкий уровень
 Два способа регистрации роутов:
	Статическая регистрация: регистрируется при инициализации роутера   Вход / Главная страница / 404
	Динамическая регистрация: функциональные роуты страницы (за пределами статических роутов)  Управление товарами / Управление правами доступа 
 			Асинхронные роуты / Роуты прав доступа
 			router.addRoute(route)
  Роуты разделены на три категории:
  		staticRoutes статические роуты: [{}]    ==> Инициализация статической регистрации
  		allAsyncRoutes все асинхронные роуты:  [{}]  ==> Фильтрация по данным прав доступа роутов текущего пользователя routes для динамической регистрации
  		anyRoute универсальные роуты: {}  => Динамическая регистрация после данных прав доступа пользователя
  Каждый пользователь имеет соответствующие данные прав доступа, сохраненные в базе данных
 При входе пользователя, запрос на получение данных пользователя, чтобы получить данные, связанные с правами доступа
 		routes:  ["Product", "Trademark", "Attr"]
 		buttons: ["btn.Attr.add", "btn.Trademark.update"]
 		roles: ["role_test"]По данным прав доступа роутов routes, определяются все разрешенные роуты пользователя, и они регистрируются динамически.
По routes производится рекурсивное фильтрация allAsyncRoutes для генерации разрешенных роутов пользователя.
Динамическая регистрация всех разрешенных роутов пользователя: перебор роутов, router.addRoute(route).
Объединение статических роутов и разрешенных роутов пользователя используется для отображения навигации меню.
При выходе пользователя, удаляются все разрешенные роуты, регистрируются только статические роуты.

```## Потоки кодирования для контроля разрешений маршрутизации``````
1. Разбейте маршрутизацию на 3 части (статическая/асинхронная/шаблонная) и выполните статическую регистрацию для статического маршрутизационного списка.
	staticRoutes: статический маршрутизационный список
	allAsyncRoutes: все асинхронные маршрутизационные списки
	anyRoute: шаблонный маршрут (объект)
	
	createRouter({
		routes: staticRoutes
	})
2. После запроса данных разрешений пользователя, используйте данные маршрутизации разрешений routes для фильтрации всех асинхронных маршрутизационных списков allAsyncRoutes и выполнения динамической регистрации.
		Зарегистрируйте шаблонный маршрут в конце.
		Определите рекурсивную функцию фильтрации: filterAsyncRoutes
		Вызовите filterAsyncRoutes для глубокого фильтрации всех асинхронных маршрутизационных списков allAsyncRoutes по данным маршрутизации разрешений routes и создайте список маршрутизации разрешений asyncRoutes.
		Пройдите по asyncRoutes и вызовите router.addRoute(route) для регистрации каждого маршрута, а затем зарегистрируйте anyRoute.
		
		Сохраните список разрешений кнопок и список ролей в состоянии state.
3. Сгенерируйте навигационное меню разрешений на основе статического маршрутизационного списка и списка маршрутизации разрешений пользователя.
		this.menuRoutes = [...staticRoutes, ...asyncRoutes]

Понимание функции next next(): разрешить переход маршрута, в конечном итоге достигнув целевого маршрута next(path): прервать текущий переход маршрута, принудительно перейти к маршруту по указанному пути без параметров next(to.path): прервать текущий переход маршрута, перейти к целевому маршруту без параметров next(location): прервать текущий переход маршрута, принудительно перейти к маршруту по указанному местоположению с параметрами next(to): прервать текущий переход маршрута, перейти к целевому маршруту с сохранением параметров

	Причина:
		Динамически зарегистрированные маршруты не видны при текущем переходе, только после следующего перехода они становятся видимыми.
		Если в предварительном защите просто разрешить переход next(), динамически зарегистрированные маршруты не будут отображаться.
	Решение:
		Принудительно перейти к целевому маршруту:
			next(path): можно увидеть целевой маршрут, проблема: если переход был с параметрами, параметры будут потеряны
			next(to): можно увидеть целевой маршрут, и параметры не потеряются
			
ошибка 2: При смене пользователя на входе видны только некоторые разрешенные маршруты (наша проблема)
	Причина: При входе предыдущего пользователя, все подмаршруты из всех асинхронных маршрутизационных списков были отфильтрованы (allAsyncRoutes изменился).
			При входе следующего пользователя, видны не все асинхронные маршруты.
	Решение: Выполните глубокое копирование всех асинхронных маршрутизационных списков перед фильтрацией (не изменяйте все асинхронные маршрутизационные списки напрямую)
```## Выйти из системы``````
При выходе необходимо очистить все зарегистрированные маршруты с правами доступа, оставив только статические маршруты.
Проблема при несбросе маршрутов: из-за повторной регистрации маршрутов возникает путаница в маршрутизации, что приводит к ошибкам при перенаправлении на страницу входа и неудачам при перенаправлении на страницу успешного входа.

Реализация контроля прав доступа для кнопок

После входа пользователя, при открытии страницы с правами доступа, отображаются только те кнопки, к которым у пользователя есть доступ.
Данные о правах доступа для кнопок:
	buttons: ['btn.Attr.add', 'btn.Attr.update'] => в pinia в userInfoStore.state
Проверка наличия кнопки по её правам доступа в массиве buttons, если права доступа отсутствуют, кнопка не отображается.

Создание пользовательского директивы
	v-has="права доступа кнопки"
	Проверка наличия указанных прав доступа в массиве buttons, если права отсутствуют, элемент кнопки удаляется

	app.directive('has', {
		// выполнение при монтировании компонента
		// el: элемент, на котором используется директива, то есть кнопка
		// binding: содержит данные, связанные с директивой   value: значение атрибута директивы, то есть права доступа кнопки
		mounted(el, binding) {
			if (!buttons.includes(binding.value)) {
				el.parentNode.removeChild(el)
			}
		},
	})

Поток выполнения в файле permission.ts

Проверка наличия токена?
	-Есть: Проверка нахождения на странице входа?
		-Да: Принудительное перенаправление на главную страницу
		-Нет: Проверка успешности входа?
			-Да: Разрешение перехода на страницу управления
			-Нет: Запрос данных пользователя и прав доступа, и динамическая регистрация маршрутов с правами доступа?
				- Успешно: Принудительное перенаправление на целевую страницу (не разрешение перехода)
				- Неуспешно: Обновление данных пользователя (токен) и перенаправление на страницу входа
	-Нет: Проверка нахождения на странице входа?
		-Да: Разрешение перехода на страницу входа
		-Нет: Принудительное перенаправление на страницу входа
```## Ошибка: при переключении с управления атрибутами платформы на управление SPU отображаются ранее выбранные категории

Причина: они используют один и тот же pinia для хранения данных о категориях, и при переключении данные не очищаются. Решение: очистка состояния данных о категориях в pinia перед удалением компонента выбора категорий.


Опубликовать ( 0 )

Вы можете оставить комментарий после Вход в систему

1
https://api.gitlife.ru/oschina-mirror/zxfjd3g-wh220822_gshop-admin.git
git@api.gitlife.ru:oschina-mirror/zxfjd3g-wh220822_gshop-admin.git
oschina-mirror
zxfjd3g-wh220822_gshop-admin
zxfjd3g-wh220822_gshop-admin
master