Compare commits
10 commits
6ca9d89b8d
...
b12c79fa21
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b12c79fa21 | ||
|
|
142a422052 | ||
|
|
4735899c39 | ||
|
|
a62258819f | ||
|
|
122f123ba6 | ||
|
|
3a068f57dc | ||
|
|
5593d59279 | ||
|
|
f3660160a5 | ||
|
|
d25d121293 | ||
|
|
a25fb939b3 |
11 changed files with 317 additions and 90 deletions
40
.forgejo/workflows/make-image.yml
Normal file
40
.forgejo/workflows/make-image.yml
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v[0-9]+*'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
IMAGE_NAME: mtv2-frontend
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-push:
|
||||||
|
name: Make image
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Login to Forgejo
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: repo.nqws.ru
|
||||||
|
username: ${{ secrets.MAINTAINER_USERNAME }}
|
||||||
|
password: ${{ secrets.MAINTAINER_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract version from tag
|
||||||
|
id: extract_version
|
||||||
|
run: |
|
||||||
|
VERSION=${GITHUB_REF#refs/tags/}
|
||||||
|
echo "VERSION=${VERSION}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Make image
|
||||||
|
run: |
|
||||||
|
docker buildx build --platform linux/amd64 \
|
||||||
|
--tag repo.nqws.ru/${{ github.repository }}:latest \
|
||||||
|
--tag repo.nqws.ru/${{ github.repository }}:${{ env.VERSION }} \
|
||||||
|
--push .
|
||||||
14
Dockerfile
14
Dockerfile
|
|
@ -1,5 +1,17 @@
|
||||||
|
FROM node:24.10-alpine3.22 AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci --prefer-offline --no-audit
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
FROM nginx:alpine
|
FROM nginx:alpine
|
||||||
COPY dist/ /usr/share/nginx/html
|
|
||||||
|
COPY --from=builder /app/dist/ /usr/share/nginx/html/
|
||||||
|
|
||||||
COPY nginx.conf /etc/nginx/nginx.conf
|
COPY nginx.conf /etc/nginx/nginx.conf
|
||||||
|
|
||||||
|
|
|
||||||
35
src/App.vue
35
src/App.vue
|
|
@ -1,23 +1,40 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import NavBar from '@/components/Navbar/NavBar.vue'
|
import NavBar from '@/components/Navbar/NavBar.vue'
|
||||||
|
import Footer from '@/components/Footer/Footer.vue'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<NavBar />
|
<n-config-provider>
|
||||||
|
<n-message-provider>
|
||||||
|
<n-dialog-provider>
|
||||||
|
<n-notification-provider>
|
||||||
|
<NavBar />
|
||||||
|
|
||||||
<n-grid responsive="screen" item-responsive cols="24" :x-gap="16" :y-gap="16" class="shift">
|
<n-grid
|
||||||
<n-gi span="xs:1 s:1 m:2 l:2 xl:3 xxl:3" />
|
responsive="screen"
|
||||||
|
item-responsive
|
||||||
|
cols="24"
|
||||||
|
:x-gap="16"
|
||||||
|
:y-gap="16"
|
||||||
|
class="shift"
|
||||||
|
>
|
||||||
|
<n-gi span="xs:1 s:1 m:2 l:2 xl:3 xxl:3" />
|
||||||
|
|
||||||
<n-gi span="xs:22 s:22 m:20 l:20 xl:18 xxl:18">
|
<n-gi span="xs:22 s:22 m:20 l:20 xl:18 xxl:18">
|
||||||
<router-view />
|
<router-view />
|
||||||
</n-gi>
|
</n-gi>
|
||||||
|
|
||||||
<n-gi span="xs:1 s:1 m:2 l:2 xl:3 xxl:3" />
|
<n-gi span="xs:1 s:1 m:2 l:2 xl:3 xxl:3" />
|
||||||
</n-grid>
|
</n-grid>
|
||||||
|
</n-notification-provider>
|
||||||
|
</n-dialog-provider>
|
||||||
|
</n-message-provider>
|
||||||
|
</n-config-provider>
|
||||||
|
<Footer />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.shift{
|
.shift {
|
||||||
padding-top: 10px;
|
padding-top: 10px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -137,11 +137,9 @@ const chartOptions = {
|
||||||
watch(
|
watch(
|
||||||
() => props.chartsData,
|
() => props.chartsData,
|
||||||
(newData) => {
|
(newData) => {
|
||||||
// Сброс состояния
|
|
||||||
showPlaceholder.value = false
|
showPlaceholder.value = false
|
||||||
placeholderMessage.value = ''
|
placeholderMessage.value = ''
|
||||||
|
|
||||||
// Случай 1: null или undefined
|
|
||||||
if (newData === null || newData === undefined) {
|
if (newData === null || newData === undefined) {
|
||||||
showPlaceholder.value = true
|
showPlaceholder.value = true
|
||||||
placeholderMessage.value = 'No prices found'
|
placeholderMessage.value = 'No prices found'
|
||||||
|
|
@ -149,7 +147,6 @@ watch(
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Случай 2: явная ошибка в объекте
|
|
||||||
if (typeof newData === 'object' && newData.error) {
|
if (typeof newData === 'object' && newData.error) {
|
||||||
showPlaceholder.value = true
|
showPlaceholder.value = true
|
||||||
placeholderMessage.value = newData.error
|
placeholderMessage.value = newData.error
|
||||||
|
|
@ -157,13 +154,11 @@ watch(
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Случай 3: нормальный объект — пробуем преобразовать
|
|
||||||
let datasets = []
|
let datasets = []
|
||||||
if (newData.origins && Array.isArray(newData.origins)) {
|
if (newData.origins && Array.isArray(newData.origins)) {
|
||||||
datasets = transformToChartJSSeries(newData)
|
datasets = transformToChartJSSeries(newData)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔥 Ключевое изменение: проверяем, есть ли хоть один датасет
|
|
||||||
if (datasets.length === 0) {
|
if (datasets.length === 0) {
|
||||||
showPlaceholder.value = true
|
showPlaceholder.value = true
|
||||||
placeholderMessage.value = 'No prices found'
|
placeholderMessage.value = 'No prices found'
|
||||||
|
|
|
||||||
48
src/components/CopyToClipboard.vue
Normal file
48
src/components/CopyToClipboard.vue
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
<script setup>
|
||||||
|
import { useMessage } from 'naive-ui'
|
||||||
|
|
||||||
|
const message = useMessage()
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
text: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const copyToClipboard = async () => {
|
||||||
|
if (!props.text || props.text.trim() === '') {
|
||||||
|
message.error('Nothing to copy to clipboard')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(props.text)
|
||||||
|
copySuccess()
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error copy to clipboard', err)
|
||||||
|
copyError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function copySuccess() {
|
||||||
|
const displayText = props.text.length > 50
|
||||||
|
? props.text.substring(0, 50) + '...'
|
||||||
|
: props.text
|
||||||
|
message.success(`Copied to clipboard: ${displayText}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyError() {
|
||||||
|
message.error('Nothing to copy to clipboard')
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<span @click="copyToClipboard">
|
||||||
|
<slot />
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
12
src/components/Footer/Footer.vue
Normal file
12
src/components/Footer/Footer.vue
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
<script setup>
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<n-divider class="mt-10" />
|
||||||
|
<p class="text-center text-muted">Merch tracker 2024 - 2025 </p>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
@ -7,7 +7,10 @@ import App from './App.vue'
|
||||||
import router from './router'
|
import router from './router'
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
export const BASE_URL = 'http://localhost:9090/api/v2'
|
export const BASE_URL = 'https://api.nqws.ru/api/v2'
|
||||||
|
// export const BASE_URL = 'http://localhost:9090/api/v2'
|
||||||
|
|
||||||
|
export const BASE_MANDARAKE_LINK = 'https://order.mandarake.co.jp/order/listPage/list?soldOut=1&keyword='
|
||||||
|
|
||||||
app.use(createPinia())
|
app.use(createPinia())
|
||||||
app.use(router)
|
app.use(router)
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,10 @@
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mb-10 {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.mb-20 {
|
.mb-20 {
|
||||||
margin-bottom: 30px;
|
margin-bottom: 30px;
|
||||||
}
|
}
|
||||||
|
|
@ -53,6 +57,10 @@
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text-muted {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
.sticky-search-container {
|
.sticky-search-container {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
|
@ -100,3 +108,15 @@
|
||||||
:deep(.mobile-full-width) {
|
:deep(.mobile-full-width) {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.default-color{
|
||||||
|
color: #18a058;
|
||||||
|
}
|
||||||
|
|
||||||
|
.underline {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-like-text {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,10 @@
|
||||||
import router from '@/router/index.js'
|
import router from '@/router/index.js'
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { useMerchApi } from '@/api/merch.js'
|
import { useMerchApi } from '@/api/merch.js'
|
||||||
|
import { BASE_MANDARAKE_LINK } from '@/main.js'
|
||||||
|
|
||||||
const { addMerch } = useMerchApi()
|
const { addMerch } = useMerchApi()
|
||||||
|
|
||||||
const mandarakeLink = 'https://order.mandarake.co.jp/order/listPage/list?soldOut=1&keyword='
|
|
||||||
|
|
||||||
const name = ref('')
|
const name = ref('')
|
||||||
|
|
||||||
// surugaya block
|
// surugaya block
|
||||||
|
|
@ -17,7 +16,7 @@ const checkAutoComplete = ref(true)
|
||||||
const customLink = ref('')
|
const customLink = ref('')
|
||||||
|
|
||||||
const mandarakeAutocomplete = computed(() => {
|
const mandarakeAutocomplete = computed(() => {
|
||||||
return `${mandarakeLink}${name.value}`
|
return `${BASE_MANDARAKE_LINK}${name.value}`
|
||||||
})
|
})
|
||||||
|
|
||||||
const mandarakeResultLink = computed({
|
const mandarakeResultLink = computed({
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import PeriodSelector from '@/components/PeriodSelector.vue'
|
||||||
import ChartBlock from '@/components/ChartBlock.vue'
|
import ChartBlock from '@/components/ChartBlock.vue'
|
||||||
import { useChartsApi } from '@/api/charts.js'
|
import { useChartsApi } from '@/api/charts.js'
|
||||||
import EditLink from '@/views/DetailsView/EditLink.vue'
|
import EditLink from '@/views/DetailsView/EditLink.vue'
|
||||||
|
import CopyToClipboard from '@/components/CopyToClipboard.vue'
|
||||||
|
|
||||||
const { getMerchDetails, deleteMerch } = useMerchApi()
|
const { getMerchDetails, deleteMerch } = useMerchApi()
|
||||||
const { getDistinctPrices } = useChartsApi()
|
const { getDistinctPrices } = useChartsApi()
|
||||||
|
|
@ -17,6 +18,11 @@ const props = defineProps({
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const editing = ref({
|
||||||
|
surugaya: false,
|
||||||
|
mandarake: false,
|
||||||
|
})
|
||||||
|
|
||||||
const merchDetails = ref(null)
|
const merchDetails = ref(null)
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const error = ref(null)
|
const error = ref(null)
|
||||||
|
|
@ -76,11 +82,15 @@ const fetchPrices = async (days = 7) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleLinkUpdate(origin, newLink) {
|
||||||
|
merchDetails.value[`origin_${origin}`].link = newLink
|
||||||
|
editing.value[origin] = false
|
||||||
|
}
|
||||||
|
|
||||||
function handleSelectDays(days) {
|
function handleSelectDays(days) {
|
||||||
fetchPrices(days)
|
fetchPrices(days)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchMerch()
|
fetchMerch()
|
||||||
fetchPrices(7)
|
fetchPrices(7)
|
||||||
|
|
@ -102,20 +112,71 @@ onMounted(() => {
|
||||||
<ChartBlock :charts-data="prices" />
|
<ChartBlock :charts-data="prices" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<n-divider title-placement="left">Surugaya</n-divider>
|
<!-- Surugaya -->
|
||||||
|
<CopyToClipboard :text="merchDetails.origin_surugaya?.link">
|
||||||
|
<n-divider title-placement="left">Surugaya</n-divider>
|
||||||
|
</CopyToClipboard>
|
||||||
|
<div v-if="!editing.surugaya">
|
||||||
|
<template v-if="merchDetails.origin_surugaya?.link">
|
||||||
|
<a
|
||||||
|
:href="merchDetails.origin_surugaya.link"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
class="default-color"
|
||||||
|
>
|
||||||
|
{{ merchDetails.origin_surugaya.link }}
|
||||||
|
</a>
|
||||||
|
<n-button type="primary" style="margin-left: 8px" @click="editing.surugaya = true">
|
||||||
|
Edit link
|
||||||
|
</n-button>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<span class="default-color underline link-like-text" @click="editing.surugaya = true">Add link</span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
<EditLink
|
<EditLink
|
||||||
|
v-else
|
||||||
:merch-uuid="merch_uuid"
|
:merch-uuid="merch_uuid"
|
||||||
origin="surugaya"
|
origin="surugaya"
|
||||||
:name="merchDetails.name"
|
:name="merchDetails.name"
|
||||||
v-model="merchDetails.origin_surugaya.link" />
|
:model-value="merchDetails.origin_surugaya?.link || ''"
|
||||||
|
@update:model-value="handleLinkUpdate('surugaya', $event)"
|
||||||
|
@cancel-edit="editing.surugaya = false"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Mandarake -->
|
||||||
|
<CopyToClipboard :text="merchDetails.origin_mandarake?.link">
|
||||||
|
<n-divider title-placement="left">Mandarake</n-divider>
|
||||||
|
</CopyToClipboard>
|
||||||
|
<div v-if="!editing.mandarake">
|
||||||
|
<template v-if="merchDetails.origin_mandarake?.link">
|
||||||
|
<a
|
||||||
|
:href="merchDetails.origin_mandarake.link"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
class="default-color"
|
||||||
|
>
|
||||||
|
{{ merchDetails.origin_mandarake.link }}
|
||||||
|
</a>
|
||||||
|
<n-button type="primary" style="margin-left: 8px" @click="editing.mandarake = true">
|
||||||
|
Edit link
|
||||||
|
</n-button>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<span class="default-color underline link-like-text" @click="editing.mandarake = true">Add link</span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
<n-divider title-placement="left">Mandarake</n-divider>
|
|
||||||
<EditLink
|
<EditLink
|
||||||
|
v-else
|
||||||
:merch-uuid="merch_uuid"
|
:merch-uuid="merch_uuid"
|
||||||
origin="mandarake"
|
origin="mandarake"
|
||||||
:name="merchDetails.name"
|
:name="merchDetails.name"
|
||||||
v-model="merchDetails.origin_mandarake.link" />
|
:model-value="merchDetails.origin_mandarake?.link || ''"
|
||||||
|
@update:model-value="handleLinkUpdate('mandarake', $event)"
|
||||||
|
@cancel-edit="editing.mandarake = false"
|
||||||
|
/>
|
||||||
</n-card>
|
</n-card>
|
||||||
<div v-else>Not found</div>
|
<div v-else>Not found</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,26 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, nextTick, watch, computed } from 'vue'
|
import { ref, nextTick } from 'vue'
|
||||||
import { useMerchApi } from '@/api/merch.js'
|
import { useMerchApi } from '@/api/merch.js'
|
||||||
|
import { BASE_MANDARAKE_LINK } from '@/main.js'
|
||||||
|
|
||||||
const { updateMerch } = useMerchApi()
|
const { updateMerch } = useMerchApi()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true,
|
||||||
},
|
|
||||||
type: {
|
|
||||||
type: String,
|
|
||||||
default: 'text'
|
|
||||||
},
|
},
|
||||||
placeholder: {
|
placeholder: {
|
||||||
type: String,
|
type: String,
|
||||||
default: ''
|
default: 'Enter link here...',
|
||||||
},
|
|
||||||
emptyText: {
|
|
||||||
type: String,
|
|
||||||
default: 'Add link'
|
|
||||||
},
|
},
|
||||||
merchUuid: {
|
merchUuid: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true,
|
||||||
},
|
},
|
||||||
name: {
|
name: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true,
|
||||||
},
|
},
|
||||||
origin: {
|
origin: {
|
||||||
type: String,
|
type: String,
|
||||||
|
|
@ -35,32 +28,31 @@ const props = defineProps({
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['update:modelValue'])
|
const emit = defineEmits(['update:modelValue', 'cancel-edit'])
|
||||||
|
|
||||||
const isEditing = ref(false)
|
|
||||||
const tempValue = ref('')
|
const tempValue = ref('')
|
||||||
const inputRef = ref(null)
|
const inputRef = ref(null)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
|
||||||
const displayValue = computed(() => {
|
tempValue.value = props.modelValue
|
||||||
const val = props.modelValue.trim()
|
|
||||||
return val === '' || val == null ? props.emptyText : props.modelValue
|
|
||||||
})
|
|
||||||
|
|
||||||
const startEditing = () => {
|
nextTick(() => {
|
||||||
tempValue.value = props.modelValue
|
inputRef.value?.focus?.()
|
||||||
isEditing.value = true
|
})
|
||||||
nextTick(() => {
|
|
||||||
inputRef.value?.focus?.()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const save = async () => {
|
const save = async () => {
|
||||||
const newValue = tempValue.value.trim()
|
const newValue = tempValue.value.trim()
|
||||||
if (newValue !== props.modelValue) {
|
|
||||||
emit('update:modelValue', newValue)
|
if (newValue === '') {
|
||||||
|
emit('cancel-edit')
|
||||||
|
return
|
||||||
}
|
}
|
||||||
isEditing.value = false
|
|
||||||
|
if (newValue === props.modelValue.trim()) {
|
||||||
|
emit('cancel-edit')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
loading.value = true
|
loading.value = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -68,7 +60,7 @@ const save = async () => {
|
||||||
merch_uuid: props.merchUuid,
|
merch_uuid: props.merchUuid,
|
||||||
name: props.name,
|
name: props.name,
|
||||||
origin: props.origin,
|
origin: props.origin,
|
||||||
link: newValue
|
link: newValue,
|
||||||
})
|
})
|
||||||
|
|
||||||
emit('update:modelValue', newValue)
|
emit('update:modelValue', newValue)
|
||||||
|
|
@ -76,63 +68,91 @@ const save = async () => {
|
||||||
console.error('Update link error:', error)
|
console.error('Update link error:', error)
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
isEditing.value = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const showModal = ref(false)
|
||||||
|
const clearLink = async () => {
|
||||||
|
showModal.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmClearLink = async () => {
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
await updateMerch({
|
||||||
|
merch_uuid: props.merchUuid,
|
||||||
|
name: props.name,
|
||||||
|
origin: props.origin,
|
||||||
|
link: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
emit('update:modelValue', '')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Update link error:', error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
showModal.value = false
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
|
||||||
const cancel = () => {
|
const cancel = () => {
|
||||||
isEditing.value = false
|
emit('cancel-edit')
|
||||||
|
}
|
||||||
|
|
||||||
|
const insertAutoCompletedLink = () => {
|
||||||
|
tempValue.value = BASE_MANDARAKE_LINK+props.name
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(() => props.modelValue, (newVal) => {
|
|
||||||
if (isEditing.value) {
|
|
||||||
tempValue.value = newVal
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<div v-if="props.origin === 'mandarake'" class="center-button-container mb-10">
|
||||||
<span v-if="!isEditing" @click="startEditing" class="editable-text">
|
<n-button type="warning" class="center-button" @click="insertAutoCompletedLink"
|
||||||
{{ displayValue }}
|
>Insert auto-completed link</n-button
|
||||||
</span>
|
>
|
||||||
<div v-else class="editing-area">
|
</div>
|
||||||
|
<div class="editing-area">
|
||||||
<n-input
|
<n-input
|
||||||
v-model:value="tempValue"
|
v-model:value="tempValue"
|
||||||
type="text"
|
type="textarea"
|
||||||
size="small"
|
size="large"
|
||||||
ref="inputRef"
|
ref="inputRef"
|
||||||
@keyup.enter="save"
|
@keyup.enter="save"
|
||||||
@keyup.esc="cancel"
|
@keyup.esc="cancel"
|
||||||
:placeholder="placeholder || 'Enter link here...'"
|
:placeholder="placeholder"
|
||||||
|
:loading="loading"
|
||||||
/>
|
/>
|
||||||
<n-button size="small" type="primary" @click="save">✓</n-button>
|
<n-button size="small" type="primary" :loading="loading" @click="save">Save</n-button>
|
||||||
<n-button size="small" @click="cancel">✕</n-button>
|
<n-button size="small" @click="cancel">Cancel</n-button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="center-button-container">
|
||||||
|
<n-button type="error" class="center-button" @click="clearLink">Clear link</n-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<n-modal v-model:show="showModal" preset="dialog" title="Confirmation">
|
||||||
|
<template #default>
|
||||||
|
<p>Confirm clear link</p>
|
||||||
|
</template>
|
||||||
|
<template #action>
|
||||||
|
<n-button @click="confirmClearLink" type="error">Clear</n-button>
|
||||||
|
<n-button @click="showModal = false">Cancel</n-button>
|
||||||
|
</template>
|
||||||
|
</n-modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.editable-text {
|
|
||||||
cursor: pointer;
|
|
||||||
text-decoration: underline;
|
|
||||||
color: #18a058;
|
|
||||||
padding: 2px 4px;
|
|
||||||
border-radius: 4px;
|
|
||||||
transition: background 0.2s;
|
|
||||||
min-height: 1.2em;
|
|
||||||
display: inline-block;
|
|
||||||
min-width: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editable-text:hover {
|
|
||||||
background-color: #f0f9ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editing-area {
|
.editing-area {
|
||||||
display: inline-flex;
|
display: flex;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
vertical-align: middle;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editing-area :deep(.n-input) {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 200px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue